Skip to content

Commit 020e4dc

Browse files
bpaseroegamma
authored andcommitted
Provide additional workspace API to add/remove workspace folders (for #35407) (#36820)
* Provide additional workspace API to add/remove workspace folders (for #35407) * add/removeFolders => add/removeFolder * make add/remove folder return a boolean * use proper service for workspace editing * workspac => workspace * do not log promise canceled messages * show confirm dialog
1 parent ed5ffae commit 020e4dc

File tree

7 files changed

+118
-10
lines changed

7 files changed

+118
-10
lines changed

src/vs/vscode.d.ts

+28
Original file line numberDiff line numberDiff line change
@@ -5210,6 +5210,34 @@ declare module 'vscode' {
52105210
*/
52115211
export const onDidChangeWorkspaceFolders: Event<WorkspaceFoldersChangeEvent>;
52125212

5213+
/**
5214+
* Adds a workspace folder to the currently opened workspace.
5215+
*
5216+
* This method will be a no-op if the folder is already part of the workspace.
5217+
*
5218+
* Note: if this workspace had no folder opened, all extensions will be restarted
5219+
* so that the (deprecated) `rootPath` property is updated to point to the first workspace
5220+
* folder.
5221+
*
5222+
* @param folder a workspace folder to add.
5223+
* @return A thenable that resolves when the workspace folder was added successfully.
5224+
*/
5225+
export function addWorkspaceFolder(uri: Uri, name?: string): Thenable<boolean>;
5226+
5227+
/**
5228+
* Remove a workspace folder from the currently opened workspace.
5229+
*
5230+
* This method will be a no-op when called while not having a workspace opened.
5231+
*
5232+
* Note: if the first workspace folder is removed, all extensions will be restarted
5233+
* so that the (deprecated) `rootPath` property is updated to point to the first workspace
5234+
* folder.
5235+
*
5236+
* @param folder a [workspace folder](#WorkspaceFolder) to remove.
5237+
* @return A thenable that resolves when the workspace folder was removed successfully
5238+
*/
5239+
export function removeWorkspaceFolder(folder: WorkspaceFolder): Thenable<boolean>;
5240+
52135241
/**
52145242
* Returns the [workspace folder](#WorkspaceFolder) that contains a given uri.
52155243
* * returns `undefined` when the given uri doesn't match any workspace folder

src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts

+52-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainCo
1414
import { IFileService } from 'vs/platform/files/common/files';
1515
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
1616
import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers';
17-
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
17+
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
1818
import { IRelativePattern } from 'vs/base/common/glob';
19+
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
20+
import { IMessageService } from 'vs/platform/message/common/message';
21+
import { localize } from 'vs/nls';
1922

2023
@extHostNamedCustomer(MainContext.MainThreadWorkspace)
2124
export class MainThreadWorkspace implements MainThreadWorkspaceShape {
@@ -30,7 +33,9 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
3033
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
3134
@ITextFileService private readonly _textFileService: ITextFileService,
3235
@IConfigurationService private _configurationService: IConfigurationService,
33-
@IFileService private readonly _fileService: IFileService
36+
@IFileService private readonly _fileService: IFileService,
37+
@IWorkspaceEditingService private _workspaceEditingService: IWorkspaceEditingService,
38+
@IMessageService private _messageService: IMessageService
3439
) {
3540
this._proxy = extHostContext.get(ExtHostContext.ExtHostWorkspace);
3641
this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose);
@@ -52,6 +57,51 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
5257
this._proxy.$acceptWorkspaceData(this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : this._contextService.getWorkspace());
5358
}
5459

60+
$addFolder(extensionName: string, uri: URI, name?: string): Thenable<boolean> {
61+
return this.confirmAddRemoveFolder(extensionName, uri, false).then(confirmed => {
62+
if (!confirmed) {
63+
return TPromise.as(false);
64+
}
65+
66+
return this._workspaceEditingService.addFolders([{ uri, name }]).then(() => true);
67+
});
68+
}
69+
70+
$removeFolder(extensionName: string, uri: URI): Thenable<boolean> {
71+
return this.confirmAddRemoveFolder(extensionName, uri, true).then(confirmed => {
72+
if (!confirmed) {
73+
return TPromise.as(false);
74+
}
75+
76+
return this._workspaceEditingService.removeFolders([uri]).then(() => true);
77+
});
78+
}
79+
80+
private confirmAddRemoveFolder(extensionName, uri: URI, isRemove: boolean): Thenable<boolean> {
81+
if (!this._configurationService.getValue<boolean>('workbench.confirmChangesToWorkspaceFromExtensions')) {
82+
return TPromise.as(true); // return confirmed if the setting indicates this
83+
}
84+
85+
return this._messageService.confirm({
86+
message: isRemove ?
87+
localize('folderMessageRemove', "Extension {0} wants to remove a folder from the workspace. Please confirm.", extensionName) :
88+
localize('folderMessageAdd', "Extension {0} wants to add a folder to the workspace. Please confirm.", extensionName),
89+
detail: localize('folderPath', "Folder path: '{0}'", uri.scheme === 'file' ? uri.fsPath : uri.toString()),
90+
type: 'question',
91+
primaryButton: isRemove ? localize('removeFolder', "&&Remove Folder") : localize('addFolder', "&&Add Folder"),
92+
checkbox: {
93+
label: localize('doNotAskAgain', "Do not ask me again")
94+
}
95+
}).then(confirmation => {
96+
let updateConfirmSettingsPromise: TPromise<void> = TPromise.as(void 0);
97+
if (confirmation.confirmed && confirmation.checkboxChecked === true) {
98+
updateConfirmSettingsPromise = this._configurationService.updateValue('workbench.confirmChangesToWorkspaceFromExtensions', false, ConfigurationTarget.USER);
99+
}
100+
101+
return updateConfirmSettingsPromise.then(() => confirmation.confirmed);
102+
});
103+
}
104+
55105
// --- search ---
56106

57107
$startSearch(include: string | IRelativePattern, exclude: string | IRelativePattern, maxResults: number, requestId: number): Thenable<URI[]> {

src/vs/workbench/api/node/extHost.api.impl.ts

+6
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,12 @@ export function createApiFactory(
403403
set name(value) {
404404
throw errors.readonly();
405405
},
406+
addWorkspaceFolder(uri, name) {
407+
return extHostWorkspace.addWorkspaceFolder(extension.displayName || extension.name, uri, name);
408+
},
409+
removeWorkspaceFolder(folder) {
410+
return extHostWorkspace.removeWorkspaceFolder(extension.displayName || extension.name, folder);
411+
},
406412
onDidChangeWorkspaceFolders: function (listener, thisArgs?, disposables?) {
407413
return extHostWorkspace.onDidChangeWorkspace(listener, thisArgs, disposables);
408414
},

src/vs/workbench/api/node/extHost.protocol.ts

+2
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ export interface MainThreadWorkspaceShape extends IDisposable {
331331
$startSearch(include: string | IRelativePattern, exclude: string | IRelativePattern, maxResults: number, requestId: number): Thenable<URI[]>;
332332
$cancelSearch(requestId: number): Thenable<boolean>;
333333
$saveAll(includeUntitled?: boolean): Thenable<boolean>;
334+
$addFolder(extensioName: string, uri: URI, name?: string): Thenable<boolean>;
335+
$removeFolder(extensioName: string, uri: URI): Thenable<boolean>;
334336
}
335337

336338
export interface MainThreadFileSystemShape extends IDisposable {

src/vs/workbench/api/node/extHostWorkspace.ts

+12
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape {
7878
}
7979
}
8080

81+
addWorkspaceFolder(extensionName: string, uri: URI, name?: string): Thenable<boolean> {
82+
return this._proxy.$addFolder(extensionName, uri, name);
83+
}
84+
85+
removeWorkspaceFolder(extensionName: string, folder: vscode.WorkspaceFolder): Thenable<boolean> {
86+
if (this.getWorkspaceFolders().indexOf(folder) === -1) {
87+
return Promise.resolve(false);
88+
}
89+
90+
return this._proxy.$removeFolder(extensionName, folder.uri);
91+
}
92+
8193
getWorkspaceFolder(uri: vscode.Uri, resolveParent?: boolean): vscode.WorkspaceFolder {
8294
if (!this._workspace) {
8395
return undefined;

src/vs/workbench/electron-browser/main.contribution.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ let workbenchProperties: { [path: string]: IJSONSchema; } = {
247247
'type': 'boolean',
248248
'description': nls.localize('closeOnFileDelete', "Controls if editors showing a file should close automatically when the file is deleted or renamed by some other process. Disabling this will keep the editor open as dirty on such an event. Note that deleting from within the application will always close the editor and that dirty files will never close to preserve your data."),
249249
'default': true
250+
},
251+
'workbench.confirmChangesToWorkspaceFromExtensions': {
252+
'type': 'boolean',
253+
'description': nls.localize('confirmChangesFromExtensions', "Controls if a confirmation should be shown for extensions that add or remove workspace folders."),
254+
'default': true
250255
}
251256
};
252257

@@ -256,8 +261,8 @@ if (isMacintosh) {
256261
'enum': ['default', 'antialiased', 'none'],
257262
'default': 'default',
258263
'description':
259-
nls.localize('fontAliasing',
260-
`Controls font aliasing method in the workbench.
264+
nls.localize('fontAliasing',
265+
`Controls font aliasing method in the workbench.
261266
- default: Sub-pixel font smoothing. On most non-retina displays this will give the sharpest text
262267
- antialiased: Smooth the font on the level of the pixel, as opposed to the subpixel. Can make the font appear lighter overall
263268
- none: Disables font smoothing. Text will show with jagged sharp edges`),
@@ -297,13 +302,13 @@ let properties: { [path: string]: IJSONSchema; } = {
297302
],
298303
'default': 'off',
299304
'description':
300-
nls.localize('openFilesInNewWindow',
301-
`Controls if files should open in a new window.
305+
nls.localize('openFilesInNewWindow',
306+
`Controls if files should open in a new window.
302307
- default: files will open in the window with the files' folder open or the last active window unless opened via the dock or from finder (macOS only)
303308
- on: files will open in a new window
304309
- off: files will open in the window with the files' folder open or the last active window
305310
Note that there can still be cases where this setting is ignored (e.g. when using the -new-window or -reuse-window command line option).`
306-
)
311+
)
307312
},
308313
'window.openFoldersInNewWindow': {
309314
'type': 'string',

src/vs/workbench/parts/quickopen/browser/commandsHandler.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { BoundedMap, ISerializedBoundedLinkedMap } from 'vs/base/common/map';
3333
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
3434
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
3535
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
36+
import { isPromiseCanceledError } from 'vs/base/common/errors';
3637

3738
export const ALL_COMMANDS_PREFIX = '>';
3839

@@ -263,9 +264,13 @@ abstract class BaseCommandEntry extends QuickOpenEntryGroup {
263264
return nls.localize('entryAriaLabel', "{0}, commands", this.getLabel());
264265
}
265266

266-
protected onError(error?: Error): void;
267-
protected onError(messagesWithAction?: IMessageWithAction): void;
268-
protected onError(arg1?: any): void {
267+
private onError(error?: Error): void;
268+
private onError(messagesWithAction?: IMessageWithAction): void;
269+
private onError(arg1?: any): void {
270+
if (isPromiseCanceledError(arg1)) {
271+
return;
272+
}
273+
269274
const messagesWithAction: IMessageWithAction = arg1;
270275
if (messagesWithAction && typeof messagesWithAction.message === 'string' && Array.isArray(messagesWithAction.actions)) {
271276
this.messageService.show(Severity.Error, messagesWithAction);

0 commit comments

Comments
 (0)