mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-06-13 07:10:26 +08:00
Adding source control sample
This commit is contained in:
107
source-control-sample/src/extension.ts
Normal file
107
source-control-sample/src/extension.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
import * as vscode from 'vscode';
|
||||
import { JSFIDDLE_SCHEME } from './fiddleRepository';
|
||||
import { FiddleSourceControl } from './fiddleSourceControl';
|
||||
import { JSFiddleDocumentContentProvider } from './fiddleDocumentContentProvider';
|
||||
import * as path from 'path';
|
||||
import { unlinkSync, readdirSync } from 'fs';
|
||||
|
||||
const SOURCE_CONTROL_OPEN_COMMAND = 'extension.source-control.open';
|
||||
|
||||
// this method is called when your extension is activated
|
||||
// your extension is activated the very first time the command is executed
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Congratulations, your extension "source-control-sample" is now active!');
|
||||
|
||||
let fiddleId = context.globalState.get(FIDDLE_ID_TO_OPEN);
|
||||
if (fiddleId) {
|
||||
// new workspace folder was open and the extension needs to continue opening the fiddle files into it
|
||||
vscode.commands.executeCommand(SOURCE_CONTROL_OPEN_COMMAND, fiddleId);
|
||||
context.globalState.update(FIDDLE_ID_TO_OPEN, undefined);
|
||||
}
|
||||
|
||||
const jsFiddleDocumentContentProvider = new JSFiddleDocumentContentProvider();
|
||||
let fiddleSourceControl: FiddleSourceControl = null;
|
||||
|
||||
let disposable = vscode.commands.registerCommand(SOURCE_CONTROL_OPEN_COMMAND, async (id?: string) => {
|
||||
|
||||
if (fiddleSourceControl) vscode.window.showErrorMessage("Another Fiddle was already open in this workspace. Open a new workspace first.");
|
||||
|
||||
if (!id) {
|
||||
id = await vscode.window.showInputBox({ prompt: 'Paste JSFiddle ID and optionally version', placeHolder: 'hash or hash/version, e.g. u8B29/1', value: 'u8B29/1' });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
let workspaceFolder = await clearWorkspaceFolder(await getWorkspaceFolder(context, id));
|
||||
if (!workspaceFolder) return; // canceled by user
|
||||
|
||||
// show the file explorer with the three new files
|
||||
vscode.commands.executeCommand("workbench.view.explorer");
|
||||
|
||||
// register source control
|
||||
let fiddleSourceControl = await FiddleSourceControl.fromFiddle(id, context, workspaceFolder);
|
||||
|
||||
// update the fiddle document content provider with the latest content
|
||||
jsFiddleDocumentContentProvider.updated(fiddleSourceControl.fiddle);
|
||||
|
||||
// every time the repository is updated with new fiddle version, notify the content provider
|
||||
fiddleSourceControl.onRepositoryChange(fiddle => jsFiddleDocumentContentProvider.updated(fiddle));
|
||||
|
||||
context.subscriptions.push(fiddleSourceControl);
|
||||
}
|
||||
catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
console.log(ex);
|
||||
}
|
||||
});
|
||||
|
||||
vscode.workspace.registerTextDocumentContentProvider(JSFIDDLE_SCHEME, jsFiddleDocumentContentProvider);
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
const FIDDLE_ID_TO_OPEN = "fiddleIdToOpen";
|
||||
|
||||
async function getWorkspaceFolder(context: vscode.ExtensionContext, fiddleId: String): Promise<vscode.WorkspaceFolder> {
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) {
|
||||
return await vscode.window.showWorkspaceFolderPick({ placeHolder: 'Pick workspace folder to create files in.' });
|
||||
}
|
||||
else if (!vscode.workspace.workspaceFolders) {
|
||||
let folderUris = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select folder' });
|
||||
if (!folderUris) {
|
||||
return null;
|
||||
}
|
||||
vscode.workspace.updateWorkspaceFolders(0, 0, { uri: folderUris[0] });
|
||||
context.globalState.update(FIDDLE_ID_TO_OPEN, fiddleId);
|
||||
return null; // the extension will get reloaded in the context of the newly open workspace
|
||||
}
|
||||
else {
|
||||
return vscode.workspace.workspaceFolders[0];
|
||||
}
|
||||
}
|
||||
|
||||
async function clearWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder): Promise<vscode.WorkspaceFolder> {
|
||||
|
||||
if (!workspaceFolder) return null;
|
||||
|
||||
// check if the workspace is empty, or clear it
|
||||
let existingWorkspaceFiles = readdirSync(workspaceFolder.uri.fsPath);
|
||||
if (existingWorkspaceFiles.length > 0) {
|
||||
let answer = await vscode.window.showQuickPick(["Yes", "No"],
|
||||
{ placeHolder: `Remove ${existingWorkspaceFiles.length} file(s) from the workspace before cloning the remote repository?` });
|
||||
if (answer === undefined) return null;
|
||||
|
||||
if (answer === "Yes") {
|
||||
existingWorkspaceFiles
|
||||
.map(filename =>
|
||||
unlinkSync(path.join(workspaceFolder.uri.fsPath, filename)));
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceFolder;
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() { }
|
||||
36
source-control-sample/src/fiddleDocumentContentProvider.ts
Normal file
36
source-control-sample/src/fiddleDocumentContentProvider.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { CancellationToken, ProviderResult, TextDocumentContentProvider, Event, Uri, EventEmitter, Disposable } from "vscode";
|
||||
import { toExtension, JSFIDDLE_SCHEME, Fiddle } from "./fiddleRepository";
|
||||
|
||||
/**
|
||||
* Provides the content of the JS Fiddle documents as fetched from the server i.e. without the local edits.
|
||||
* This is used for the source control diff.
|
||||
*/
|
||||
export class JSFiddleDocumentContentProvider implements TextDocumentContentProvider, Disposable {
|
||||
private _onDidChange = new EventEmitter<Uri>();
|
||||
private fiddle: Fiddle;
|
||||
|
||||
get onDidChange(): Event<Uri> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
updated(newFiddle: Fiddle): void {
|
||||
this.fiddle = newFiddle;
|
||||
|
||||
// let's assume all 3 documents actually changed and notify the quick-diff
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${this.fiddle.hash}.html`));
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${this.fiddle.hash}.css`));
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${this.fiddle.hash}.js`));
|
||||
}
|
||||
|
||||
provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult<string> {
|
||||
if (token.isCancellationRequested) return "Canceled";
|
||||
|
||||
let fiddlePart = toExtension(uri);
|
||||
|
||||
return this.fiddle.data[fiddlePart];
|
||||
}
|
||||
}
|
||||
108
source-control-sample/src/fiddleRepository.ts
Normal file
108
source-control-sample/src/fiddleRepository.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import JSFiddle = require("jsfiddle");
|
||||
import { QuickDiffProvider, Uri, CancellationToken, ProviderResult, WorkspaceFolder, workspace, TextDocument } from "vscode";
|
||||
import * as path from 'path';
|
||||
|
||||
export class Fiddle {
|
||||
constructor(public hash: string, public version: number, public data: FiddleData) { }
|
||||
}
|
||||
|
||||
export interface FiddleData {
|
||||
html: string;
|
||||
js: string;
|
||||
css: string;
|
||||
}
|
||||
|
||||
export function areIdentical(first: FiddleData, second: FiddleData): boolean {
|
||||
return first.html == second.html
|
||||
&& first.css == second.css
|
||||
&& first.js == second.js;
|
||||
}
|
||||
|
||||
export const JSFIDDLE_SCHEME = 'jsfiddle';
|
||||
|
||||
export class FiddleRepository implements QuickDiffProvider {
|
||||
|
||||
constructor(private workspaceFolder: WorkspaceFolder, private fiddleHash: string) { }
|
||||
|
||||
provideOriginalResource?(uri: Uri, token: CancellationToken): ProviderResult<Uri> {
|
||||
// converts the local file uri to jsfiddle:file.ext
|
||||
let relativePath = workspace.asRelativePath(uri.fsPath);
|
||||
return Uri.parse(`${JSFIDDLE_SCHEME}:${relativePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFiddle(hash: string, version: number | undefined): Promise<Fiddle> {
|
||||
|
||||
if (hash === "demo") {
|
||||
let maxDemoVersion = 1;
|
||||
if (version === undefined) version = maxDemoVersion;
|
||||
|
||||
if (version <= maxDemoVersion) {
|
||||
let fiddleData: FiddleData = {
|
||||
html: '<div class="hi">Hi</div>',
|
||||
css: `.hi {\n color: red;\n}`,
|
||||
js: '$(".hi").fadeOut();'
|
||||
};
|
||||
return new Fiddle(hash, version, fiddleData);
|
||||
}
|
||||
else {
|
||||
throw "Invalid demo fiddle version.";
|
||||
}
|
||||
}
|
||||
|
||||
let id = toFiddleId(hash, version);
|
||||
|
||||
return new Promise<Fiddle>((resolve, reject) => {
|
||||
JSFiddle.getFiddle(id, (err: any, fiddleData: any) => {
|
||||
// handle error
|
||||
if (err) reject(err);
|
||||
|
||||
let fiddle = new Fiddle(hash, version, fiddleData);
|
||||
|
||||
resolve(fiddle);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadFiddle(hash: string, version: number, html: string, js: string, css: string): Promise<Fiddle> {
|
||||
|
||||
if (hash === "demo") {
|
||||
return new Fiddle(hash, version, { html: html, js: js, css: css });
|
||||
}
|
||||
|
||||
let data = {
|
||||
slug: hash,
|
||||
version: version,
|
||||
html: html,
|
||||
js: js,
|
||||
css: css
|
||||
};
|
||||
|
||||
return new Promise<Fiddle>((resolve, reject) => {
|
||||
JSFiddle.saveFiddle(data, (err: any, fiddleData: any) => {
|
||||
// handle error
|
||||
if (err) reject(err);
|
||||
|
||||
let fiddle = new Fiddle(hash, version, fiddleData);
|
||||
|
||||
resolve(fiddle);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toFiddleId(hash: string, version: number | undefined): string {
|
||||
if (version === undefined) {
|
||||
return hash;
|
||||
}
|
||||
else {
|
||||
return hash + '/' + version;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets extension trimming the dot character.
|
||||
* @param uri document uri
|
||||
*/
|
||||
export function toExtension(uri: Uri): string {
|
||||
return path.extname(uri.fsPath).substr(1);
|
||||
}
|
||||
250
source-control-sample/src/fiddleSourceControl.ts
Normal file
250
source-control-sample/src/fiddleSourceControl.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FiddleData, FiddleRepository, toExtension, downloadFiddle, areIdentical, uploadFiddle, Fiddle } from './fiddleRepository';
|
||||
import * as path from 'path';
|
||||
import { writeFileSync, existsSync, unlinkSync } from 'fs';
|
||||
|
||||
export class FiddleSourceControl implements vscode.Disposable {
|
||||
jsFiddleScm: vscode.SourceControl;
|
||||
changedResources: vscode.SourceControlResourceGroup;
|
||||
fiddleRepository: FiddleRepository;
|
||||
latestFiddleVersion: number = Number.POSITIVE_INFINITY; // until actual value is established
|
||||
private _onRepositoryChange = new vscode.EventEmitter<Fiddle>();
|
||||
|
||||
constructor(context: vscode.ExtensionContext, private workspaceFolder: vscode.WorkspaceFolder, private documents: vscode.TextDocument[], public fiddle: Fiddle) {
|
||||
this.jsFiddleScm = vscode.scm.createSourceControl('jsfiddle', 'JSFiddle #' + fiddle.hash, workspaceFolder.uri);
|
||||
this.changedResources = this.jsFiddleScm.createResourceGroup('workingTree', 'Changes');
|
||||
this.fiddleRepository = new FiddleRepository(workspaceFolder, fiddle.hash);
|
||||
this.jsFiddleScm.quickDiffProvider = this.fiddleRepository;
|
||||
this.refreshStatusBar();
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(e => this.updateChangedGroup(e)));
|
||||
context.subscriptions.push(this.jsFiddleScm);
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.refresh", () => this.refresh()));
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.discard", () => this.resetFilesToCheckedOutVersion()));
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.commit", () => this.commitAll()));
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.checkout", newVersion => this.tryCheckout(newVersion)));
|
||||
|
||||
if (Number.isNaN(this.fiddle.version)) {
|
||||
this.establishVersion();
|
||||
} else {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
static async fromFiddle(id: string, context: vscode.ExtensionContext, workspaceFolder: vscode.WorkspaceFolder): Promise<FiddleSourceControl> {
|
||||
let idFragments = id.split('/');
|
||||
let fiddleHash = idFragments[0];
|
||||
let fiddleVersion = idFragments.length > 1 ? parseInt(id.split('/')[1]) : undefined;
|
||||
|
||||
let fiddle = await downloadFiddle(fiddleHash, fiddleVersion);
|
||||
|
||||
let workspacePath = workspaceFolder.uri.fsPath;
|
||||
|
||||
let documents = [
|
||||
await createDocument(fiddle.data.html, path.join(workspacePath, fiddleHash + '.html'), vscode.ViewColumn.One),
|
||||
await createDocument(fiddle.data.js, path.join(workspacePath, fiddleHash + '.js'), vscode.ViewColumn.Two),
|
||||
await createDocument(fiddle.data.css, path.join(workspacePath, fiddleHash + '.css'), vscode.ViewColumn.Three)
|
||||
];
|
||||
|
||||
return new FiddleSourceControl(context, workspaceFolder, documents, fiddle);
|
||||
}
|
||||
|
||||
private refreshStatusBar() {
|
||||
this.jsFiddleScm.statusBarCommands = [
|
||||
{
|
||||
"command": "extension.source-control.checkout",
|
||||
"title": `↕ ${this.fiddle.hash} #${this.fiddle.version} / ${this.latestFiddleVersion}`,
|
||||
"tooltip": "Checkout another version of this fiddle.",
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async commitAll(): Promise<void> {
|
||||
if (this.fiddle.version < this.latestFiddleVersion) {
|
||||
vscode.window.showErrorMessage("Checkout the latest fiddle version before committing your chanes.");
|
||||
}
|
||||
else {
|
||||
let answer = await vscode.window.showQuickPick(["Yes, upload the changes to JS Fiddle.", "No, I was just clicking around."],
|
||||
{ placeHolder: "Are you sure you want to commit?" });
|
||||
|
||||
if (answer && answer.toLowerCase().startsWith("yes")) {
|
||||
let html = this.documents.find(doc => path.extname(doc.fileName) == ".html").getText();
|
||||
let js = this.documents.find(doc => path.extname(doc.fileName) == ".js").getText();
|
||||
let css = this.documents.find(doc => path.extname(doc.fileName) == ".css").getText();
|
||||
|
||||
// here we assume nobody updated the Fiddle on the server since we refreshed the list of versions
|
||||
let newFiddle = await uploadFiddle(this.fiddle.hash, this.fiddle.version + 1, html, js, css);
|
||||
this.setFiddle(newFiddle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws away all local changes and resets all files to the checked out version of the repository.
|
||||
*/
|
||||
resetFilesToCheckedOutVersion(): void {
|
||||
this.resetFile('html');
|
||||
this.resetFile('css');
|
||||
this.resetFile('js');
|
||||
}
|
||||
|
||||
private resetFile(extension: string) {
|
||||
let filePath = path.join(this.workspaceFolder.uri.fsPath, this.fiddle.hash + '.' + extension);
|
||||
writeFileSync(filePath, this.fiddle.data[extension]);
|
||||
}
|
||||
|
||||
async tryCheckout(newVersion: number | undefined): Promise<void> {
|
||||
if (!Number.isFinite(this.latestFiddleVersion)) return void 0;
|
||||
|
||||
if (newVersion === undefined) {
|
||||
let allVersions = [...Array(this.latestFiddleVersion).keys()]
|
||||
.map(n => n + 1)
|
||||
.map(ver => new VersionQuickPickItem(ver, ver == this.fiddle.version));
|
||||
let newVersionPick = await vscode.window.showQuickPick(allVersions, { canPickMany: false, placeHolder: 'Select a version...' });
|
||||
if (newVersionPick) {
|
||||
newVersion = newVersionPick.version;
|
||||
}
|
||||
else {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.changedResources.resourceStates.length) {
|
||||
let changedResourcesCount = this.changedResources.resourceStates.length;
|
||||
vscode.window.showErrorMessage(`There is one or more changed resources. Discard or commit your local changes before checking out another version.`);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
let newFiddle = await downloadFiddle(this.fiddle.hash, newVersion);
|
||||
this.setFiddle(newFiddle);
|
||||
} catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setFiddle(newFiddle: Fiddle) {
|
||||
if (newFiddle.version > this.latestFiddleVersion) this.latestFiddleVersion = newFiddle.version;
|
||||
this.fiddle = newFiddle;
|
||||
this.resetFilesToCheckedOutVersion(); // overwrite local file content
|
||||
this._onRepositoryChange.fire(this.fiddle);
|
||||
this.refreshStatusBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh is used when the information on the server may have changed.
|
||||
* For example another user updates the Fiddle online.
|
||||
*/
|
||||
async refresh(): Promise<void> {
|
||||
let latestVersion = this.fiddle.version;
|
||||
while (true) {
|
||||
try {
|
||||
latestVersion++;
|
||||
let latestFiddle = await downloadFiddle(this.fiddle.hash, latestVersion);
|
||||
} catch (ex) {
|
||||
// typically the ex.statusCode == 404, when there is no further version
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.latestFiddleVersion = latestVersion - 1;
|
||||
this.refreshStatusBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which version was checked out and finds the index of the latest version.
|
||||
*/
|
||||
async establishVersion(): Promise<void> {
|
||||
let latestVersion = 0;
|
||||
let currentFiddle: Fiddle = undefined;
|
||||
while (true) {
|
||||
try {
|
||||
latestVersion++;
|
||||
let latestFiddle = await downloadFiddle(this.fiddle.hash, latestVersion);
|
||||
if (areIdentical(this.fiddle.data, latestFiddle.data)) {
|
||||
currentFiddle = latestFiddle;
|
||||
}
|
||||
} catch (ex) {
|
||||
// typically the ex.statusCode == 404, when there is no further version
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.latestFiddleVersion = latestVersion - 1;
|
||||
|
||||
// now we know the version of the current fiddle, let's set it
|
||||
this.setFiddle(currentFiddle);
|
||||
}
|
||||
|
||||
|
||||
get onRepositoryChange(): vscode.Event<Fiddle> {
|
||||
return this._onRepositoryChange.event;
|
||||
}
|
||||
|
||||
updateChangedGroup(e: vscode.TextDocumentChangeEvent): any {
|
||||
this.changedResources.resourceStates = this.documents
|
||||
.filter(doc => this.isDirty(doc))
|
||||
.map(doc => this.toSourceControlResourceState(doc));
|
||||
}
|
||||
|
||||
isDirty(doc: vscode.TextDocument): boolean {
|
||||
let originalText = this.fiddle.data[toExtension(doc.uri)];
|
||||
return originalText.replace('\r', '') != doc.getText().replace('\r', '');
|
||||
}
|
||||
|
||||
toSourceControlResourceState(doc: vscode.TextDocument): vscode.SourceControlResourceState {
|
||||
|
||||
let repositoryUri = this.fiddleRepository.provideOriginalResource(doc.uri, null);
|
||||
|
||||
const fiddlePart = toExtension(doc.uri).toUpperCase();
|
||||
return {
|
||||
resourceUri: doc.uri,
|
||||
command: {
|
||||
title: "Show changes",
|
||||
command: "vscode.diff",
|
||||
arguments: [repositoryUri, doc.uri, `JSFiddle#${this.fiddle.hash} ${fiddlePart} ↔ Local changes`],
|
||||
tooltip: "Diff your changes"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onRepositoryChange.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class VersionQuickPickItem implements vscode.QuickPickItem {
|
||||
label: string;
|
||||
description?: string;
|
||||
detail?: string;
|
||||
alwaysShow?: boolean;
|
||||
|
||||
constructor(public version: number, public picked: boolean) {
|
||||
this.label = this.version.toString();
|
||||
this.alwaysShow = picked;
|
||||
|
||||
if (picked) {
|
||||
this.description = "Currently checked out.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function createDocument(content: string, fileName: string, column: vscode.ViewColumn): Promise<vscode.TextDocument> {
|
||||
if (existsSync(fileName)) {
|
||||
unlinkSync(fileName);
|
||||
}
|
||||
let fileUri = vscode.Uri.file(fileName).with({scheme: 'untitled'});
|
||||
let doc = await vscode.workspace.openTextDocument(fileUri);
|
||||
|
||||
let edit = new vscode.WorkspaceEdit();
|
||||
edit.insert(doc.uri, new vscode.Position(0, 0), content);
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
await doc.save();
|
||||
|
||||
// now that the document is saved, let's get the document through the 'file' schema, not 'untitled'
|
||||
doc = await vscode.workspace.openTextDocument(vscode.Uri.file(fileName));
|
||||
|
||||
await vscode.window.showTextDocument(doc, { viewColumn: column});
|
||||
return doc;
|
||||
}
|
||||
Reference in New Issue
Block a user