diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 1493935a0130b..9ba87d6253599 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -205,8 +205,8 @@ export class Workspace implements IWorkspace { export class WorkspaceFolder implements IWorkspaceFolder { readonly uri: URI; - readonly name: string; - readonly index: number; + name: string; + index: number; constructor(data: IWorkspaceFolderData, readonly raw?: IStoredWorkspaceFolder) { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 4551f8a4f2aca..073054ad61a56 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -157,6 +157,41 @@ 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, remove + * and change 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: ...}); + * ``` + * + * It is valid to remove an existing workspace folder and add it again with a different name + * to rename that folder. + * + * Note: if the first workspace folder is added, removed or changed, all extensions will be restarted + * so that the (deprecated) `rootPath` property is updated to point to the first workspace + * folder. + * @param start the zero-based location in the list of currently opened [workspace folders](#WorkspaceFolder) + * from which to start deleting workspace folders. + * @param deleteCount the optional number of workspace folders to remove. + * @param workspaceFoldersToAdd the optional variable set of workspace folders to add in place of the deleted ones. + * Each workspace is identified with a mandatory URI and an optional name. + * @return true if the operation was successfully started. Use the [onDidChangeWorkspaceFolders()](#onDidChangeWorkspaceFolders) + * event to get notified when the workspace folders have been updated. + */ + export function updateWorkspaceFolders(start: number, deleteCount: number, ...workspaceFoldersToAdd: { uri: Uri, name?: string }[]): boolean; } export namespace window { diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 08382f85d1ac8..b1cd93ae038a8 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'; @@ -14,6 +14,9 @@ import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainCo 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 { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; +import { localize } from 'vs/nls'; +import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { @@ -27,7 +30,9 @@ 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, + @IStatusbarService private _statusbarService: IStatusbarService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace); this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose); @@ -45,6 +50,47 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { // --- workspace --- + $updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, foldersToAdd: { uri: UriComponents, name?: string }[]): Thenable { + const workspaceFoldersToAdd = foldersToAdd.map(f => ({ uri: URI.revive(f.uri), name: f.name })); + + // Indicate in status message + this._statusbarService.setStatusMessage(this.getStatusMessage(extensionName, workspaceFoldersToAdd.length, deleteCount), 10 * 1000 /* 10s */); + + return this._workspaceEditingService.updateFolders(index, deleteCount, workspaceFoldersToAdd, true); + } + + private getStatusMessage(extensionName, addCount: number, removeCount: number): string { + let message: string; + + const wantsToAdd = addCount > 0; + const wantsToDelete = removeCount > 0; + + // Add Folders + if (wantsToAdd && !wantsToDelete) { + if (addCount === 1) { + message = localize('folderStatusMessageAddSingleFolder', "Extension '{0}' added 1 folder to the workspace", extensionName); + } else { + message = localize('folderStatusMessageAddMultipleFolders', "Extension '{0}' added {1} folders to the workspace", extensionName, addCount); + } + } + + // Delete Folders + else if (wantsToDelete && !wantsToAdd) { + if (removeCount === 1) { + message = localize('folderStatusMessageRemoveSingleFolder', "Extension '{0}' removed 1 folder from the workspace", extensionName); + } else { + message = localize('folderStatusMessageRemoveMultipleFolders', "Extension '{0}' removed {1} folders from the workspace", extensionName, removeCount); + } + } + + // Change Folders + else { + message = localize('folderStatusChangeFolder', "Extension '{0}' changed folders of the workspace", extensionName); + } + + return message; + } + private _onDidChangeWorkspace(): void { this._proxy.$acceptWorkspaceData(this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace()); } @@ -122,4 +168,4 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return result.results.every(each => each.success === 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 c3dc59749724f..3b62afdfdfe30 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -419,6 +419,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 c98531aa15a4d..dd1bd6574d5e0 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -349,6 +349,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 2152866058ef3..5592dfb8a7565 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -7,40 +7,107 @@ import URI from 'vs/base/common/uri'; import Event, { Emitter } from 'vs/base/common/event'; import { normalize } from 'vs/base/common/paths'; -import { delta } from 'vs/base/common/arrays'; +import { delta as arrayDelta } from 'vs/base/common/arrays'; import { relative, dirname } from 'path'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext } from './extHost.protocol'; import * as vscode from 'vscode'; import { compare } from 'vs/base/common/strings'; import { TernarySearchTree } from 'vs/base/common/map'; +import { basenameOrAuthority, isEqual } from 'vs/base/common/resources'; +import { isLinux } from 'vs/base/common/platform'; +import { onUnexpectedError } from 'vs/base/common/errors'; -class Workspace2 extends Workspace { +function isFolderEqual(folderA: URI, folderB: URI): boolean { + return isEqual(folderA, folderB, !isLinux); +} + +function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { + return isFolderEqual(a.uri, b.uri) ? 0 : compare(a.uri.toString(), b.uri.toString()); +} + +function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { + if (a.index !== b.index) { + return a.index < b.index ? -1 : 1; + } + + return isFolderEqual(a.uri, b.uri) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString()); +} + +function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder) => number): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } { + const oldSortedFolders = oldFolders.slice(0).sort(compare); + const newSortedFolders = newFolders.slice(0).sort(compare); + + return arrayDelta(oldSortedFolders, newSortedFolders, compare); +} + +interface MutableWorkspaceFolder extends vscode.WorkspaceFolder { + name: string; + index: number; +} + +class ExtHostWorkspaceImpl extends Workspace { - static fromData(data: IWorkspaceData) { + static toExtHostWorkspace(data: IWorkspaceData, previousConfirmedWorkspace?: ExtHostWorkspaceImpl, previousUnconfirmedWorkspace?: ExtHostWorkspaceImpl): { workspace: ExtHostWorkspaceImpl, added: vscode.WorkspaceFolder[], removed: vscode.WorkspaceFolder[] } { if (!data) { - return null; + return { workspace: null, added: [], removed: [] }; + } + + const { id, name, folders } = data; + const newWorkspaceFolders: vscode.WorkspaceFolder[] = []; + + // If we have an existing workspace, we try to find the folders that match our + // data and update their properties. It could be that an extension stored them + // for later use and we want to keep them "live" if they are still present. + const oldWorkspace = previousConfirmedWorkspace; + if (oldWorkspace) { + folders.forEach((folderData, index) => { + const folderUri = URI.revive(folderData.uri); + const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri); + + if (existingFolder) { + existingFolder.name = folderData.name; + existingFolder.index = folderData.index; + + newWorkspaceFolders.push(existingFolder); + } else { + newWorkspaceFolders.push({ uri: folderUri, name: folderData.name, index }); + } + }); } else { - const { id, name, folders } = data; - return new Workspace2( - id, - name, - folders.map(({ uri, name, index }) => new WorkspaceFolder({ name, index, uri: URI.revive(uri) })) - ); + newWorkspaceFolders.push(...folders.map(({ uri, name, index }) => ({ uri: URI.revive(uri), name, index }))); } + + // make sure to restore sort order based on index + newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1); + + const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders); + const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri); + + return { workspace, added, removed }; + } + + private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI): MutableWorkspaceFolder { + for (let i = 0; i < workspace.folders.length; i++) { + const folder = workspace.workspaceFolders[i]; + if (isFolderEqual(folder.uri, folderUriToFind)) { + return folder; + } + } + + return undefined; } private readonly _workspaceFolders: vscode.WorkspaceFolder[] = []; private readonly _structure = TernarySearchTree.forPaths(); - private constructor(id: string, name: string, folders: WorkspaceFolder[]) { - super(id, name, folders); + private constructor(id: string, name: string, folders: vscode.WorkspaceFolder[]) { + super(id, name, folders.map(f => new WorkspaceFolder(f))); // setup the workspace folder data structure - this.folders.forEach(({ name, uri, index }) => { - const workspaceFolder = { name, uri, index }; - this._workspaceFolders.push(workspaceFolder); - this._structure.set(workspaceFolder.uri.toString(), workspaceFolder); + folders.forEach(folder => { + this._workspaceFolders.push(folder); + this._structure.set(folder.uri.toString(), folder); }); } @@ -63,44 +130,98 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { private readonly _onDidChangeWorkspace = new Emitter(); private readonly _proxy: MainThreadWorkspaceShape; - private _workspace: Workspace2; + + private _confirmedWorkspace: ExtHostWorkspaceImpl; + private _unconfirmedWorkspace: ExtHostWorkspaceImpl; readonly onDidChangeWorkspace: Event = this._onDidChangeWorkspace.event; constructor(mainContext: IMainContext, data: IWorkspaceData) { this._proxy = mainContext.getProxy(MainContext.MainThreadWorkspace); - this._workspace = Workspace2.fromData(data); + this._confirmedWorkspace = ExtHostWorkspaceImpl.toExtHostWorkspace(data).workspace; } // --- workspace --- get workspace(): Workspace { - return this._workspace; + return this._actualWorkspace; + } + + private get _actualWorkspace(): ExtHostWorkspaceImpl { + return this._unconfirmedWorkspace || this._confirmedWorkspace; } getWorkspaceFolders(): vscode.WorkspaceFolder[] { - if (!this._workspace) { + if (!this._actualWorkspace) { return undefined; - } else { - return this._workspace.workspaceFolders.slice(0); } + return this._actualWorkspace.workspaceFolders.slice(0); + } + + updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, ...workspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[]): boolean { + const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[] = []; + if (Array.isArray(workspaceFoldersToAdd)) { + workspaceFoldersToAdd.forEach(folderToAdd => { + if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri))) { + validatedDistinctWorkspaceFoldersToAdd.push({ uri: folderToAdd.uri, name: folderToAdd.name || basenameOrAuthority(folderToAdd.uri) }); + } + }); + } + + if ([index, deleteCount].some(i => typeof i !== 'number' || i < 0)) { + return false; // validate numbers + } + + if (deleteCount === 0 && validatedDistinctWorkspaceFoldersToAdd.length === 0) { + return false; // nothing to delete or add + } + + const currentWorkspaceFolders: MutableWorkspaceFolder[] = this._actualWorkspace ? this._actualWorkspace.workspaceFolders : []; + if (index + deleteCount > currentWorkspaceFolders.length) { + return false; // cannot delete more than we have + } + + const newWorkspaceFolders = currentWorkspaceFolders.slice(0); + newWorkspaceFolders.splice(index, deleteCount, ...validatedDistinctWorkspaceFoldersToAdd.map(f => ({ uri: f.uri, name: f.name || basenameOrAuthority(f.uri) }))); + newWorkspaceFolders.forEach((f, index) => f.index = index); // fix index + const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex); + if (added.length === 0 && removed.length === 0) { + return false; // nothing actually changed + } + + // Trigger on main side + if (this._proxy) { + this._proxy.$updateWorkspaceFolders(extensionName, index, deleteCount, validatedDistinctWorkspaceFoldersToAdd).then(null, onUnexpectedError); + } + + // Try to accept directly + const accepted = this.trySetWorkspaceData({ + id: this._actualWorkspace.id, + name: this._actualWorkspace.name, + configuration: this._actualWorkspace.configuration, + folders: newWorkspaceFolders + } as IWorkspaceData); + + return accepted; } getWorkspaceFolder(uri: vscode.Uri, resolveParent?: boolean): vscode.WorkspaceFolder { - if (!this._workspace) { + if (!this._actualWorkspace) { return undefined; } - return this._workspace.getWorkspaceFolder(uri, resolveParent); + return this._actualWorkspace.getWorkspaceFolder(uri, resolveParent); } getPath(): string { + // this is legacy from the days before having // multi-root and we keep it only alive if there // is just one workspace folder. - if (!this._workspace) { + if (!this._actualWorkspace) { return undefined; } - const { folders } = this._workspace; + + const { folders } = this._actualWorkspace; if (folders.length === 0) { return undefined; } @@ -130,7 +251,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { } if (typeof includeWorkspace === 'undefined') { - includeWorkspace = this.workspace.folders.length > 1; + includeWorkspace = this._actualWorkspace.folders.length > 1; } let result = relative(folder.uri.fsPath, path); @@ -140,27 +261,35 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { return normalize(result, true); } + private trySetWorkspaceData(data: IWorkspaceData): boolean { + + // Update directly here. The workspace is unconfirmed as long as we did not get an + // acknowledgement from the main side (via $acceptWorkspaceData) + if (this._actualWorkspace) { + this._unconfirmedWorkspace = ExtHostWorkspaceImpl.toExtHostWorkspace(data, this._actualWorkspace).workspace; + + return true; + } + + return false; + } + $acceptWorkspaceData(data: IWorkspaceData): void { - // keep old workspace folder, build new workspace, and - // capture new workspace folders. Compute delta between - // them send that as event - const oldRoots = this._workspace ? this._workspace.workspaceFolders.sort(ExtHostWorkspace._compareWorkspaceFolder) : []; + const { workspace, added, removed } = ExtHostWorkspaceImpl.toExtHostWorkspace(data, this._confirmedWorkspace, this._unconfirmedWorkspace); - this._workspace = Workspace2.fromData(data); - const newRoots = this._workspace ? this._workspace.workspaceFolders.sort(ExtHostWorkspace._compareWorkspaceFolder) : []; + // Update our workspace object. We have a confirmed workspace, so we drop our + // unconfirmed workspace. + this._confirmedWorkspace = workspace; + this._unconfirmedWorkspace = undefined; - const { added, removed } = delta(oldRoots, newRoots, ExtHostWorkspace._compareWorkspaceFolder); + // Events this._onDidChangeWorkspace.fire(Object.freeze({ added: Object.freeze(added), removed: Object.freeze(removed) })); } - private static _compareWorkspaceFolder(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number { - return compare(a.uri.toString(), b.uri.toString()); - } - // --- search --- findFiles(include: vscode.GlobPattern, exclude: vscode.GlobPattern, maxResults?: number, token?: vscode.CancellationToken): Thenable { diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index 4e3b25efb56e1..577c4f78dcdfd 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -110,9 +110,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this.workspace.getFolder(resource); } - public addFolders(foldersToAdd: IWorkspaceFolderCreationData[]): TPromise { + public addFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number): TPromise { assert.ok(this.jsonEditingService, 'Workbench is not initialized yet'); - return this.workspaceEditingQueue.queue(() => this.doAddFolders(foldersToAdd)); + return this.workspaceEditingQueue.queue(() => this.doAddFolders(foldersToAdd, index)); } public removeFolders(foldersToRemove: URI[]): TPromise { @@ -134,7 +134,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return false; } - private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[]): TPromise { + private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number): TPromise { if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) { return TPromise.as(void 0); // we need a workspace to begin with } @@ -176,7 +176,16 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat }); if (storedFoldersToAdd.length > 0) { - return this.setFolders([...currentStoredFolders, ...storedFoldersToAdd]); + let newStoredWorkspaceFolders: IStoredWorkspaceFolder[] = []; + + if (typeof index === 'number' && index >= 0 && index < currentStoredFolders.length) { + newStoredWorkspaceFolders = currentStoredFolders.slice(0); + newStoredWorkspaceFolders.splice(index, 0, ...storedFoldersToAdd); + } else { + newStoredWorkspaceFolders = [...currentStoredFolders, ...storedFoldersToAdd]; + } + + return this.setFolders(newStoredWorkspaceFolders); } return TPromise.as(void 0); diff --git a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts index 424b704b3d532..6563fd4a42c7d 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts @@ -192,6 +192,34 @@ suite('WorkspaceContextService - Workspace', () => { }); }); + test('add folders (at specific index)', () => { + const workspaceDir = path.dirname(testObject.getWorkspace().folders[0].uri.fsPath); + return testObject.addFolders([{ uri: URI.file(path.join(workspaceDir, 'd')) }, { uri: URI.file(path.join(workspaceDir, 'c')) }], 0) + .then(() => { + const actual = testObject.getWorkspace().folders; + + assert.equal(actual.length, 4); + assert.equal(path.basename(actual[0].uri.fsPath), 'd'); + assert.equal(path.basename(actual[1].uri.fsPath), 'c'); + assert.equal(path.basename(actual[2].uri.fsPath), 'a'); + assert.equal(path.basename(actual[3].uri.fsPath), 'b'); + }); + }); + + test('add folders (at specific wrong index)', () => { + const workspaceDir = path.dirname(testObject.getWorkspace().folders[0].uri.fsPath); + return testObject.addFolders([{ uri: URI.file(path.join(workspaceDir, 'd')) }, { uri: URI.file(path.join(workspaceDir, 'c')) }], 10) + .then(() => { + const actual = testObject.getWorkspace().folders; + + assert.equal(actual.length, 4); + assert.equal(path.basename(actual[0].uri.fsPath), 'a'); + assert.equal(path.basename(actual[1].uri.fsPath), 'b'); + assert.equal(path.basename(actual[2].uri.fsPath), 'd'); + assert.equal(path.basename(actual[3].uri.fsPath), 'c'); + }); + }); + test('add folders (with name)', () => { const workspaceDir = path.dirname(testObject.getWorkspace().folders[0].uri.fsPath); return testObject.addFolders([{ uri: URI.file(path.join(workspaceDir, 'd')), name: 'DDD' }, { uri: URI.file(path.join(workspaceDir, 'c')), name: 'CCC' }]) diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index 3e22bf28ba644..920be3ef66792 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -27,6 +27,12 @@ export interface IWorkspaceEditingService { */ removeFolders(folders: URI[], donotNotifyError?: boolean): TPromise; + /** + * Allows to add and remove folders to the existing workspace at once. + * When `donotNotifyError` is `true`, error will be bubbled up otherwise, the service handles the error with proper message and action + */ + updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise; + /** * creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 7bb21530f427e..c35254fc87fe5 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -47,16 +47,56 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { ) { } + public updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): TPromise { + const folders = this.contextService.getWorkspace().folders; + + let foldersToDelete: URI[] = []; + if (typeof deleteCount === 'number') { + foldersToDelete = folders.slice(index, index + deleteCount).map(f => f.uri); + } + + 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) { + return this.doAddFolders(foldersToAdd, index, donotNotifyError); + } + + // Delete Folders + if (wantsToDelete && !wantsToAdd) { + return this.removeFolders(foldersToDelete); + } + + // Add & Delete Folders + if (this.includesSingleFolderWorkspace(foldersToDelete)) { + // if we are in single-folder state and the folder is replaced with + // other folders, we handle this specially and just enter workspace + // mode with the folders that are being added. + return this.createAndEnterWorkspace(foldersToAdd); + } + + // Make sure to first remove folders and then add them to account for folders being updated + return this.removeFolders(foldersToDelete).then(() => this.doAddFolders(foldersToAdd, index, donotNotifyError)); + } + public addFolders(foldersToAdd: IWorkspaceFolderCreationData[], donotNotifyError: boolean = false): TPromise { + return this.doAddFolders(foldersToAdd, void 0, donotNotifyError); + } + + private doAddFolders(foldersToAdd: IWorkspaceFolderCreationData[], index?: number, donotNotifyError: boolean = false): TPromise { const state = this.contextService.getWorkbenchState(); // If we are in no-workspace or single-folder workspace, adding folders has to // enter a workspace. if (state !== WorkbenchState.WORKSPACE) { - const newWorkspaceFolders: IWorkspaceFolderCreationData[] = distinct([ - ...this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri } as IWorkspaceFolderCreationData)), - ...foldersToAdd - ] as IWorkspaceFolderCreationData[], folder => isLinux ? folder.uri.toString() : folder.uri.toString().toLowerCase()); + let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri } as IWorkspaceFolderCreationData)); + newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd); + newWorkspaceFolders = distinct(newWorkspaceFolders, folder => isLinux ? folder.uri.toString() : folder.uri.toString().toLowerCase()); if (state === WorkbenchState.EMPTY && newWorkspaceFolders.length === 0 || state === WorkbenchState.FOLDER && newWorkspaceFolders.length === 1) { return TPromise.as(void 0); // return if the operation is a no-op for the current state @@ -66,19 +106,16 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } // Delegate addition of folders to workspace service otherwise - return this.contextService.addFolders(foldersToAdd) + return this.contextService.addFolders(foldersToAdd, index) .then(() => null, error => donotNotifyError ? TPromise.wrapError(error) : this.handleWorkspaceConfigurationEditingError(error)); } public removeFolders(foldersToRemove: URI[], donotNotifyError: boolean = false): TPromise { // If we are in single-folder state and the opened folder is to be removed, - // we close the workspace and enter the empty workspace state for the window. - if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { - const workspaceFolder = this.contextService.getWorkspace().folders[0]; - if (foldersToRemove.some(folder => isEqual(folder, workspaceFolder.uri, !isLinux))) { - return this.windowService.closeWorkspace(); - } + // we create an empty workspace and enter it. + if (this.includesSingleFolderWorkspace(foldersToRemove)) { + return this.createAndEnterWorkspace([]); } // Delegate removal of folders to workspace service otherwise @@ -86,6 +123,15 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { .then(() => null, error => donotNotifyError ? TPromise.wrapError(error) : this.handleWorkspaceConfigurationEditingError(error)); } + private includesSingleFolderWorkspace(folders: URI[]): boolean { + if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { + const workspaceFolder = this.contextService.getWorkspace().folders[0]; + return (folders.some(folder => isEqual(folder, workspaceFolder.uri, !isLinux))); + } + + return false; + } + public createAndEnterWorkspace(folders?: IWorkspaceFolderCreationData[], path?: string): TPromise { return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folders, path)); } diff --git a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts index 75f5cb5effeb4..395826d82df29 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostWorkspace.test.ts @@ -159,57 +159,382 @@ suite('ExtHostWorkspace', function () { assert.equal(folder.name, 'Two'); }); - test('Multiroot change event should have a delta, #29641', function () { + test('Multiroot change event should have a delta, #29641', function (done) { let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }); + let finished = false; + const finish = (error?) => { + if (!finished) { + finished = true; + done(error); + } + }; + let sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.added, []); - assert.deepEqual(e.removed, []); + try { + assert.deepEqual(e.added, []); + assert.deepEqual(e.removed, []); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.removed, []); - assert.equal(e.added.length, 1); - assert.equal(e.added[0].uri.toString(), 'foo:bar'); + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar'); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.deepEqual(e.removed, []); - assert.equal(e.added.length, 1); - assert.equal(e.added[0].uri.toString(), 'foo:bar2'); + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar2'); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0), aWorkspaceFolderData(URI.parse('foo:bar2'), 1)] }); sub.dispose(); sub = ws.onDidChangeWorkspace(e => { - assert.equal(e.removed.length, 2); - assert.equal(e.removed[0].uri.toString(), 'foo:bar'); - assert.equal(e.removed[1].uri.toString(), 'foo:bar2'); - - assert.equal(e.added.length, 1); - assert.equal(e.added[0].uri.toString(), 'foo:bar3'); + try { + assert.equal(e.removed.length, 2); + assert.equal(e.removed[0].uri.toString(), 'foo:bar'); + assert.equal(e.removed[1].uri.toString(), 'foo:bar2'); + + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar3'); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0)] }); sub.dispose(); + finish(); + }); + + test('Multiroot change keeps existing workspaces live', function () { + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); + + let firstFolder = ws.getWorkspaceFolders()[0]; + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar2'), 0), aWorkspaceFolderData(URI.parse('foo:bar'), 1, 'renamed')] }); + + assert.equal(ws.getWorkspaceFolders()[1], firstFolder); + assert.equal(firstFolder.index, 1); + assert.equal(firstFolder.name, 'renamed'); + + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0), aWorkspaceFolderData(URI.parse('foo:bar2'), 1), aWorkspaceFolderData(URI.parse('foo:bar'), 2)] }); + assert.equal(ws.getWorkspaceFolders()[2], firstFolder); + assert.equal(firstFolder.index, 2); + + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0)] }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0), aWorkspaceFolderData(URI.parse('foo:bar'), 1)] }); + assert.notEqual(firstFolder, ws.workspace.folders[0]); }); - test('Multiroot change event is immutable', function () { + test('updateWorkspaceFolders - invalid arguments', function () { let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }); + + assert.equal(false, ws.updateWorkspaceFolders('ext', null, null)); + assert.equal(false, ws.updateWorkspaceFolders('ext', 0, 0)); + assert.equal(false, ws.updateWorkspaceFolders('ext', 0, 1)); + assert.equal(false, ws.updateWorkspaceFolders('ext', 1, 0)); + assert.equal(false, ws.updateWorkspaceFolders('ext', -1, 0)); + assert.equal(false, ws.updateWorkspaceFolders('ext', -1, -1)); + + ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); + + assert.equal(false, ws.updateWorkspaceFolders('ext', 1, 1)); + assert.equal(false, ws.updateWorkspaceFolders('ext', 0, 2)); + assert.equal(false, ws.updateWorkspaceFolders('ext', 0, 1, asUpdateWorkspaceFolderData(URI.parse('foo:bar')))); + }); + + test('updateWorkspaceFolders - valid arguments', function (done) { + let finished = false; + const finish = (error?) => { + if (!finished) { + finished = true; + done(error); + } + }; + + const protocol = { + getProxy: () => { return undefined; }, + set: undefined, + assertRegistered: undefined + }; + + const ws = new ExtHostWorkspace(protocol, { id: 'foo', name: 'Test', folders: [] }); + + // + // Add one folder + // + + assert.equal(true, ws.updateWorkspaceFolders('ext', 0, 0, asUpdateWorkspaceFolderData(URI.parse('foo:bar')))); + assert.equal(1, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + + const firstAddedFolder = ws.getWorkspaceFolders()[0]; + + let gotEvent = false; let sub = ws.onDidChangeWorkspace(e => { - assert.throws(() => { - (e).added = []; - }); - assert.throws(() => { - (e.added)[0] = null; - }); + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 1); + assert.equal(e.added[0].uri.toString(), 'foo:bar'); + assert.equal(e.added[0], firstAddedFolder); // verify object is still live + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + + // + // Add two more folders + // + + assert.equal(true, ws.updateWorkspaceFolders('ext', 1, 0, asUpdateWorkspaceFolderData(URI.parse('foo:bar1')), asUpdateWorkspaceFolderData(URI.parse('foo:bar2')))); + assert.equal(3, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar1').toString()); + assert.equal(ws.workspace.folders[2].uri.toString(), URI.parse('foo:bar2').toString()); + + const secondAddedFolder = ws.getWorkspaceFolders()[1]; + const thirdAddedFolder = ws.getWorkspaceFolders()[2]; + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.deepEqual(e.removed, []); + assert.equal(e.added.length, 2); + assert.equal(e.added[0].uri.toString(), 'foo:bar1'); + assert.equal(e.added[1].uri.toString(), 'foo:bar2'); + assert.equal(e.added[0], secondAddedFolder); + assert.equal(e.added[1], thirdAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0), aWorkspaceFolderData(URI.parse('foo:bar1'), 1), aWorkspaceFolderData(URI.parse('foo:bar2'), 2)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], secondAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[2], thirdAddedFolder); // verify object is still live + + // + // Remove one folder + // + + assert.equal(true, ws.updateWorkspaceFolders('ext', 2, 1)); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar1').toString()); + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.deepEqual(e.added, []); + assert.equal(e.removed.length, 1); + assert.equal(e.removed[0], thirdAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0), aWorkspaceFolderData(URI.parse('foo:bar1'), 1)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], secondAddedFolder); // verify object is still live + + // + // Rename folder + // + + assert.equal(true, ws.updateWorkspaceFolders('ext', 0, 2, asUpdateWorkspaceFolderData(URI.parse('foo:bar'), 'renamed 1'), asUpdateWorkspaceFolderData(URI.parse('foo:bar1'), 'renamed 2'))); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar1').toString()); + assert.equal(ws.workspace.folders[0].name, 'renamed 1'); + assert.equal(ws.workspace.folders[1].name, 'renamed 2'); + assert.equal(ws.getWorkspaceFolders()[0].name, 'renamed 1'); + assert.equal(ws.getWorkspaceFolders()[1].name, 'renamed 2'); + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.deepEqual(e.added, []); + assert.equal(e.removed.length, []); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar'), 0, 'renamed 1'), aWorkspaceFolderData(URI.parse('foo:bar1'), 1, 'renamed 2')] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], firstAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], secondAddedFolder); // verify object is still live + assert.equal(ws.workspace.folders[0].name, 'renamed 1'); + assert.equal(ws.workspace.folders[1].name, 'renamed 2'); + assert.equal(ws.getWorkspaceFolders()[0].name, 'renamed 1'); + assert.equal(ws.getWorkspaceFolders()[1].name, 'renamed 2'); + + // + // Add and remove folders + // + + assert.equal(true, ws.updateWorkspaceFolders('ext', 0, 2, asUpdateWorkspaceFolderData(URI.parse('foo:bar3')), asUpdateWorkspaceFolderData(URI.parse('foo:bar4')))); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar3').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar4').toString()); + + const fourthAddedFolder = ws.getWorkspaceFolders()[0]; + const fifthAddedFolder = ws.getWorkspaceFolders()[1]; + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.equal(e.added.length, 2); + assert.equal(e.added[0], fourthAddedFolder); + assert.equal(e.added[1], fifthAddedFolder); + assert.equal(e.removed.length, 2); + assert.equal(e.removed[0], firstAddedFolder); + assert.equal(e.removed[1], secondAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar3'), 0), aWorkspaceFolderData(URI.parse('foo:bar4'), 1)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], fourthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fifthAddedFolder); // verify object is still live + + // + // Swap folders + // + + assert.equal(true, ws.updateWorkspaceFolders('ext', 0, 2, asUpdateWorkspaceFolderData(URI.parse('foo:bar4')), asUpdateWorkspaceFolderData(URI.parse('foo:bar3')))); + assert.equal(2, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar4').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar3').toString()); + + assert.equal(ws.getWorkspaceFolders()[0], fifthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fourthAddedFolder); // verify object is still live + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.equal(e.added.length, 0); + assert.equal(e.removed.length, 0); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [aWorkspaceFolderData(URI.parse('foo:bar4'), 0), aWorkspaceFolderData(URI.parse('foo:bar3'), 1)] }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + assert.equal(ws.getWorkspaceFolders()[0], fifthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fourthAddedFolder); // verify object is still live + assert.equal(fifthAddedFolder.index, 0); + assert.equal(fourthAddedFolder.index, 1); + + // + // Add one folder after the other without waiting for confirmation + // + + assert.equal(true, ws.updateWorkspaceFolders('ext', 2, 0, asUpdateWorkspaceFolderData(URI.parse('foo:bar5')))); + + assert.equal(3, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar4').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar3').toString()); + assert.equal(ws.workspace.folders[2].uri.toString(), URI.parse('foo:bar5').toString()); + + assert.equal(true, ws.updateWorkspaceFolders('ext', 3, 0, asUpdateWorkspaceFolderData(URI.parse('foo:bar6')))); + + assert.equal(4, ws.workspace.folders.length); + assert.equal(ws.workspace.folders[0].uri.toString(), URI.parse('foo:bar4').toString()); + assert.equal(ws.workspace.folders[1].uri.toString(), URI.parse('foo:bar3').toString()); + assert.equal(ws.workspace.folders[2].uri.toString(), URI.parse('foo:bar5').toString()); + assert.equal(ws.workspace.folders[3].uri.toString(), URI.parse('foo:bar6').toString()); + + const sixthAddedFolder = ws.getWorkspaceFolders()[2]; + const seventhAddedFolder = ws.getWorkspaceFolders()[3]; + + gotEvent = false; + sub = ws.onDidChangeWorkspace(e => { + try { + assert.equal(e.added.length, 2); + assert.equal(e.added[0], sixthAddedFolder); + assert.equal(e.added[1], seventhAddedFolder); + gotEvent = true; + } catch (error) { + finish(error); + } + }); + ws.$acceptWorkspaceData({ + id: 'foo', name: 'Test', folders: [ + aWorkspaceFolderData(URI.parse('foo:bar4'), 0), + aWorkspaceFolderData(URI.parse('foo:bar3'), 1), + aWorkspaceFolderData(URI.parse('foo:bar5'), 2), + aWorkspaceFolderData(URI.parse('foo:bar6'), 3) + ] + }); // simulate acknowledgement from main side + assert.equal(gotEvent, true); + sub.dispose(); + + assert.equal(ws.getWorkspaceFolders()[0], fifthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[1], fourthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[2], sixthAddedFolder); // verify object is still live + assert.equal(ws.getWorkspaceFolders()[3], seventhAddedFolder); // verify object is still live + + finish(); + }); + + test('Multiroot change event is immutable', function (done) { + let finished = false; + const finish = (error?) => { + if (!finished) { + finished = true; + done(error); + } + }; + + let ws = new ExtHostWorkspace(new TestRPCProtocol(), { id: 'foo', name: 'Test', folders: [] }); + let sub = ws.onDidChangeWorkspace(e => { + try { + assert.throws(() => { + (e).added = []; + }); + assert.throws(() => { + (e.added)[0] = null; + }); + } catch (error) { + finish(error); + } }); ws.$acceptWorkspaceData({ id: 'foo', name: 'Test', folders: [] }); sub.dispose(); + finish(); }); test('`vscode.workspace.getWorkspaceFolder(file)` don\'t return workspace folder when file open from command line. #36221', function () { @@ -230,4 +555,8 @@ suite('ExtHostWorkspace', function () { name: name || basename(uri.path) }; } + + function asUpdateWorkspaceFolderData(uri: URI, name?: string): { uri: URI, name?: string } { + return { uri, name }; + } });