Adding source control sample
4
source-control-sample/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
out
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
35
source-control-sample/.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
source-control-sample/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.insertSpaces": false
|
||||
}
|
||||
20
source-control-sample/.vscode/tasks.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
9
source-control-sample/.vscodeignore
Normal file
@ -0,0 +1,9 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
out/test/**
|
||||
out/**/*.map
|
||||
src/**
|
||||
.gitignore
|
||||
tsconfig.json
|
||||
vsc-extension-quickstart.md
|
||||
tslint.json
|
||||
99
source-control-sample/README.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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;
|
||||
```
|
||||
|
||||

|
||||
|
||||
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.",
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||

|
||||
|
||||
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`.
|
||||
|
||||

|
||||
2121
source-control-sample/package-lock.json
generated
Normal file
91
source-control-sample/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
source-control-sample/resources/icons/dark/check.svg
Normal 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 |
1
source-control-sample/resources/icons/dark/discard.svg
Normal 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 |
1
source-control-sample/resources/icons/dark/refresh.svg
Normal 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 |
1
source-control-sample/resources/icons/light/check.svg
Normal 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 |
1
source-control-sample/resources/icons/light/discard.svg
Normal 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 |
1
source-control-sample/resources/icons/light/refresh.svg
Normal 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 |
BIN
source-control-sample/resources/images/demo.gif
Normal file
|
After Width: | Height: | Size: 951 KiB |
BIN
source-control-sample/resources/images/quick_diff.gif
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
source-control-sample/resources/images/source_control_view.PNG
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
source-control-sample/resources/images/status_bar.PNG
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
107
source-control-sample/src/extension.ts
Normal file
@ -0,0 +1,107 @@
|
||||
// The module 'vscode' contains the VS Code extensibility API
|
||||
// Import the module and reference it with the alias vscode in your code below
|
||||
import * as vscode from 'vscode';
|
||||
import { JSFIDDLE_SCHEME } from './fiddleRepository';
|
||||
import { FiddleSourceControl } from './fiddleSourceControl';
|
||||
import { JSFiddleDocumentContentProvider } from './fiddleDocumentContentProvider';
|
||||
import * as path from 'path';
|
||||
import { unlinkSync, readdirSync } from 'fs';
|
||||
|
||||
const SOURCE_CONTROL_OPEN_COMMAND = 'extension.source-control.open';
|
||||
|
||||
// this method is called when your extension is activated
|
||||
// your extension is activated the very first time the command is executed
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Congratulations, your extension "source-control-sample" is now active!');
|
||||
|
||||
let fiddleId = context.globalState.get(FIDDLE_ID_TO_OPEN);
|
||||
if (fiddleId) {
|
||||
// new workspace folder was open and the extension needs to continue opening the fiddle files into it
|
||||
vscode.commands.executeCommand(SOURCE_CONTROL_OPEN_COMMAND, fiddleId);
|
||||
context.globalState.update(FIDDLE_ID_TO_OPEN, undefined);
|
||||
}
|
||||
|
||||
const jsFiddleDocumentContentProvider = new JSFiddleDocumentContentProvider();
|
||||
let fiddleSourceControl: FiddleSourceControl = null;
|
||||
|
||||
let disposable = vscode.commands.registerCommand(SOURCE_CONTROL_OPEN_COMMAND, async (id?: string) => {
|
||||
|
||||
if (fiddleSourceControl) vscode.window.showErrorMessage("Another Fiddle was already open in this workspace. Open a new workspace first.");
|
||||
|
||||
if (!id) {
|
||||
id = await vscode.window.showInputBox({ prompt: 'Paste JSFiddle ID and optionally version', placeHolder: 'hash or hash/version, e.g. u8B29/1', value: 'u8B29/1' });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
let workspaceFolder = await clearWorkspaceFolder(await getWorkspaceFolder(context, id));
|
||||
if (!workspaceFolder) return; // canceled by user
|
||||
|
||||
// show the file explorer with the three new files
|
||||
vscode.commands.executeCommand("workbench.view.explorer");
|
||||
|
||||
// register source control
|
||||
let fiddleSourceControl = await FiddleSourceControl.fromFiddle(id, context, workspaceFolder);
|
||||
|
||||
// update the fiddle document content provider with the latest content
|
||||
jsFiddleDocumentContentProvider.updated(fiddleSourceControl.fiddle);
|
||||
|
||||
// every time the repository is updated with new fiddle version, notify the content provider
|
||||
fiddleSourceControl.onRepositoryChange(fiddle => jsFiddleDocumentContentProvider.updated(fiddle));
|
||||
|
||||
context.subscriptions.push(fiddleSourceControl);
|
||||
}
|
||||
catch (ex) {
|
||||
vscode.window.showErrorMessage(ex);
|
||||
console.log(ex);
|
||||
}
|
||||
});
|
||||
|
||||
vscode.workspace.registerTextDocumentContentProvider(JSFIDDLE_SCHEME, jsFiddleDocumentContentProvider);
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
|
||||
const FIDDLE_ID_TO_OPEN = "fiddleIdToOpen";
|
||||
|
||||
async function getWorkspaceFolder(context: vscode.ExtensionContext, fiddleId: String): Promise<vscode.WorkspaceFolder> {
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) {
|
||||
return await vscode.window.showWorkspaceFolderPick({ placeHolder: 'Pick workspace folder to create files in.' });
|
||||
}
|
||||
else if (!vscode.workspace.workspaceFolders) {
|
||||
let folderUris = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select folder' });
|
||||
if (!folderUris) {
|
||||
return null;
|
||||
}
|
||||
vscode.workspace.updateWorkspaceFolders(0, 0, { uri: folderUris[0] });
|
||||
context.globalState.update(FIDDLE_ID_TO_OPEN, fiddleId);
|
||||
return null; // the extension will get reloaded in the context of the newly open workspace
|
||||
}
|
||||
else {
|
||||
return vscode.workspace.workspaceFolders[0];
|
||||
}
|
||||
}
|
||||
|
||||
async function clearWorkspaceFolder(workspaceFolder: vscode.WorkspaceFolder): Promise<vscode.WorkspaceFolder> {
|
||||
|
||||
if (!workspaceFolder) return null;
|
||||
|
||||
// check if the workspace is empty, or clear it
|
||||
let existingWorkspaceFiles = readdirSync(workspaceFolder.uri.fsPath);
|
||||
if (existingWorkspaceFiles.length > 0) {
|
||||
let answer = await vscode.window.showQuickPick(["Yes", "No"],
|
||||
{ placeHolder: `Remove ${existingWorkspaceFiles.length} file(s) from the workspace before cloning the remote repository?` });
|
||||
if (answer === undefined) return null;
|
||||
|
||||
if (answer === "Yes") {
|
||||
existingWorkspaceFiles
|
||||
.map(filename =>
|
||||
unlinkSync(path.join(workspaceFolder.uri.fsPath, filename)));
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceFolder;
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() { }
|
||||
36
source-control-sample/src/fiddleDocumentContentProvider.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { CancellationToken, ProviderResult, TextDocumentContentProvider, Event, Uri, EventEmitter, Disposable } from "vscode";
|
||||
import { toExtension, JSFIDDLE_SCHEME, Fiddle } from "./fiddleRepository";
|
||||
|
||||
/**
|
||||
* Provides the content of the JS Fiddle documents as fetched from the server i.e. without the local edits.
|
||||
* This is used for the source control diff.
|
||||
*/
|
||||
export class JSFiddleDocumentContentProvider implements TextDocumentContentProvider, Disposable {
|
||||
private _onDidChange = new EventEmitter<Uri>();
|
||||
private fiddle: Fiddle;
|
||||
|
||||
get onDidChange(): Event<Uri> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
updated(newFiddle: Fiddle): void {
|
||||
this.fiddle = newFiddle;
|
||||
|
||||
// let's assume all 3 documents actually changed and notify the quick-diff
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${this.fiddle.hash}.html`));
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${this.fiddle.hash}.css`));
|
||||
this._onDidChange.fire(Uri.parse(`${JSFIDDLE_SCHEME}:${this.fiddle.hash}.js`));
|
||||
}
|
||||
|
||||
provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult<string> {
|
||||
if (token.isCancellationRequested) return "Canceled";
|
||||
|
||||
let fiddlePart = toExtension(uri);
|
||||
|
||||
return this.fiddle.data[fiddlePart];
|
||||
}
|
||||
}
|
||||
108
source-control-sample/src/fiddleRepository.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import JSFiddle = require("jsfiddle");
|
||||
import { QuickDiffProvider, Uri, CancellationToken, ProviderResult, WorkspaceFolder, workspace, TextDocument } from "vscode";
|
||||
import * as path from 'path';
|
||||
|
||||
export class Fiddle {
|
||||
constructor(public hash: string, public version: number, public data: FiddleData) { }
|
||||
}
|
||||
|
||||
export interface FiddleData {
|
||||
html: string;
|
||||
js: string;
|
||||
css: string;
|
||||
}
|
||||
|
||||
export function areIdentical(first: FiddleData, second: FiddleData): boolean {
|
||||
return first.html == second.html
|
||||
&& first.css == second.css
|
||||
&& first.js == second.js;
|
||||
}
|
||||
|
||||
export const JSFIDDLE_SCHEME = 'jsfiddle';
|
||||
|
||||
export class FiddleRepository implements QuickDiffProvider {
|
||||
|
||||
constructor(private workspaceFolder: WorkspaceFolder, private fiddleHash: string) { }
|
||||
|
||||
provideOriginalResource?(uri: Uri, token: CancellationToken): ProviderResult<Uri> {
|
||||
// converts the local file uri to jsfiddle:file.ext
|
||||
let relativePath = workspace.asRelativePath(uri.fsPath);
|
||||
return Uri.parse(`${JSFIDDLE_SCHEME}:${relativePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFiddle(hash: string, version: number | undefined): Promise<Fiddle> {
|
||||
|
||||
if (hash === "demo") {
|
||||
let maxDemoVersion = 1;
|
||||
if (version === undefined) version = maxDemoVersion;
|
||||
|
||||
if (version <= maxDemoVersion) {
|
||||
let fiddleData: FiddleData = {
|
||||
html: '<div class="hi">Hi</div>',
|
||||
css: `.hi {\n color: red;\n}`,
|
||||
js: '$(".hi").fadeOut();'
|
||||
};
|
||||
return new Fiddle(hash, version, fiddleData);
|
||||
}
|
||||
else {
|
||||
throw "Invalid demo fiddle version.";
|
||||
}
|
||||
}
|
||||
|
||||
let id = toFiddleId(hash, version);
|
||||
|
||||
return new Promise<Fiddle>((resolve, reject) => {
|
||||
JSFiddle.getFiddle(id, (err: any, fiddleData: any) => {
|
||||
// handle error
|
||||
if (err) reject(err);
|
||||
|
||||
let fiddle = new Fiddle(hash, version, fiddleData);
|
||||
|
||||
resolve(fiddle);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadFiddle(hash: string, version: number, html: string, js: string, css: string): Promise<Fiddle> {
|
||||
|
||||
if (hash === "demo") {
|
||||
return new Fiddle(hash, version, { html: html, js: js, css: css });
|
||||
}
|
||||
|
||||
let data = {
|
||||
slug: hash,
|
||||
version: version,
|
||||
html: html,
|
||||
js: js,
|
||||
css: css
|
||||
};
|
||||
|
||||
return new Promise<Fiddle>((resolve, reject) => {
|
||||
JSFiddle.saveFiddle(data, (err: any, fiddleData: any) => {
|
||||
// handle error
|
||||
if (err) reject(err);
|
||||
|
||||
let fiddle = new Fiddle(hash, version, fiddleData);
|
||||
|
||||
resolve(fiddle);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toFiddleId(hash: string, version: number | undefined): string {
|
||||
if (version === undefined) {
|
||||
return hash;
|
||||
}
|
||||
else {
|
||||
return hash + '/' + version;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets extension trimming the dot character.
|
||||
* @param uri document uri
|
||||
*/
|
||||
export function toExtension(uri: Uri): string {
|
||||
return path.extname(uri.fsPath).substr(1);
|
||||
}
|
||||
250
source-control-sample/src/fiddleSourceControl.ts
Normal file
@ -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;
|
||||
}
|
||||
11
source-control-sample/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": ["es6"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
6
source-control-sample/tslint.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"rules": {
|
||||
"indent": [true, "tabs"],
|
||||
"semicolon": [true, "always"]
|
||||
}
|
||||
}
|
||||