From f963973eef1ba187512a95bd4cc6a769ef1df57c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 22 Jan 2018 12:45:11 +0100 Subject: [PATCH] first cut of proposed updateWorkspaceFolders API --- src/vs/vscode.proposed.d.ts | 31 ++++ .../electron-browser/mainThreadWorkspace.ts | 138 +++++++++++++++++- src/vs/workbench/api/node/extHost.api.impl.ts | 3 + src/vs/workbench/api/node/extHost.protocol.ts | 1 + src/vs/workbench/api/node/extHostWorkspace.ts | 4 + .../node/configurationService.ts | 2 +- .../workspace/node/workspaceEditingService.ts | 20 +-- 7 files changed, 186 insertions(+), 13 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7cbea7b4289..162f08a5eb2 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -157,6 +157,37 @@ declare module 'vscode' { export namespace workspace { export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider): Disposable; + + /** + * Updates the workspace folders of the currently opened workspace. This method allows to add and remove + * workspace folders a the same time. + * + * Example: adding a new workspace folder at the end of workspace folders + * ```typescript + * workspace.updateWorkspaceFolders(workspace.workspaceFolders ? workspace.workspaceFolders.length : 0, null, [{ uri: ...}]) + * ``` + * + * Example: removing the first workspace folder + * ```typescript + * workspace.updateWorkspaceFolders(0, 1) + * ``` + * + * Example: replacing an existing workspace folder with a new one + * ```typescript + * workspace.updateWorkspaceFolders(0, 1, [{ uri: ...}]) + * ``` + * + * Note: if the first workspace folder is removed or changed, all extensions will be restarted + * so that the (deprecated) `rootPath` property is updated to point to the first workspace + * folder. + * + * @param index the zero-based index in the list of currently opened [workspace folders](#WorkspaceFolder) + * from where to delete workspace folders or from where to add to. + * @param deleteCount the optional number of workspace folders to delete from the index that is provided. + * @param workspaceFoldersToAdd the optional number of workspace folders to add + * @return A thenable that resolves when the workspace folder was removed successfully + */ + export function updateWorkspaceFolders(index: number, deleteCount?: number, workspaceFoldersToAdd?: { uri: Uri, name?: string }[]): Thenable; } export namespace window { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 08382f85d1a..54d563065a1 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -5,7 +5,7 @@ 'use strict'; import { isPromiseCanceledError } from 'vs/base/common/errors'; -import URI from 'vs/base/common/uri'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { ISearchService, QueryType, ISearchQuery, IFolderQuery, ISearchConfiguration } from 'vs/platform/search/common/search'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -13,11 +13,20 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; +import { IMessageService, IConfirmation } from 'vs/platform/message/common/message'; +import { localize } from 'vs/nls'; +import { getPathLabel } from 'vs/base/common/labels'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { + private static CONFIRM_CHANGES_TO_WORKSPACES_KEY = 'workbench.confirmChangesToWorkspaceFromExtensions'; + private readonly _toDispose: IDisposable[] = []; private readonly _activeSearches: { [id: number]: TPromise } = Object.create(null); private readonly _proxy: ExtHostWorkspaceShape; @@ -27,7 +36,10 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @ITextFileService private readonly _textFileService: ITextFileService, - @IConfigurationService private _configurationService: IConfigurationService + @IConfigurationService private _configurationService: IConfigurationService, + @IWorkspaceEditingService private _workspaceEditingService: IWorkspaceEditingService, + @IMessageService private _messageService: IMessageService, + @IEnvironmentService private _environmentService: IEnvironmentService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose); @@ -45,6 +57,110 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- workspace --- + $updateWorkspaceFolders(extensionName: string, index: number, deleteCount?: number, add?: { uri: UriComponents, name?: string }[]): Thenable { + let workspaceFoldersToAdd: { uri: URI, name?: string }[] = []; + if (Array.isArray(add)) { + workspaceFoldersToAdd = add.map(f => ({ uri: URI.revive(f.uri), name: f.name })); + } + + let workspaceFoldersToRemove: URI[] = []; + if (typeof deleteCount === 'number') { + workspaceFoldersToRemove = this._contextService.getWorkspace().folders.slice(index, index + deleteCount).map(f => f.uri); + } + + if (!workspaceFoldersToAdd.length && !workspaceFoldersToRemove.length) { + return TPromise.as(false); // return early if we neither have folders to add nor remove + } + + return this.confirmUpdateWorkspaceFolders(extensionName, workspaceFoldersToRemove, workspaceFoldersToAdd.map(f => f.uri)).then(confirmed => { + if (!confirmed) { + return TPromise.as(false); // return if not confirmed by the user + } + + return this._workspaceEditingService.updateFolders(index, deleteCount, workspaceFoldersToAdd, true).then(() => true); + }); + } + + private confirmUpdateWorkspaceFolders(extensionName: string, workspaceFoldersToRemove?: URI[], workspaceFoldersToAdd?: URI[]): Thenable { + if (!this._configurationService.getValue(MainThreadWorkspace.CONFIRM_CHANGES_TO_WORKSPACES_KEY)) { + return TPromise.as(true); // return confirmed if the setting indicates this + } + + return this._messageService.confirmWithCheckbox(this.getConfirmationOptions(extensionName, workspaceFoldersToRemove, workspaceFoldersToAdd)).then(confirmation => { + let updateConfirmSettingsPromise: TPromise = TPromise.as(void 0); + if (confirmation.confirmed && confirmation.checkboxChecked === true) { + updateConfirmSettingsPromise = this._configurationService.updateValue(MainThreadWorkspace.CONFIRM_CHANGES_TO_WORKSPACES_KEY, false, ConfigurationTarget.USER); + } + + return updateConfirmSettingsPromise.then(() => confirmation.confirmed); + }); + } + + private getConfirmationOptions(extensionName, workspaceFoldersToRemove?: URI[], workspaceFoldersToAdd?: URI[]): IConfirmation { + const wantsToDelete = Array.isArray(workspaceFoldersToRemove) && workspaceFoldersToRemove.length; + const wantsToAdd = Array.isArray(workspaceFoldersToAdd) && workspaceFoldersToAdd.length; + + let message: string; + let detail: string; + let primaryButton: string; + + // Add Folders + if (wantsToAdd && !wantsToDelete) { + if (workspaceFoldersToAdd.length === 1) { + message = localize('folderMessageAddSingleFolder', "Extension '{0}' wants to add a folder to the workspace. Please confirm.", extensionName); + primaryButton = localize('addFolder', "&&Add Folder"); + } else { + message = localize('folderMessageAddMultipleFolders', "Extension '{0}' wants to add {1} folders to the workspace. Please confirm.", extensionName, workspaceFoldersToAdd.length); + primaryButton = localize('addFolders', "&&Add Folders"); + } + + detail = this.getConfirmationDetail(workspaceFoldersToAdd, false); + } + + // Delete Folders + else if (wantsToDelete && !wantsToAdd) { + if (workspaceFoldersToRemove.length === 1) { + message = localize('folderMessageRemoveSingleFolder', "Extension '{0}' wants to remove a folder from the workspace. Please confirm.", extensionName); + primaryButton = localize('removeFolder', "&&Remove Folder"); + } else { + message = localize('folderMessageRemoveMultipleFolders', "Extension '{0}' wants to remove folders from the workspace. Please confirm.", extensionName); + primaryButton = localize('removeFolders', "&&Remove Folders"); + } + + detail = this.getConfirmationDetail(workspaceFoldersToRemove, true); + } + + // Change Folders + else { + message = localize('folderChangeFolder', "Extension '{0}' wants to change the folders of the workspace. Please confirm.", extensionName); + primaryButton = localize('changeFolders', "&&Change Folders"); + + detail = [this.getConfirmationDetail(workspaceFoldersToAdd, false), this.getConfirmationDetail(workspaceFoldersToRemove, true)].join('\n\n'); + } + + return { message, detail, type: 'question', primaryButton, checkbox: { label: localize('doNotAskAgain', "Do not ask me again") } }; + } + + private getConfirmationDetail(folders: URI[], isRemove: boolean): string { + const getFolderName = uri => { + return uri.scheme === 'file' ? getPathLabel(uri.fsPath, null, this._environmentService) : uri.toString(); + }; + + if (folders.length === 1) { + if (isRemove) { + return [localize('folderToRemove', "Folder to remove:"), ...folders.map(f => getFolderName(f))].join('\n'); + } + + return [localize('folderToAdd', "Folder to add:"), ...folders.map(f => getFolderName(f))].join('\n'); + } + + if (isRemove) { + return [localize('foldersToRemove', "Folders to remove:"), ...folders.map(f => getFolderName(f))].join('\n'); + } + + return [localize('foldersToAdd', "Folders to add:"), ...folders.map(f => getFolderName(f))].join('\n'); + } + private _onDidChangeWorkspace(): void { this._proxy.$acceptWorkspaceData(this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace()); } @@ -123,3 +239,19 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { }); } } + +const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + +configurationRegistry.registerConfiguration({ + 'id': 'workbench', + 'order': 7, + 'title': localize('workbenchConfigurationTitle', "Workbench"), + 'type': 'object', + 'properties': { + 'workbench.confirmChangesToWorkspaceFromExtensions': { + 'type': 'boolean', + 'description': localize('confirmChangesFromExtensions', "Controls if a confirmation should be shown for extensions that add or remove workspace folders."), + 'default': true + } + } +}); \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 4d6eb312357..2405ce5b037 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -418,6 +418,9 @@ export function createApiFactory( set name(value) { throw errors.readonly(); }, + updateWorkspaceFolders: proposedApiFunction(extension, (index, deleteCount, workspaceFoldersToAdd) => { + return extHostWorkspace.updateWorkspaceFolders(extension.displayName || extension.name, index, deleteCount, workspaceFoldersToAdd); + }), onDidChangeWorkspaceFolders: function (listener, thisArgs?, disposables?) { return extHostWorkspace.onDidChangeWorkspace(listener, thisArgs, disposables); }, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index ea9d6e8b960..b7d5d23db57 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -364,6 +364,7 @@ export interface MainThreadWorkspaceShape extends IDisposable { $startSearch(includePattern: string, includeFolder: string, excludePattern: string, maxResults: number, requestId: number): Thenable; $cancelSearch(requestId: number): Thenable; $saveAll(includeUntitled?: boolean): Thenable; + $updateWorkspaceFolders(extensionName: string, index: number, deleteCount?: number, workspaceFoldersToAdd?: { uri: UriComponents, name?: string }[]): Thenable; } export interface IFileChangeDto { diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 2152866058e..6431e6fbdf5 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -86,6 +86,10 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { } } + updateWorkspaceFolders(extensionName: string, index: number, deleteCount?: number, workspaceFoldersToAdd?: { uri: vscode.Uri, name?: string }[]): Thenable { + return this._proxy.$updateWorkspaceFolders(extensionName, index, deleteCount, workspaceFoldersToAdd); + } + getWorkspaceFolder(uri: vscode.Uri, resolveParent?: boolean): vscode.WorkspaceFolder { if (!this._workspace) { return undefined; diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 2f8413c1199..8162473cd97 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -178,7 +178,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat if (storedFoldersToAdd.length > 0) { let newStoredWorkspaceFolders: IStoredWorkspaceFolder[] = []; - if (typeof index === 'number' && index >= 0 && index < currentStoredFolders.length - 1) { + if (typeof index === 'number' && index >= 0 && index < currentStoredFolders.length) { newStoredWorkspaceFolders = currentStoredFolders.slice(0); newStoredWorkspaceFolders.splice(index, 0, ...storedFoldersToAdd); } else { diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index ead13721d64..deed5f8603c 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -49,16 +49,18 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { public updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise { const folders = this.contextService.getWorkspace().folders; - if (index < 0 || index > folders.length - 1) { - return TPromise.wrapError(new Error(nls.localize('errorInvalidIndex', "The index for updating workspace folders is invalid."))); - } - if (typeof deleteCount === 'number' && (deleteCount < 0 || index + deleteCount > folders.length - 1)) { - return TPromise.wrapError(new Error(nls.localize('errorInvalidDelete', "The number of workspace folders to delete is invalid."))); + let foldersToDelete: URI[] = []; + if (typeof deleteCount === 'number') { + foldersToDelete = folders.slice(index, index + deleteCount).map(f => f.uri); } - const wantsToDelete = typeof deleteCount === 'number'; - const wantsToAdd = Array.isArray(foldersToAdd) && foldersToAdd.length; + const wantsToDelete = foldersToDelete.length > 0; + const wantsToAdd = Array.isArray(foldersToAdd) && foldersToAdd.length > 0; + + if (!wantsToAdd && !wantsToDelete) { + return TPromise.as(void 0); // return early if there is nothing to do + } // Add Folders if (wantsToAdd && !wantsToDelete) { @@ -67,11 +69,11 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { // Delete Folders if (wantsToDelete && !wantsToAdd) { - return this.removeFolders(folders.slice(index, index + deleteCount).map(f => f.uri)); + return this.removeFolders(foldersToDelete); } // Add & Delete Folders (first remove and then add to allow for updating existing folders) - return this.removeFolders(folders.slice(index, index + deleteCount).map(f => f.uri)).then(() => { + return this.removeFolders(foldersToDelete).then(() => { return this.doAddFolders(foldersToAdd, index, donotNotifyError); }); } -- GitLab