Adding source control sample

This commit is contained in:
Jan Dolejsi
2019-03-12 23:24:46 +00:00
parent b487366633
commit bca441ddab
24 changed files with 2906 additions and 0 deletions

4
source-control-sample/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
out
node_modules
.vscode-test/
*.vsix

View File

@ -0,0 +1,35 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: watch"
},
{
"name": "Run Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "npm: watch"
}
]
}

View File

@ -0,0 +1,3 @@
{
"editor.insertSpaces": false
}

View File

@ -0,0 +1,20 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@ -0,0 +1,9 @@
.vscode/**
.vscode-test/**
out/test/**
out/**/*.map
src/**
.gitignore
tsconfig.json
vsc-extension-quickstart.md
tslint.json

View File

@ -0,0 +1,99 @@
# Source Control Sample
This sample implements a minimal source control provider. It shows how the source control experience in VS Code could be used to for example interact with code in JSFiddle.
![alt text](resources/images/demo.gif "Extension demo")
Activate the extension by invoking the `Open JSFiddle` command. This invokes following 4 lines, which does the following:
1. creates the custom source control provider associated with the workspace folder
1. creates the source control resource group to later show the local changes to the files in the repository
1. registers the `quickDiffProvider`, which implements the mapping between the documents in the remote repository and documents in the local folder.
```javascript
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;
```
![alt text](resources/images/source_control_view.PNG "Source control view")
See `fiddleSourceControl.ts` for full code listing.
The three commands (command, roll-back and refresh) in the title of the source control view pane are configured this way:
```JSON
"contributes": {
"commands": [
...
],
"menus": {
"scm/title": [
{
"command": "extension.source-control.commit",
"group": "navigation",
"when": "scmProvider == jsfiddle"
},
{
"command": "extension.source-control.discard",
"group": "navigation",
"when": "scmProvider == jsfiddle"
},
{
"command": "extension.source-control.refresh",
"group": "navigation",
"when": "scmProvider == jsfiddle"
}
]
}
},
```
It is also worth noting that the sample extension needs to overcome reloading that VS Code triggers when a new workspace folder is added. This is done by writing a memo into the `context.globalState` and reading it upon the next extension activation.
## Status bar controls
The custom source control can add its own controls to the status bar. This typically needs to be refreshed everytime a new version/branch is checked-out.
```javascript
this.jsFiddleScm.statusBarCommands = [
{
"command": "extension.source-control.checkout",
"title": `↕ ${this.fiddle.hash} #${this.fiddle.version} / ${this.latestFiddleVersion}`,
"tooltip": "Checkout another version of this fiddle.",
}
];
```
![alt text](resources/images/status_bar.PNG "Status bar integration")
The command `extension.source-control.checkout` displays quick pick of the JSFiddle versions to check-out.
## Populating changed files view
The extension listens to changes to files in the workspace folder and compares the new document text to the version originally checked out from the repository. When it differs, it creates `vscode.SourceControlResourceState` for every changed document assigns such list to `this.changedResources.resourceStates`, where the `this.changedResources` was created earlier.
```JS
{
resourceUri: doc.uri,
command: {
title: "Show changes",
command: "vscode.diff",
arguments: [repositoryUri, doc.uri, `Checked-out version ↔ Local changes`],
tooltip: "Diff your changes"
}
}
```
where `repositoryUri` is determined by
```JS
this.fiddleRepository.provideOriginalResource(doc.uri, null)
```
## Quick diff
Both the regular diff (invoking the built-in `vscode.diff` when user clicks on the changed resource in the source control view) and the Quick Diff (available in the left margin of the text editor) are rendered automatically by VS Code as long as the extension provides it with the content of the original document checked out from the repository. This is done by implementing a `TextDocumentContentProvider`.
![alt text](resources/images/quick_diff.gif "Quick diff")

2121
source-control-sample/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,91 @@
{
"name": "source-control-sample",
"displayName": "source-control-sample",
"description": "Source control VS Code extension sample",
"version": "0.0.1",
"publisher": "vscode-samples",
"engines": {
"vscode": "^1.30.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:extension.source-control.open"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "extension.source-control.open",
"title": "Open JSFiddle"
},
{
"command": "extension.source-control.commit",
"title": "Commit local changes to JS Fiddle",
"icon": {
"light": "resources/icons/light/check.svg",
"dark": "resources/icons/dark/check.svg"
}
},
{
"command": "extension.source-control.refresh",
"title": "Refresh JS Fiddle status",
"icon": {
"light": "resources/icons/light/refresh.svg",
"dark": "resources/icons/dark/refresh.svg"
}
},
{
"command": "extension.source-control.discard",
"title": "Discards local changes to JS Fiddle",
"icon": {
"light": "resources/icons/light/discard.svg",
"dark": "resources/icons/dark/discard.svg"
}
},
{
"command": "extension.source-control.checkout",
"title": "Checkout another version of this Fiddle",
"icon": {
"light": "resources/icons/light/refresh.svg",
"dark": "resources/icons/dark/refresh.svg"
}
}
],
"menus": {
"scm/title": [
{
"command": "extension.source-control.commit",
"group": "navigation",
"when": "scmProvider == jsfiddle"
},
{
"command": "extension.source-control.discard",
"group": "navigation",
"when": "scmProvider == jsfiddle"
},
{
"command": "extension.source-control.refresh",
"group": "navigation",
"when": "scmProvider == jsfiddle"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"postinstall": "node ./node_modules/vscode/bin/install"
},
"dependencies": {
"jsfiddle": "^1.1.7"
},
"devDependencies": {
"@types/node": "^8.10.25",
"tslint": "^5.11.0",
"typescript": "^2.6.1",
"vscode": "^1.1.30"
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-2 -2 16 16" enable-background="new -2 -2 16 16"><polygon fill="#C5C5C5" points="9,0 4.5,9 3,6 0,6 3,12 6,12 12,0"/></svg>

After

Width:  |  Height:  |  Size: 194 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-0.994 0 16 16" enable-background="new -0.994 0 16 16"><path fill="#C5C5C5" d="M13 6c0 1.461-.636 2.846-1.746 3.797l-5.584 4.951-1.324-1.496 5.595-4.962c.678-.582 1.061-1.413 1.061-2.29 0-1.654-1.345-3-2.997-3-.71 0-1.399.253-1.938.713l-1.521 1.287h2.448l-1.998 2h-3.996v-4l1.998-2v2.692l1.775-1.504c.899-.766 2.047-1.188 3.232-1.188 2.754 0 4.995 2.243 4.995 5z"/></svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#2D2D30"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#C5C5C5"/></svg>

After

Width:  |  Height:  |  Size: 986 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><polygon points="5.382,13 2.382,7 6.618,7 7,7.764 9.382,3 13.618,3 8.618,13" fill="#F6F6F6"/><path d="M12 4l-4 8h-2l-2-4h2l1 2 3-6h2z" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 220 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-0.994 0 16 16" enable-background="new -0.994 0 16 16"><path fill="#424242" d="M13 6c0 1.461-.636 2.846-1.746 3.797l-5.584 4.951-1.324-1.496 5.595-4.962c.678-.582 1.061-1.413 1.061-2.29 0-1.654-1.345-3-2.997-3-.71 0-1.399.253-1.938.713l-1.521 1.287h2.448l-1.998 2h-3.996v-4l1.998-2v2.692l1.775-1.504c.899-.766 2.047-1.188 3.232-1.188 2.754 0 4.995 2.243 4.995 5z"/></svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#F6F6F6"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View 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() { }

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

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

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

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": ["es6"],
"sourceMap": true,
"rootDir": "src"
},
"exclude": ["node_modules", ".vscode-test"]
}

View File

@ -0,0 +1,6 @@
{
"rules": {
"indent": [true, "tabs"],
"semicolon": [true, "always"]
}
}