mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-04-27 16:55:44 +08:00
- awaiting async fs operation to avoid race conditions
- more flexible handling of workspace folders
This commit is contained in:
64
source-control-sample/src/afs.ts
Normal file
64
source-control-sample/src/afs.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// This file promisifies necessary file system functions.
|
||||
// This should be removed when VS Code updates to Node.js ^11.14 and replaced by the native fs promises.
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
|
||||
function handleResult<T>(resolve: (result: T) => void, reject: (error: Error) => void, error: Error | null | undefined, result: T): void {
|
||||
if (error) {
|
||||
reject(massageError(error));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function massageError(error: Error & { code?: string }): Error {
|
||||
if (error.code === 'ENOENT') {
|
||||
return vscode.FileSystemError.FileNotFound();
|
||||
}
|
||||
|
||||
if (error.code === 'EISDIR') {
|
||||
return vscode.FileSystemError.FileIsADirectory();
|
||||
}
|
||||
|
||||
if (error.code === 'EEXIST') {
|
||||
return vscode.FileSystemError.FileExists();
|
||||
}
|
||||
|
||||
if (error.code === 'EPERM' || error.code === 'EACCESS') {
|
||||
return vscode.FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
export function readFile(path: string): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
fs.readFile(path, (error, buffer) => handleResult(resolve, reject, error, buffer));
|
||||
});
|
||||
}
|
||||
|
||||
export function writeFile(path: string, content: Buffer): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.writeFile(path, content, error => handleResult(resolve, reject, error, void 0));
|
||||
});
|
||||
}
|
||||
|
||||
export function exists(path: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
fs.exists(path, exists => handleResult(resolve, reject, null, exists));
|
||||
});
|
||||
}
|
||||
|
||||
export function readdir(path: string): Promise<string[]> {
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
fs.readdir(path, (error, files) => handleResult(resolve, reject, error, files));
|
||||
});
|
||||
}
|
||||
|
||||
export function unlink(path: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.unlink(path, error => handleResult(resolve, reject, error, void 0));
|
||||
});
|
||||
}
|
||||
@ -5,9 +5,9 @@ import { JSFIDDLE_SCHEME } from './fiddleRepository';
|
||||
import { FiddleSourceControl, CONFIGURATION_FILE } from './fiddleSourceControl';
|
||||
import { JSFiddleDocumentContentProvider } from './fiddleDocumentContentProvider';
|
||||
import * as path from 'path';
|
||||
import { unlinkSync, readdirSync, existsSync, exists, readFile } from 'fs';
|
||||
import * as afs from './afs';
|
||||
import { FiddleConfiguration, parseFiddleId } from './fiddleConfiguration';
|
||||
import { firstIndex } from './util';
|
||||
import { firs tIndex, UTF8 } from './util';
|
||||
|
||||
const SOURCE_CONTROL_OPEN_COMMAND = 'extension.source-control.open';
|
||||
var jsFiddleDocumentContentProvider: JSFiddleDocumentContentProvider;
|
||||
@ -15,12 +15,18 @@ 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) {
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
console.log('Congratulations, your extension "source-control-sample" is now active!');
|
||||
|
||||
jsFiddleDocumentContentProvider = new JSFiddleDocumentContentProvider();
|
||||
|
||||
initializeFromConfigurationFile(context);
|
||||
try {
|
||||
await initializeFromConfigurationFile(context);
|
||||
}
|
||||
catch (err) {
|
||||
console.log('Failed to initialize a Fiddle workspace.');
|
||||
vscode.window.showErrorMessage(err);
|
||||
}
|
||||
|
||||
let openCommand = vscode.commands.registerCommand(SOURCE_CONTROL_OPEN_COMMAND,
|
||||
(fiddleId?: string, workspaceUri?: vscode.Uri) => {
|
||||
@ -58,15 +64,19 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
|
||||
context.subscriptions.push(vscode.workspace.onDidChangeWorkspaceFolders(e => {
|
||||
// dispose source control for removed workspace folders
|
||||
e.removed.forEach(wf => {
|
||||
unregisterFiddleSourceControl(wf.uri);
|
||||
});
|
||||
|
||||
// initialize new source control for manually added workspace folders
|
||||
e.added.forEach(wf => {
|
||||
initializeFolderFromConfigurationFile(wf, context);
|
||||
});
|
||||
try {
|
||||
// initialize new source control for manually added workspace folders
|
||||
e.added.forEach(wf => {
|
||||
initializeFolderFromConfigurationFile(wf, context);
|
||||
});
|
||||
} catch (ex) {
|
||||
vscode.window.showErrorMessage(ex.message);
|
||||
} finally {
|
||||
// dispose source control for removed workspace folders
|
||||
e.removed.forEach(wf => {
|
||||
unregisterFiddleSourceControl(wf.uri);
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@ -121,8 +131,7 @@ async function openFiddle(context: vscode.ExtensionContext, fiddleId?: string, w
|
||||
vscode.workspace.getWorkspaceFolder(workspaceUri) :
|
||||
await selectWorkspaceFolder(context, fiddleId);
|
||||
|
||||
workspaceFolder = await clearWorkspaceFolder(workspaceFolder);
|
||||
if (!workspaceFolder) { return; } // canceled by user
|
||||
if (! await clearWorkspaceFolder(workspaceFolder.uri)) { return; }
|
||||
|
||||
// show the file explorer with the three new files
|
||||
vscode.commands.executeCommand("workbench.view.explorer");
|
||||
@ -131,7 +140,10 @@ async function openFiddle(context: vscode.ExtensionContext, fiddleId?: string, w
|
||||
let fiddleSourceControl = await FiddleSourceControl.fromFiddleId(fiddleId, context, workspaceFolder, true);
|
||||
|
||||
registerFiddleSourceControl(fiddleSourceControl, context);
|
||||
showFiddleInEditor(fiddleSourceControl);
|
||||
}
|
||||
|
||||
async function showFiddleInEditor(fiddleSourceControl: FiddleSourceControl): Promise<void> {
|
||||
// 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);
|
||||
@ -169,93 +181,117 @@ function unregisterFiddleSourceControl(folderUri: vscode.Uri): void {
|
||||
* When the extension starts up, it must visit all workspace folders to see if any of them are fiddles.
|
||||
* @param context extension context
|
||||
*/
|
||||
function initializeFromConfigurationFile(context: vscode.ExtensionContext): void {
|
||||
async function initializeFromConfigurationFile(context: vscode.ExtensionContext): Promise<void> {
|
||||
if (!vscode.workspace.workspaceFolders) { return; }
|
||||
|
||||
vscode.workspace.workspaceFolders.forEach(folder => initializeFolderFromConfigurationFile(folder, context));
|
||||
let folderPromises = vscode.workspace.workspaceFolders.map(async (folder) => await initializeFolderFromConfigurationFile(folder, context));
|
||||
await Promise.all(folderPromises);
|
||||
}
|
||||
|
||||
function initializeFolderFromConfigurationFile(folder: vscode.WorkspaceFolder, context: vscode.ExtensionContext): void {
|
||||
async function initializeFolderFromConfigurationFile(folder: vscode.WorkspaceFolder, context: vscode.ExtensionContext): Promise<void> {
|
||||
const 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let configFileExists = await afs.exists(configurationPath);
|
||||
|
||||
if (configFileExists) {
|
||||
let data = await afs.readFile(configurationPath);
|
||||
let fiddleConfiguration = <FiddleConfiguration>JSON.parse(data.toString(UTF8));
|
||||
let fiddleSourceControl = await FiddleSourceControl.fromConfiguration(fiddleConfiguration, folder, context, !fiddleConfiguration.downloaded);
|
||||
registerFiddleSourceControl(fiddleSourceControl, context);
|
||||
if (!fiddleConfiguration.downloaded) {
|
||||
// the fiddle was not downloaded before the extension restart, so let's show it now
|
||||
showFiddleInEditor(fiddleSourceControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function selectWorkspaceFolder(context: vscode.ExtensionContext, fiddleId: string): Promise<vscode.WorkspaceFolder | undefined> {
|
||||
var selectedFolder: vscode.WorkspaceFolder | undefined;
|
||||
var workspaceFolderUri: vscode.Uri | undefined;
|
||||
var workspaceFolderIndex: number | undefined;
|
||||
var folderOpeningMode: FolderOpeningMode;
|
||||
|
||||
const fiddleConfiguration = parseFiddleId(fiddleId);
|
||||
var folderPicks: WorkspaceFolderPick[] = [newFolderPick];
|
||||
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) {
|
||||
selectedFolder = await vscode.window.showWorkspaceFolderPick({ placeHolder: 'Pick workspace folder to create files in.' });
|
||||
if (!selectedFolder) { return undefined; }
|
||||
if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
|
||||
folderPicks.push(newWorkspaceFolderPick);
|
||||
|
||||
for (const wf of vscode.workspace.workspaceFolders) {
|
||||
let content = await afs.readdir(wf.uri.fsPath);
|
||||
folderPicks.push(new ExistingWorkspaceFolderPick(wf, content));
|
||||
}
|
||||
}
|
||||
|
||||
let selectedFolderPick: WorkspaceFolderPick =
|
||||
folderPicks.length === 1 ?
|
||||
folderPicks[0] :
|
||||
await vscode.window.showQuickPick(folderPicks, {
|
||||
canPickMany: false, ignoreFocusOut: true, placeHolder: 'Pick workspace folder to create files in.'
|
||||
});
|
||||
|
||||
if (!selectedFolderPick) { return null; }
|
||||
|
||||
if (selectedFolderPick instanceof ExistingWorkspaceFolderPick) {
|
||||
selectedFolder = selectedFolderPick.workspaceFolder;
|
||||
workspaceFolderIndex = selectedFolder.index;
|
||||
workspaceFolderUri = selectedFolder.uri;
|
||||
}
|
||||
else if (!vscode.workspace.workspaceFolders) {
|
||||
|
||||
folderOpeningMode = selectedFolderPick.folderOpeningMode;
|
||||
|
||||
if (!workspaceFolderUri && !selectedFolder) {
|
||||
let folderUris = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false, canSelectMany: false, openLabel: 'Select folder' });
|
||||
if (!folderUris) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
workspaceFolderUri = folderUris[0];
|
||||
// was such workspace folder already open?
|
||||
workspaceFolderIndex = vscode.workspace.workspaceFolders && firstIndex(vscode.workspace.workspaceFolders, (folder1: any) => folder1.uri.toString() === workspaceFolderUri!.toString());
|
||||
|
||||
// save folder configuration
|
||||
FiddleSourceControl.saveConfiguration(workspaceFolderUri, fiddleConfiguration);
|
||||
|
||||
selectedFolder = undefined; // the extension will get reloaded in the context of the newly open workspace
|
||||
}
|
||||
else {
|
||||
selectedFolder = vscode.workspace.workspaceFolders[0];
|
||||
}
|
||||
|
||||
let workSpacesToReplace = typeof workspaceFolderIndex === 'number' && workspaceFolderIndex > -1 ? 1 : 0;
|
||||
if (workspaceFolderIndex === undefined || workspaceFolderIndex < 0) { workspaceFolderIndex = 0; }
|
||||
if (! await clearWorkspaceFolder(workspaceFolderUri)) { return null; }
|
||||
|
||||
// replace or insert the workspace
|
||||
if (workspaceFolderUri) {
|
||||
vscode.workspace.updateWorkspaceFolders(workspaceFolderIndex, workSpacesToReplace, { uri: workspaceFolderUri });
|
||||
const fiddleConfiguration = parseFiddleId(fiddleId);
|
||||
|
||||
// save folder configuration
|
||||
FiddleSourceControl.saveConfiguration(workspaceFolderUri, fiddleConfiguration);
|
||||
|
||||
if (folderOpeningMode === FolderOpeningMode.AddToWorkspace || folderOpeningMode === undefined) {
|
||||
let workSpacesToReplace = typeof workspaceFolderIndex === 'number' && workspaceFolderIndex > -1 ? 1 : 0;
|
||||
if (workspaceFolderIndex === undefined || workspaceFolderIndex < 0) { workspaceFolderIndex = 0; }
|
||||
|
||||
// replace or insert the workspace
|
||||
if (workspaceFolderUri) {
|
||||
vscode.workspace.updateWorkspaceFolders(workspaceFolderIndex, workSpacesToReplace, { uri: workspaceFolderUri });
|
||||
}
|
||||
}
|
||||
else if (folderOpeningMode === FolderOpeningMode.OpenFolder) {
|
||||
vscode.commands.executeCommand("vscode.openFolder", workspaceFolderUri);
|
||||
}
|
||||
|
||||
return selectedFolder;
|
||||
}
|
||||
|
||||
async function clearWorkspaceFolder(workspaceFolder?: vscode.WorkspaceFolder): Promise<vscode.WorkspaceFolder | undefined> {
|
||||
async function clearWorkspaceFolder(workspaceFolderUri: vscode.Uri): Promise<boolean> {
|
||||
|
||||
if (!workspaceFolder) { return undefined; }
|
||||
if (!workspaceFolderUri) { return undefined; }
|
||||
|
||||
// check if the workspace is empty, or clear it
|
||||
let existingWorkspaceFiles = readdirSync(workspaceFolder.uri.fsPath);
|
||||
let existingWorkspaceFiles: string[] = await afs.readdir(workspaceFolderUri.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 undefined; }
|
||||
{ placeHolder: `Remove ${existingWorkspaceFiles.length} file(s) from the folder ${workspaceFolderUri.fsPath} before cloning the remote repository?` });
|
||||
if (!answer) { return false; }
|
||||
|
||||
if (answer === "Yes") {
|
||||
existingWorkspaceFiles
|
||||
.map(filename =>
|
||||
unlinkSync(path.join(workspaceFolder.uri.fsPath, filename)));
|
||||
.forEach(async filename =>
|
||||
await afs.unlink(path.join(workspaceFolderUri.fsPath, filename)));
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceFolder;
|
||||
return true;
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
@ -277,4 +313,39 @@ async function openDocumentInColumn(fileName: string, column: vscode.ViewColumn)
|
||||
let doc = await vscode.workspace.openTextDocument(uri);
|
||||
|
||||
await vscode.window.showTextDocument(doc, { viewColumn: column });
|
||||
}
|
||||
}
|
||||
|
||||
abstract class WorkspaceFolderPick implements vscode.QuickPickItem {
|
||||
label: string;
|
||||
constructor(public folderOpeningMode: FolderOpeningMode) { }
|
||||
}
|
||||
|
||||
class ExistingWorkspaceFolderPick extends WorkspaceFolderPick {
|
||||
|
||||
constructor(public readonly workspaceFolder: vscode.WorkspaceFolder, private content: string[]) {
|
||||
super(FolderOpeningMode.AddToWorkspace);
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this.workspaceFolder.name;
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.workspaceFolder.uri.fsPath;
|
||||
}
|
||||
|
||||
get detail(): string {
|
||||
return this.content.length ? `${this.content.length} files/directories may need to be removed..` : null;
|
||||
}
|
||||
}
|
||||
|
||||
class NewWorkspaceFolderPick extends WorkspaceFolderPick {
|
||||
constructor(public label: string, folderOpeningMode: FolderOpeningMode) {
|
||||
super(folderOpeningMode);
|
||||
}
|
||||
}
|
||||
|
||||
enum FolderOpeningMode { AddToWorkspace, OpenFolder }
|
||||
|
||||
const newWorkspaceFolderPick = new NewWorkspaceFolderPick("Select/create a local folder to add to this workspace...", FolderOpeningMode.AddToWorkspace);
|
||||
const newFolderPick = new NewWorkspaceFolderPick("Select/create a local folder...", FolderOpeningMode.OpenFolder);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export interface FiddleConfiguration {
|
||||
readonly slug: string;
|
||||
readonly version?: number;
|
||||
readonly downloaded: boolean;
|
||||
}
|
||||
|
||||
export function parseFiddleId(id: string): FiddleConfiguration {
|
||||
@ -8,5 +9,5 @@ export function parseFiddleId(id: string): FiddleConfiguration {
|
||||
let fiddleSlug = idFragments[0];
|
||||
let fiddleVersion = idFragments.length > 1 ? parseInt(id.split('/')[1]) : undefined;
|
||||
|
||||
return { slug: fiddleSlug, version: fiddleVersion };
|
||||
return { slug: fiddleSlug, version: fiddleVersion, downloaded: false };
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { FiddleRepository, toExtension, downloadFiddle, areIdentical, uploadFiddle, Fiddle, toFiddleId } from './fiddleRepository';
|
||||
import * as path from 'path';
|
||||
import { writeFileSync, existsSync, writeFile } from 'fs';
|
||||
import * as afs from './afs';
|
||||
import { FiddleConfiguration, parseFiddleId } from './fiddleConfiguration';
|
||||
import { UTF8 } from './util';
|
||||
|
||||
export const CONFIGURATION_FILE = '.jsfiddle';
|
||||
|
||||
@ -15,7 +16,7 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
private timeout?: NodeJS.Timer;
|
||||
private fiddle!: Fiddle;
|
||||
|
||||
constructor(context: vscode.ExtensionContext, private readonly workspaceFolder: vscode.WorkspaceFolder, fiddle: Fiddle, overwrite: boolean) {
|
||||
constructor(context: vscode.ExtensionContext, private readonly workspaceFolder: vscode.WorkspaceFolder, fiddle: Fiddle, download: 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.slug);
|
||||
@ -31,7 +32,7 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
context.subscriptions.push(fileSystemWatcher);
|
||||
|
||||
// clone fiddle to the local workspace
|
||||
this.setFiddle(fiddle, overwrite);
|
||||
this.setFiddle(fiddle, download);
|
||||
|
||||
if (this.fiddle.version === undefined || Number.isNaN(this.fiddle.version)) {
|
||||
this.establishVersion();
|
||||
@ -106,9 +107,9 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
}
|
||||
|
||||
/** Resets the given local file content to the checked-out version. */
|
||||
private resetFile(extension: string): void {
|
||||
private async resetFile(extension: string): Promise<void> {
|
||||
let filePath = this.fiddleRepository.createLocalResourcePath(extension);
|
||||
writeFileSync(filePath, this.fiddle.data[extension]);
|
||||
await afs.writeFile(filePath, this.fiddle.data[extension]);
|
||||
}
|
||||
|
||||
async tryCheckout(newVersion: number | undefined): Promise<void> {
|
||||
@ -172,7 +173,8 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
private saveCurrentConfiguration(): void {
|
||||
let fiddleConfiguration: FiddleConfiguration = {
|
||||
slug: this.fiddle.slug,
|
||||
version: this.fiddle.version
|
||||
version: this.fiddle.version,
|
||||
downloaded: true
|
||||
};
|
||||
|
||||
FiddleSourceControl.saveConfiguration(this.workspaceFolder.uri, fiddleConfiguration);
|
||||
@ -180,9 +182,7 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
|
||||
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);
|
||||
});
|
||||
afs.writeFile(path.join(workspaceFolderUri.fsPath, CONFIGURATION_FILE), Buffer.from(fiddleConfigurationString, UTF8));
|
||||
}
|
||||
|
||||
get onRepositoryChange(): vscode.Event<Fiddle> {
|
||||
@ -214,7 +214,9 @@ export class FiddleSourceControl implements vscode.Disposable {
|
||||
let isDirty: boolean;
|
||||
let wasDeleted: boolean;
|
||||
|
||||
if (existsSync(uri.fsPath)) {
|
||||
let pathExists = await afs.exists(uri.fsPath);
|
||||
|
||||
if (pathExists) {
|
||||
let document = await vscode.workspace.openTextDocument(uri);
|
||||
isDirty = this.isDirty(document);
|
||||
wasDeleted = false;
|
||||
|
||||
@ -7,3 +7,5 @@ export function firstIndex<T>(array: T[], fn: (t: T) => boolean): number {
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export const UTF8 = 'utf8';
|
||||
Reference in New Issue
Block a user