- support for multi workspace folders

- mock fiddle for offline testing
- redesigned to support files closing
- support for deleted files
This commit is contained in:
Jan Dolejsi
2019-03-19 03:06:13 +01:00
parent d3dcc39f87
commit 0dceda85ee
9 changed files with 506 additions and 199 deletions

View File

@ -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 });
}

View 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 };
}

View File

@ -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];
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View 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;
}