mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-04-27 16:55:44 +08:00
- support for multi workspace folders
- mock fiddle for offline testing - redesigned to support files closing - support for deleted files
This commit is contained in:
@ -2,84 +2,196 @@
|
||||
// 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 { FiddleSourceControl, CONFIGURATION_FILE } from './fiddleSourceControl';
|
||||
import { JSFiddleDocumentContentProvider } from './fiddleDocumentContentProvider';
|
||||
import * as path from 'path';
|
||||
import { unlinkSync, readdirSync } from 'fs';
|
||||
import { unlinkSync, readdirSync, existsSync, exists, readFile } from 'fs';
|
||||
import { FiddleConfiguration, parseFiddleId } from './fiddleConfiguration';
|
||||
import { firstIndex } from './util';
|
||||
|
||||
const SOURCE_CONTROL_OPEN_COMMAND = 'extension.source-control.open';
|
||||
var jsFiddleDocumentContentProvider: JSFiddleDocumentContentProvider;
|
||||
var fiddleSourceControlRegister = new Map<vscode.Uri, FiddleSourceControl>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
jsFiddleDocumentContentProvider = new JSFiddleDocumentContentProvider();
|
||||
|
||||
const jsFiddleDocumentContentProvider = new JSFiddleDocumentContentProvider();
|
||||
let fiddleSourceControl: FiddleSourceControl = null;
|
||||
initializeFromConfigurationFile(context);
|
||||
|
||||
let disposable = vscode.commands.registerCommand(SOURCE_CONTROL_OPEN_COMMAND, async (id?: string) => {
|
||||
let openCommand = vscode.commands.registerCommand(SOURCE_CONTROL_OPEN_COMMAND,
|
||||
(fiddleId?: string, workspaceUri?: vscode.Uri) => {
|
||||
tryOpenFiddle(context, fiddleId, workspaceUri);
|
||||
});
|
||||
context.subscriptions.push(openCommand);
|
||||
|
||||
if (fiddleSourceControl) vscode.window.showErrorMessage("Another Fiddle was already open in this workspace. Open a new workspace first.");
|
||||
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(JSFIDDLE_SCHEME, jsFiddleDocumentContentProvider));
|
||||
|
||||
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);
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.refresh", async () => {
|
||||
let sourceControl = await pickSourceControl();
|
||||
if (sourceControl) sourceControl.refresh();
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.discard", async () => {
|
||||
let sourceControl = await pickSourceControl();
|
||||
if (sourceControl) sourceControl.resetFilesToCheckedOutVersion();
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.commit", async () => {
|
||||
let sourceControl = await pickSourceControl();
|
||||
if (sourceControl) sourceControl.commitAll();
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand("extension.source-control.checkout",
|
||||
async (sourceControl: FiddleSourceControl, newVersion?: number) => {
|
||||
sourceControl = sourceControl || await pickSourceControl();
|
||||
if (sourceControl) sourceControl.tryCheckout(newVersion);
|
||||
}));
|
||||
}
|
||||
|
||||
const FIDDLE_ID_TO_OPEN = "fiddleIdToOpen";
|
||||
async function pickSourceControl(): Promise<FiddleSourceControl> {
|
||||
// todo: when/if the SourceControl exposes a 'selected' property, use that instead
|
||||
|
||||
if (fiddleSourceControlRegister.size == 0) return undefined;
|
||||
else if (fiddleSourceControlRegister.size == 1) return [...fiddleSourceControlRegister.values()][0];
|
||||
else {
|
||||
|
||||
let picks = [...fiddleSourceControlRegister.values()].map(fsc => new RepositoryPick(fsc));
|
||||
|
||||
if (vscode.window.activeTextEditor) {
|
||||
let activeWorkspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri);
|
||||
let activeSourceControl = activeWorkspaceFolder && fiddleSourceControlRegister.get(activeWorkspaceFolder.uri);
|
||||
let activeIndex = firstIndex(picks, pick => pick.fiddleSourceControl === activeSourceControl);
|
||||
|
||||
// if there is an active editor, move its folder to be the first in the pick list
|
||||
if (activeIndex > -1) {
|
||||
picks.unshift(...picks.splice(activeIndex, 1));
|
||||
}
|
||||
}
|
||||
|
||||
const pick = await vscode.window.showQuickPick(picks, { placeHolder: 'Select repository' });
|
||||
return pick && pick.fiddleSourceControl;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryOpenFiddle(context: vscode.ExtensionContext, fiddleId?: string, workspaceUri?: vscode.Uri): Promise<void> {
|
||||
try {
|
||||
await openFiddle(context, fiddleId, workspaceUri);
|
||||
}
|
||||
catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
console.log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
async function openFiddle(context: vscode.ExtensionContext, fiddleId?: string, workspaceUri?: vscode.Uri) {
|
||||
if (fiddleSourceControlRegister.has(workspaceUri)) vscode.window.showErrorMessage("Another Fiddle was already open in this workspace. Open a new workspace first.");
|
||||
|
||||
if (!fiddleId) {
|
||||
fiddleId = await vscode.window.showInputBox({ prompt: 'Paste JSFiddle ID and optionally version', placeHolder: 'slug or slug/version, e.g. u8B29/1', value: 'demo' });
|
||||
}
|
||||
|
||||
let workspaceFolder: vscode.WorkspaceFolder =
|
||||
workspaceUri ?
|
||||
vscode.workspace.getWorkspaceFolder(workspaceUri) :
|
||||
await selectWorkspaceFolder(context, fiddleId);
|
||||
|
||||
workspaceFolder = await clearWorkspaceFolder(workspaceFolder);
|
||||
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.fromFiddleId(fiddleId, context, workspaceFolder, true);
|
||||
|
||||
registerFiddleSourceControl(fiddleSourceControl, context);
|
||||
|
||||
// open the 3 fiddle parts in 3 view columns
|
||||
await openDocumentInColumn(fiddleSourceControl.getRepository().createLocalResourcePath('html'), vscode.ViewColumn.One);
|
||||
await openDocumentInColumn(fiddleSourceControl.getRepository().createLocalResourcePath('js'), vscode.ViewColumn.Two);
|
||||
await openDocumentInColumn(fiddleSourceControl.getRepository().createLocalResourcePath('css'), vscode.ViewColumn.Three);
|
||||
}
|
||||
|
||||
function registerFiddleSourceControl(fiddleSourceControl: FiddleSourceControl, context: vscode.ExtensionContext) {
|
||||
// update the fiddle document content provider with the latest content
|
||||
jsFiddleDocumentContentProvider.updated(fiddleSourceControl.getFiddle());
|
||||
|
||||
// every time the repository is updated with new fiddle version, notify the content provider
|
||||
fiddleSourceControl.onRepositoryChange(fiddle => jsFiddleDocumentContentProvider.updated(fiddle));
|
||||
|
||||
if (fiddleSourceControlRegister.has(fiddleSourceControl.getWorkspaceFolder().uri)) {
|
||||
// the folder was already under source control
|
||||
let previousSourceControl = fiddleSourceControlRegister.get(fiddleSourceControl.getWorkspaceFolder().uri);
|
||||
previousSourceControl.dispose();
|
||||
}
|
||||
|
||||
fiddleSourceControlRegister.set(fiddleSourceControl.getWorkspaceFolder().uri, fiddleSourceControl);
|
||||
|
||||
context.subscriptions.push(fiddleSourceControl);
|
||||
}
|
||||
|
||||
function initializeFromConfigurationFile(context: vscode.ExtensionContext): void {
|
||||
if (!vscode.workspace.workspaceFolders) return;
|
||||
|
||||
vscode.workspace.workspaceFolders.forEach(folder => {
|
||||
let configurationPath = path.join(folder.uri.fsPath, CONFIGURATION_FILE);
|
||||
exists(configurationPath, configFileExists => {
|
||||
if (configFileExists) {
|
||||
readFile(configurationPath, { flag: 'r' }, async (err, data) => {
|
||||
if (err) vscode.window.showErrorMessage(err.message);
|
||||
try {
|
||||
let fiddleSourceControl = await FiddleSourceControl.fromConfiguration(<FiddleConfiguration>JSON.parse(data.toString("utf-8")), folder, context, false);
|
||||
registerFiddleSourceControl(fiddleSourceControl, context);
|
||||
} catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function selectWorkspaceFolder(context: vscode.ExtensionContext, fiddleId: string): Promise<vscode.WorkspaceFolder> {
|
||||
var selectedFolder: vscode.WorkspaceFolder;
|
||||
var workspaceFolderUri: vscode.Uri;
|
||||
var workspaceFolderIndex: number;
|
||||
|
||||
const fiddleConfiguration = parseFiddleId(fiddleId);
|
||||
|
||||
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.' });
|
||||
selectedFolder = await vscode.window.showWorkspaceFolderPick({ placeHolder: 'Pick workspace folder to create files in.' });
|
||||
if (!selectedFolder) return null;
|
||||
|
||||
workspaceFolderIndex = selectedFolder.index;
|
||||
workspaceFolderUri = selectedFolder.uri;
|
||||
}
|
||||
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
|
||||
|
||||
workspaceFolderUri = folderUris[0];
|
||||
// was such workspace folder already open?
|
||||
workspaceFolderIndex = vscode.workspace.workspaceFolders && firstIndex(vscode.workspace.workspaceFolders, folder1 => folder1.uri.toString() === workspaceFolderUri.toString());
|
||||
|
||||
// save folder configuration
|
||||
FiddleSourceControl.saveConfiguration(workspaceFolderUri, fiddleConfiguration);
|
||||
|
||||
selectedFolder = null; // the extension will get reloaded in the context of the newly open workspace
|
||||
}
|
||||
else {
|
||||
return vscode.workspace.workspaceFolders[0];
|
||||
selectedFolder = vscode.workspace.workspaceFolders[0];
|
||||
}
|
||||
|
||||
let workSpacesToReplace = workspaceFolderIndex > -1 ? 1 : 0;
|
||||
if (workspaceFolderIndex < 0) workspaceFolderIndex = 0;
|
||||
|
||||
// replace or insert the workspace
|
||||
vscode.workspace.updateWorkspaceFolders(workspaceFolderIndex, workSpacesToReplace, { uri: workspaceFolderUri });
|
||||
|
||||
return selectedFolder;
|
||||
}
|
||||
|
||||
async function clearWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder): Promise<vscode.WorkspaceFolder> {
|
||||
@ -105,3 +217,21 @@ async function clearWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder): Pr
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() { }
|
||||
|
||||
class RepositoryPick implements vscode.QuickPickItem {
|
||||
|
||||
constructor(public readonly fiddleSourceControl: FiddleSourceControl) { }
|
||||
|
||||
get label(): string {
|
||||
return this.fiddleSourceControl.getSourceControl().label;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDocumentInColumn(fileName: string, column: vscode.ViewColumn): Promise<void> {
|
||||
let uri = vscode.Uri.file(fileName);
|
||||
|
||||
// assuming the file was saved, let's open it in a view column
|
||||
let doc = await vscode.workspace.openTextDocument(uri);
|
||||
|
||||
await vscode.window.showTextDocument(doc, { viewColumn: column });
|
||||
}
|
||||
12
source-control-sample/src/fiddleConfiguration.ts
Normal file
12
source-control-sample/src/fiddleConfiguration.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class FiddleConfiguration {
|
||||
readonly slug: string;
|
||||
readonly version: number;
|
||||
}
|
||||
|
||||
export function parseFiddleId(id: string): FiddleConfiguration {
|
||||
let idFragments = id.split('/');
|
||||
let fiddleSlug = idFragments[0];
|
||||
let fiddleVersion = idFragments.length > 1 ? parseInt(id.split('/')[1]) : undefined;
|
||||
|
||||
return { slug: fiddleSlug, version: fiddleVersion };
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { CancellationToken, ProviderResult, TextDocumentContentProvider, Event, Uri, EventEmitter, Disposable } from "vscode";
|
||||
import { toExtension, JSFIDDLE_SCHEME, Fiddle } from "./fiddleRepository";
|
||||
import { basename } from "path";
|
||||
|
||||
/**
|
||||
* Provides the content of the JS Fiddle documents as fetched from the server i.e. without the local edits.
|
||||
@ -7,7 +8,7 @@ import { toExtension, JSFIDDLE_SCHEME, Fiddle } from "./fiddleRepository";
|
||||
*/
|
||||
export class JSFiddleDocumentContentProvider implements TextDocumentContentProvider, Disposable {
|
||||
private _onDidChange = new EventEmitter<Uri>();
|
||||
private fiddle: Fiddle;
|
||||
private fiddles = new Map<string, Fiddle>();
|
||||
|
||||
get onDidChange(): Event<Uri> {
|
||||
return this._onDidChange.event;
|
||||
@ -18,19 +19,25 @@ export class JSFiddleDocumentContentProvider implements TextDocumentContentProvi
|
||||
}
|
||||
|
||||
updated(newFiddle: Fiddle): void {
|
||||
this.fiddle = newFiddle;
|
||||
this.fiddles.set(newFiddle.slug, 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`));
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${newFiddle.slug}.html`));
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${newFiddle.slug}.css`));
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${newFiddle.slug}.js`));
|
||||
}
|
||||
|
||||
provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult<string> {
|
||||
if (token.isCancellationRequested) return "Canceled";
|
||||
|
||||
let fiddleSlug = basename(uri.fsPath);
|
||||
// strip off the file extension
|
||||
fiddleSlug = fiddleSlug.split('.').slice(0, -1).join('.');
|
||||
let fiddlePart = toExtension(uri);
|
||||
|
||||
return this.fiddle.data[fiddlePart];
|
||||
let fiddle = this.fiddles.get(fiddleSlug);
|
||||
if (!fiddle) return "Resource not found: " + uri.toString();
|
||||
|
||||
return fiddle.data[fiddlePart];
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
import JSFiddle = require("jsfiddle");
|
||||
import { QuickDiffProvider, Uri, CancellationToken, ProviderResult, WorkspaceFolder, workspace, TextDocument } from "vscode";
|
||||
import { QuickDiffProvider, Uri, CancellationToken, ProviderResult, WorkspaceFolder, workspace } from "vscode";
|
||||
import * as path from 'path';
|
||||
|
||||
/** Represents one JSFiddle data and meta-data. */
|
||||
export class Fiddle {
|
||||
constructor(public hash: string, public version: number, public data: FiddleData) { }
|
||||
constructor(public slug: string, public version: number, public data: FiddleData) { }
|
||||
}
|
||||
|
||||
/** Represents JSFiddle HTML, JavaScript and CSS text. */
|
||||
export interface FiddleData {
|
||||
html: string;
|
||||
js: string;
|
||||
@ -22,56 +24,91 @@ export const JSFIDDLE_SCHEME = 'jsfiddle';
|
||||
|
||||
export class FiddleRepository implements QuickDiffProvider {
|
||||
|
||||
constructor(private workspaceFolder: WorkspaceFolder, private fiddleHash: string) { }
|
||||
constructor(private workspaceFolder: WorkspaceFolder, private fiddleSlug: 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}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerates the resources under source control.
|
||||
*/
|
||||
provideSourceControlledResources(): Uri[] {
|
||||
return [
|
||||
Uri.file(this.createLocalResourcePath('html')),
|
||||
Uri.file(this.createLocalResourcePath('js')),
|
||||
Uri.file(this.createLocalResourcePath('css')) ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a local file path in the local workspace that corresponds to the part of the
|
||||
* fiddle denoted by the given extension.
|
||||
*
|
||||
* @param extension fiddle part, which is also used as a file extension
|
||||
* @returns path of the locally cloned fiddle resource ending with the given extension
|
||||
*/
|
||||
createLocalResourcePath(extension: string) {
|
||||
return path.join(this.workspaceFolder.uri.fsPath, this.fiddleSlug + '.' + extension);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFiddle(hash: string, version: number | undefined): Promise<Fiddle> {
|
||||
const DEMO: FiddleData[] = [
|
||||
{
|
||||
html: '<div class="hi">Hi</div>',
|
||||
css: `.hi {\n color: red;\n}`,
|
||||
js: '$(".hi").fadeOut();'
|
||||
}
|
||||
];
|
||||
|
||||
if (hash === "demo") {
|
||||
let maxDemoVersion = 1;
|
||||
// emulates prior versions mock-committed in previous sessions
|
||||
var demoVersionOffset: number = undefined;
|
||||
|
||||
export async function downloadFiddle(slug: string, version: number | undefined): Promise<Fiddle> {
|
||||
|
||||
if (slug === "demo") {
|
||||
// use mock fiddle
|
||||
if (!demoVersionOffset) demoVersionOffset = version-1;
|
||||
let maxDemoVersion = DEMO.length + demoVersionOffset;
|
||||
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);
|
||||
if (version >= 1 && version <= maxDemoVersion) {
|
||||
// mock all versions committed in previous sessions by the first version
|
||||
let index = Math.max(0, version-1 - demoVersionOffset);
|
||||
let fiddleData = DEMO[index];
|
||||
return new Fiddle(slug, version, fiddleData);
|
||||
}
|
||||
else {
|
||||
throw "Invalid demo fiddle version.";
|
||||
}
|
||||
}
|
||||
|
||||
let id = toFiddleId(hash, version);
|
||||
let id = toFiddleId(slug, 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);
|
||||
let fiddle = new Fiddle(slug, version, fiddleData);
|
||||
|
||||
resolve(fiddle);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadFiddle(hash: string, version: number, html: string, js: string, css: string): Promise<Fiddle> {
|
||||
export async function uploadFiddle(slug: 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 });
|
||||
if (slug === "demo") {
|
||||
// using mock fiddle
|
||||
let fiddleData: FiddleData = { html: html, js: js, css: css };
|
||||
DEMO.push(fiddleData);
|
||||
return new Fiddle(slug, version, fiddleData);
|
||||
}
|
||||
|
||||
let data = {
|
||||
slug: hash,
|
||||
slug: slug,
|
||||
version: version,
|
||||
html: html,
|
||||
js: js,
|
||||
@ -83,19 +120,19 @@ export async function uploadFiddle(hash: string, version: number, html: string,
|
||||
// handle error
|
||||
if (err) reject(err);
|
||||
|
||||
let fiddle = new Fiddle(hash, version, fiddleData);
|
||||
let fiddle = new Fiddle(slug, version, fiddleData);
|
||||
|
||||
resolve(fiddle);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toFiddleId(hash: string, version: number | undefined): string {
|
||||
function toFiddleId(slug: string, version: number | undefined): string {
|
||||
if (version === undefined) {
|
||||
return hash;
|
||||
return slug;
|
||||
}
|
||||
else {
|
||||
return hash + '/' + version;
|
||||
return slug + '/' + version;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,28 +1,37 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FiddleRepository, toExtension, downloadFiddle, areIdentical, uploadFiddle, Fiddle } from './fiddleRepository';
|
||||
import * as path from 'path';
|
||||
import { writeFileSync, existsSync, unlinkSync } from 'fs';
|
||||
import { writeFileSync, existsSync, writeFile } from 'fs';
|
||||
import { FiddleConfiguration, parseFiddleId } from './fiddleConfiguration';
|
||||
|
||||
export const CONFIGURATION_FILE = '.jsfiddle';
|
||||
|
||||
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 jsFiddleScm: vscode.SourceControl;
|
||||
private changedResources: vscode.SourceControlResourceGroup;
|
||||
private fiddleRepository: FiddleRepository;
|
||||
private latestFiddleVersion: number = Number.POSITIVE_INFINITY; // until actual value is established
|
||||
private _onRepositoryChange = new vscode.EventEmitter<Fiddle>();
|
||||
private timeout: NodeJS.Timer;
|
||||
private fiddle: 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);
|
||||
constructor(context: vscode.ExtensionContext, private readonly workspaceFolder: vscode.WorkspaceFolder, fiddle: Fiddle, overwrite: boolean) {
|
||||
this.jsFiddleScm = vscode.scm.createSourceControl('jsfiddle', 'JSFiddle #' + fiddle.slug, workspaceFolder.uri);
|
||||
this.changedResources = this.jsFiddleScm.createResourceGroup('workingTree', 'Changes');
|
||||
this.fiddleRepository = new FiddleRepository(workspaceFolder, fiddle.hash);
|
||||
this.fiddleRepository = new FiddleRepository(workspaceFolder, fiddle.slug);
|
||||
this.jsFiddleScm.quickDiffProvider = this.fiddleRepository;
|
||||
this.refreshStatusBar();
|
||||
this.jsFiddleScm.inputBox.placeholder = 'Message is ignored by JS Fiddle :-]';
|
||||
|
||||
let fileSystemWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(workspaceFolder, "*.*"));
|
||||
fileSystemWatcher.onDidChange(uri => this.onResourceChange(uri), context.subscriptions);
|
||||
fileSystemWatcher.onDidCreate(uri => this.onResourceChange(uri), context.subscriptions);
|
||||
fileSystemWatcher.onDidDelete(uri => this.onResourceChange(uri), context.subscriptions);
|
||||
|
||||
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)));
|
||||
context.subscriptions.push(fileSystemWatcher);
|
||||
|
||||
// clone fiddle to the local workspace
|
||||
this.setFiddle(fiddle, overwrite);
|
||||
|
||||
if (Number.isNaN(this.fiddle.version)) {
|
||||
this.establishVersion();
|
||||
@ -31,54 +40,62 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
static async fromFiddleId(id: string, context: vscode.ExtensionContext, workspaceFolder: vscode.WorkspaceFolder, overwrite: boolean): Promise<FiddleSourceControl> {
|
||||
let fiddleConfiguration = parseFiddleId(id);
|
||||
|
||||
let fiddle = await downloadFiddle(fiddleHash, fiddleVersion);
|
||||
return await FiddleSourceControl.fromConfiguration(fiddleConfiguration, workspaceFolder, context, overwrite);
|
||||
}
|
||||
|
||||
static async fromConfiguration(configuration: FiddleConfiguration, workspaceFolder: vscode.WorkspaceFolder, context: vscode.ExtensionContext, overwrite: boolean): Promise<FiddleSourceControl> {
|
||||
return await FiddleSourceControl.fromFiddle(configuration.slug, configuration.version, workspaceFolder, context, overwrite);
|
||||
}
|
||||
|
||||
private static async fromFiddle(fiddleSlug: string, fiddleVersion: number, workspaceFolder: vscode.WorkspaceFolder, context: vscode.ExtensionContext, overwrite: boolean): Promise<FiddleSourceControl> {
|
||||
let fiddle = await downloadFiddle(fiddleSlug, 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);
|
||||
return new FiddleSourceControl(context, workspaceFolder, fiddle, overwrite);
|
||||
}
|
||||
|
||||
private refreshStatusBar() {
|
||||
this.jsFiddleScm.statusBarCommands = [
|
||||
{
|
||||
"command": "extension.source-control.checkout",
|
||||
"title": `↕ ${this.fiddle.hash} #${this.fiddle.version} / ${this.latestFiddleVersion}`,
|
||||
"arguments": [this],
|
||||
"title": `↕ ${this.fiddle.slug} #${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.");
|
||||
if (!this.changedResources.resourceStates.length) {
|
||||
vscode.window.showErrorMessage("There is nothing to commit.");
|
||||
}
|
||||
else if (this.fiddle.version < this.latestFiddleVersion) {
|
||||
vscode.window.showErrorMessage("Checkout the latest fiddle version before committing your changes.");
|
||||
}
|
||||
else {
|
||||
let answer = await vscode.window.showQuickPick(["Yes, upload the changes to JS Fiddle.", "No, I was just clicking around."],
|
||||
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();
|
||||
let html = await this.getLocalResourceText('html');
|
||||
let js = await this.getLocalResourceText('js');
|
||||
let css = await this.getLocalResourceText('css');
|
||||
|
||||
// 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);
|
||||
let newFiddle = await uploadFiddle(this.fiddle.slug, this.fiddle.version + 1, html, js, css);
|
||||
this.setFiddle(newFiddle, false);
|
||||
this.jsFiddleScm.inputBox.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getLocalResourceText(extension: string) {
|
||||
let document = await vscode.workspace.openTextDocument(this.fiddleRepository.createLocalResourcePath(extension));
|
||||
return document.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws away all local changes and resets all files to the checked out version of the repository.
|
||||
*/
|
||||
@ -88,13 +105,14 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
this.resetFile('js');
|
||||
}
|
||||
|
||||
private resetFile(extension: string) {
|
||||
let filePath = path.join(this.workspaceFolder.uri.fsPath, this.fiddle.hash + '.' + extension);
|
||||
/** Resets the given local file content to the checked-out version. */
|
||||
private resetFile(extension: string): void {
|
||||
let filePath = this.fiddleRepository.createLocalResourcePath(extension);
|
||||
writeFileSync(filePath, this.fiddle.data[extension]);
|
||||
}
|
||||
|
||||
async tryCheckout(newVersion: number | undefined): Promise<void> {
|
||||
if (!Number.isFinite(this.latestFiddleVersion)) return void 0;
|
||||
if (!Number.isFinite(this.latestFiddleVersion)) return;
|
||||
|
||||
if (newVersion === undefined) {
|
||||
let allVersions = [...Array(this.latestFiddleVersion).keys()]
|
||||
@ -105,7 +123,7 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
newVersion = newVersionPick.version;
|
||||
}
|
||||
else {
|
||||
return void 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,20 +133,55 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
}
|
||||
else {
|
||||
try {
|
||||
let newFiddle = await downloadFiddle(this.fiddle.hash, newVersion);
|
||||
this.setFiddle(newFiddle);
|
||||
let newFiddle = await downloadFiddle(this.fiddle.slug, newVersion);
|
||||
this.setFiddle(newFiddle, true);
|
||||
} catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setFiddle(newFiddle: Fiddle) {
|
||||
private setFiddle(newFiddle: Fiddle, overwrite: boolean) {
|
||||
if (newFiddle.version > this.latestFiddleVersion) this.latestFiddleVersion = newFiddle.version;
|
||||
this.fiddle = newFiddle;
|
||||
this.resetFilesToCheckedOutVersion(); // overwrite local file content
|
||||
if (overwrite) this.resetFilesToCheckedOutVersion(); // overwrite local file content
|
||||
this._onRepositoryChange.fire(this.fiddle);
|
||||
this.refreshStatusBar();
|
||||
|
||||
this.saveCurrentConfiguration();
|
||||
}
|
||||
|
||||
getFiddle(): Fiddle {
|
||||
return this.fiddle;
|
||||
}
|
||||
|
||||
getWorkspaceFolder(): vscode.WorkspaceFolder {
|
||||
return this.workspaceFolder;
|
||||
}
|
||||
|
||||
getSourceControl(): vscode.SourceControl {
|
||||
return this.jsFiddleScm;
|
||||
}
|
||||
|
||||
getRepository(): FiddleRepository {
|
||||
return this.fiddleRepository;
|
||||
}
|
||||
|
||||
/** save configuration for later VS Code sessions */
|
||||
private saveCurrentConfiguration(): void {
|
||||
let fiddleConfiguration: FiddleConfiguration = {
|
||||
slug: this.fiddle.slug,
|
||||
version: this.fiddle.version
|
||||
};
|
||||
|
||||
FiddleSourceControl.saveConfiguration(this.workspaceFolder.uri, fiddleConfiguration);
|
||||
}
|
||||
|
||||
static saveConfiguration(workspaceFolderUri: vscode.Uri, fiddleConfiguration: FiddleConfiguration): void {
|
||||
let fiddleConfigurationString = JSON.stringify(fiddleConfiguration);
|
||||
writeFile(path.join(workspaceFolderUri.fsPath, CONFIGURATION_FILE), fiddleConfigurationString, err => {
|
||||
vscode.window.showErrorMessage(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,7 +193,7 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
while (true) {
|
||||
try {
|
||||
latestVersion++;
|
||||
let latestFiddle = await downloadFiddle(this.fiddle.hash, latestVersion);
|
||||
let latestFiddle = await downloadFiddle(this.fiddle.slug, latestVersion);
|
||||
} catch (ex) {
|
||||
// typically the ex.statusCode == 404, when there is no further version
|
||||
break;
|
||||
@ -160,7 +213,7 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
while (true) {
|
||||
try {
|
||||
latestVersion++;
|
||||
let latestFiddle = await downloadFiddle(this.fiddle.hash, latestVersion);
|
||||
let latestFiddle = await downloadFiddle(this.fiddle.slug, latestVersion);
|
||||
if (areIdentical(this.fiddle.data, latestFiddle.data)) {
|
||||
currentFiddle = latestFiddle;
|
||||
}
|
||||
@ -173,7 +226,7 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
this.latestFiddleVersion = latestVersion - 1;
|
||||
|
||||
// now we know the version of the current fiddle, let's set it
|
||||
this.setFiddle(currentFiddle);
|
||||
this.setFiddle(currentFiddle, false);
|
||||
}
|
||||
|
||||
|
||||
@ -181,35 +234,85 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
return this._onRepositoryChange.event;
|
||||
}
|
||||
|
||||
updateChangedGroup(e: vscode.TextDocumentChangeEvent): any {
|
||||
this.changedResources.resourceStates = this.documents
|
||||
.filter(doc => this.isDirty(doc))
|
||||
.map(doc => this.toSourceControlResourceState(doc));
|
||||
onResourceChange(_uri: vscode.Uri): void {
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => this.tryUpdateChangedGroup(), 500);
|
||||
}
|
||||
|
||||
async tryUpdateChangedGroup(): Promise<void> {
|
||||
try {
|
||||
await this.updateChangedGroup();
|
||||
}
|
||||
catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
}
|
||||
}
|
||||
|
||||
async updateChangedGroup(): Promise<void> {
|
||||
// for simplicity we ignore which document was changed in this event and scan all of them
|
||||
let changedResources: vscode.SourceControlResourceState[] = [];
|
||||
|
||||
let uris = this.fiddleRepository.provideSourceControlledResources();
|
||||
|
||||
for (const uri of uris) {
|
||||
let isDirty: boolean;
|
||||
let wasDeleted: boolean;
|
||||
|
||||
if (existsSync(uri.fsPath)) {
|
||||
let document = await vscode.workspace.openTextDocument(uri);
|
||||
isDirty = this.isDirty(document);
|
||||
wasDeleted = false;
|
||||
}
|
||||
else {
|
||||
isDirty = true;
|
||||
wasDeleted = true;
|
||||
}
|
||||
|
||||
if (isDirty) {
|
||||
let resourceState = this.toSourceControlResourceState(uri, wasDeleted);
|
||||
changedResources.push(resourceState);
|
||||
}
|
||||
}
|
||||
|
||||
this.changedResources.resourceStates = changedResources;
|
||||
}
|
||||
|
||||
/** Determines whether the resource is different, regardless of line endings. */
|
||||
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 {
|
||||
toSourceControlResourceState(docUri: vscode.Uri, deleted: boolean): vscode.SourceControlResourceState {
|
||||
|
||||
let repositoryUri = this.fiddleRepository.provideOriginalResource(doc.uri, null);
|
||||
let repositoryUri = this.fiddleRepository.provideOriginalResource(docUri, null);
|
||||
|
||||
const fiddlePart = toExtension(doc.uri).toUpperCase();
|
||||
return {
|
||||
resourceUri: doc.uri,
|
||||
command: {
|
||||
const fiddlePart = toExtension(docUri).toUpperCase();
|
||||
|
||||
let command: vscode.Command = !deleted
|
||||
? {
|
||||
title: "Show changes",
|
||||
command: "vscode.diff",
|
||||
arguments: [repositoryUri, doc.uri, `JSFiddle#${this.fiddle.hash} ${fiddlePart} ↔ Local changes`],
|
||||
arguments: [repositoryUri, docUri, `JSFiddle#${this.fiddle.slug} ${fiddlePart} ↔ Local changes`],
|
||||
tooltip: "Diff your changes"
|
||||
}
|
||||
: null;
|
||||
|
||||
let resourceState: vscode.SourceControlResourceState = {
|
||||
resourceUri: docUri,
|
||||
command: command,
|
||||
decorations: {
|
||||
strikeThrough: deleted,
|
||||
tooltip: 'File was locally deleted.'
|
||||
}
|
||||
};
|
||||
|
||||
return resourceState;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onRepositoryChange.dispose();
|
||||
this.jsFiddleScm.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,23 +331,3 @@ class VersionQuickPickItem implements vscode.QuickPickItem {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
9
source-control-sample/src/util.ts
Normal file
9
source-control-sample/src/util.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function firstIndex<T>(array: T[], fn: (t: T) => boolean): number {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (fn(array[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
Reference in New Issue
Block a user