Files
vscode-extension-samples/source-control-sample/src/fiddleSourceControl.ts

334 lines
11 KiB
TypeScript
Raw Normal View History

2019-03-12 23:24:46 +00:00
import * as vscode from 'vscode';
import { FiddleRepository, toExtension, downloadFiddle, areIdentical, uploadFiddle, Fiddle } from './fiddleRepository';
2019-03-12 23:24:46 +00:00
import * as path from 'path';
import { writeFileSync, existsSync, writeFile } from 'fs';
import { FiddleConfiguration, parseFiddleId } from './fiddleConfiguration';
export const CONFIGURATION_FILE = '.jsfiddle';
2019-03-12 23:24:46 +00:00
export class FiddleSourceControl implements vscode.Disposable {
private jsFiddleScm: vscode.SourceControl;
private changedResources: vscode.SourceControlResourceGroup;
private fiddleRepository: FiddleRepository;
private latestFiddleVersion: number = Number.POSITIVE_INFINITY; // until actual value is established
2019-03-12 23:24:46 +00:00
private _onRepositoryChange = new vscode.EventEmitter<Fiddle>();
private timeout: NodeJS.Timer;
private fiddle: Fiddle;
2019-03-12 23:24:46 +00:00
constructor(context: vscode.ExtensionContext, private readonly workspaceFolder: vscode.WorkspaceFolder, fiddle: Fiddle, overwrite: boolean) {
this.jsFiddleScm = vscode.scm.createSourceControl('jsfiddle', 'JSFiddle #' + fiddle.slug, workspaceFolder.uri);
2019-03-12 23:24:46 +00:00
this.changedResources = this.jsFiddleScm.createResourceGroup('workingTree', 'Changes');
this.fiddleRepository = new FiddleRepository(workspaceFolder, fiddle.slug);
2019-03-12 23:24:46 +00:00
this.jsFiddleScm.quickDiffProvider = this.fiddleRepository;
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);
2019-03-12 23:24:46 +00:00
context.subscriptions.push(this.jsFiddleScm);
context.subscriptions.push(fileSystemWatcher);
// clone fiddle to the local workspace
this.setFiddle(fiddle, overwrite);
2019-03-12 23:24:46 +00:00
if (Number.isNaN(this.fiddle.version)) {
this.establishVersion();
} else {
this.refresh();
}
}
static async fromFiddleId(id: string, context: vscode.ExtensionContext, workspaceFolder: vscode.WorkspaceFolder, overwrite: boolean): Promise<FiddleSourceControl> {
let fiddleConfiguration = parseFiddleId(id);
2019-03-12 23:24:46 +00:00
return await FiddleSourceControl.fromConfiguration(fiddleConfiguration, workspaceFolder, context, overwrite);
}
2019-03-12 23:24:46 +00:00
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);
}
2019-03-12 23:24:46 +00:00
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;
return new FiddleSourceControl(context, workspaceFolder, fiddle, overwrite);
2019-03-12 23:24:46 +00:00
}
private refreshStatusBar() {
this.jsFiddleScm.statusBarCommands = [
{
"command": "extension.source-control.checkout",
"arguments": [this],
"title": `${this.fiddle.slug} #${this.fiddle.version} / ${this.latestFiddleVersion}`,
2019-03-12 23:24:46 +00:00
"tooltip": "Checkout another version of this fiddle.",
}
];
}
async commitAll(): Promise<void> {
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.");
2019-03-12 23:24:46 +00:00
}
else {
let answer = await vscode.window.showQuickPick(["Yes, upload the changes to JS Fiddle.", "No, I was just clicking around."],
2019-03-12 23:24:46 +00:00
{ placeHolder: "Are you sure you want to commit?" });
if (answer && answer.toLowerCase().startsWith("yes")) {
let html = await this.getLocalResourceText('html');
let js = await this.getLocalResourceText('js');
let css = await this.getLocalResourceText('css');
2019-03-12 23:24:46 +00:00
// here we assume nobody updated the Fiddle on the server since we refreshed the list of versions
let newFiddle = await uploadFiddle(this.fiddle.slug, this.fiddle.version + 1, html, js, css);
this.setFiddle(newFiddle, false);
this.jsFiddleScm.inputBox.value = '';
2019-03-12 23:24:46 +00:00
}
}
}
private async getLocalResourceText(extension: string) {
let document = await vscode.workspace.openTextDocument(this.fiddleRepository.createLocalResourcePath(extension));
return document.getText();
}
2019-03-12 23:24:46 +00:00
/**
* 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');
}
/** Resets the given local file content to the checked-out version. */
private resetFile(extension: string): void {
let filePath = this.fiddleRepository.createLocalResourcePath(extension);
2019-03-12 23:24:46 +00:00
writeFileSync(filePath, this.fiddle.data[extension]);
}
async tryCheckout(newVersion: number | undefined): Promise<void> {
if (!Number.isFinite(this.latestFiddleVersion)) return;
2019-03-12 23:24:46 +00:00
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;
2019-03-12 23:24:46 +00:00
}
}
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.slug, newVersion);
this.setFiddle(newFiddle, true);
2019-03-12 23:24:46 +00:00
} catch (ex) {
vscode.window.showErrorMessage(ex);
}
}
}
private setFiddle(newFiddle: Fiddle, overwrite: boolean) {
2019-03-12 23:24:46 +00:00
if (newFiddle.version > this.latestFiddleVersion) this.latestFiddleVersion = newFiddle.version;
this.fiddle = newFiddle;
if (overwrite) this.resetFilesToCheckedOutVersion(); // overwrite local file content
2019-03-12 23:24:46 +00:00
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);
});
2019-03-12 23:24:46 +00:00
}
/**
* 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.slug, latestVersion);
2019-03-12 23:24:46 +00:00
} 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.slug, latestVersion);
2019-03-12 23:24:46 +00:00
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, false);
2019-03-12 23:24:46 +00:00
}
get onRepositoryChange(): vscode.Event<Fiddle> {
return this._onRepositoryChange.event;
}
onResourceChange(_uri: vscode.Uri): void {
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.tryUpdateChangedGroup(), 500);
2019-03-12 23:24:46 +00:00
}
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. */
2019-03-12 23:24:46 +00:00
isDirty(doc: vscode.TextDocument): boolean {
let originalText = this.fiddle.data[toExtension(doc.uri)];
return originalText.replace('\r', '') != doc.getText().replace('\r', '');
}
toSourceControlResourceState(docUri: vscode.Uri, deleted: boolean): vscode.SourceControlResourceState {
let repositoryUri = this.fiddleRepository.provideOriginalResource(docUri, null);
2019-03-12 23:24:46 +00:00
const fiddlePart = toExtension(docUri).toUpperCase();
2019-03-12 23:24:46 +00:00
let command: vscode.Command = !deleted
? {
2019-03-12 23:24:46 +00:00
title: "Show changes",
command: "vscode.diff",
arguments: [repositoryUri, docUri, `JSFiddle#${this.fiddle.slug} ${fiddlePart} ↔ Local changes`],
2019-03-12 23:24:46 +00:00
tooltip: "Diff your changes"
}
: null;
let resourceState: vscode.SourceControlResourceState = {
resourceUri: docUri,
command: command,
decorations: {
strikeThrough: deleted,
tooltip: 'File was locally deleted.'
}
2019-03-12 23:24:46 +00:00
};
return resourceState;
2019-03-12 23:24:46 +00:00
}
dispose() {
this._onRepositoryChange.dispose();
this.jsFiddleScm.dispose();
2019-03-12 23:24:46 +00:00
}
}
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.";
}
}
}