diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 87825ee7d2c85..e0756ae894622 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -36,6 +36,7 @@ export interface NativeParsedArgs { diff?: boolean; merge?: boolean; add?: boolean; + remove?: boolean; goto?: boolean; 'new-window'?: boolean; 'reuse-window'?: boolean; diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 4ef107c79bcdf..606aa8b277fae 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -81,6 +81,7 @@ export const OPTIONS: OptionDescriptions> = { 'diff': { type: 'boolean', cat: 'o', alias: 'd', args: ['file', 'file'], description: localize('diff', "Compare two files with each other.") }, 'merge': { type: 'boolean', cat: 'o', alias: 'm', args: ['path1', 'path2', 'base', 'result'], description: localize('merge', "Perform a three-way merge by providing paths for two modified versions of a file, the common origin of both modified versions and the output file to save merge results.") }, 'add': { type: 'boolean', cat: 'o', alias: 'a', args: 'folder', description: localize('add', "Add folder(s) to the last active window.") }, + 'remove': { type: 'boolean', cat: 'o', args: 'folder', description: localize('remove', "Remove folder(s) from the last active window.") }, 'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") }, 'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") }, 'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") }, diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 36020f52cfcd4..df93721b40e31 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -207,6 +207,7 @@ export class LaunchMainService implements ILaunchMainService { diffMode: args.diff, mergeMode: args.merge, addMode: args.add, + removeMode: args.remove, noRecentEntry: !!args['skip-add-to-recently-opened'], gotoLineMode: args.goto }); diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index c3dbf1ee3ea78..794c5aed57532 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -220,6 +220,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain diffMode: options.diffMode, mergeMode: options.mergeMode, addMode: options.addMode, + removeMode: options.removeMode, gotoLineMode: options.gotoLineMode, noRecentEntry: options.noRecentEntry, waitMarkerFileURI: options.waitMarkerFileURI, diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index d547c37bf50e4..3a03481e55a50 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -63,6 +63,7 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { readonly noRecentEntry?: boolean; readonly addMode?: boolean; + readonly removeMode?: boolean; readonly diffMode?: boolean; readonly mergeMode?: boolean; @@ -71,8 +72,9 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { readonly waitMarkerFileURI?: URI; } -export interface IAddFoldersRequest { +export interface IAddRemoveFoldersRequest { readonly foldersToAdd: UriComponents[]; + readonly foldersToRemove: UriComponents[]; } interface IOpenedWindow { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index dd5148103bde4..6d14080654d4f 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -103,6 +103,7 @@ export interface IOpenConfiguration extends IBaseOpenConfiguration { readonly diffMode?: boolean; readonly mergeMode?: boolean; addMode?: boolean; + removeMode?: boolean; readonly gotoLineMode?: boolean; readonly initialStartup?: boolean; readonly noRecentEntry?: boolean; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index d403852efcc2a..8db5f04b7e3e9 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -37,7 +37,7 @@ import product from '../../product/common/product.js'; import { IProtocolMainService } from '../../protocol/electron-main/protocol.js'; import { getRemoteAuthority } from '../../remote/common/remoteHosts.js'; import { IStateService } from '../../state/node/state.js'; -import { IAddFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; +import { IAddRemoveFoldersRequest, INativeOpenFileRequest, INativeWindowConfiguration, IOpenEmptyWindowOptions, IPath, IPathsToWaitFor, isFileToOpen, isFolderToOpen, isWorkspaceToOpen, IWindowOpenable, IWindowSettings } from '../../window/common/window.js'; import { CodeWindow } from './windowImpl.js'; import { IOpenConfiguration, IOpenEmptyConfiguration, IWindowsCountChangedEvent, IWindowsMainService, OpenContext, getLastFocused } from './windows.js'; import { findWindowOnExtensionDevelopmentPath, findWindowOnFile, findWindowOnWorkspaceOrFolder } from './windowsFinder.js'; @@ -287,11 +287,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic async open(openConfig: IOpenConfiguration): Promise { this.logService.trace('windowsManager#open'); - if (openConfig.addMode && (openConfig.initialStartup || !this.getLastActiveWindow())) { - openConfig.addMode = false; // Make sure addMode is only enabled if we have an active window + // Make sure addMode/removeMode is only enabled if we have an active window + if ((openConfig.addMode || openConfig.removeMode) && (openConfig.initialStartup || !this.getLastActiveWindow())) { + openConfig.addMode = false; + openConfig.removeMode = false; } const foldersToAdd: ISingleFolderWorkspacePathToOpen[] = []; + const foldersToRemove: ISingleFolderWorkspacePathToOpen[] = []; + const foldersToOpen: ISingleFolderWorkspacePathToOpen[] = []; const workspacesToOpen: IWorkspacePathToOpen[] = []; @@ -311,6 +315,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // When run with --add, take the folders that are to be opened as // folders that should be added to the currently active window. foldersToAdd.push(path); + } else if (openConfig.removeMode) { + // When run with --remove, take the folders that are to be opened as + // folders that should be removed from the currently active window. + foldersToRemove.push(path); } else { foldersToOpen.push(path); } @@ -360,7 +368,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Open based on config - const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, openOneEmptyWindow, filesToOpen, foldersToAdd); + const { windows: usedWindows, filesOpenedInWindow } = await this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyWindowsWithBackupsToRestore, openOneEmptyWindow, filesToOpen, foldersToAdd, foldersToRemove); this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyWindowsWithBackupsToRestore.length}, openOneEmptyWindow: ${openOneEmptyWindow})`); @@ -463,7 +471,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic emptyToRestore: IEmptyWindowBackupInfo[], openOneEmptyWindow: boolean, filesToOpen: IFilesToOpen | undefined, - foldersToAdd: ISingleFolderWorkspacePathToOpen[] + foldersToAdd: ISingleFolderWorkspacePathToOpen[], + foldersToRemove: ISingleFolderWorkspacePathToOpen[] ): Promise<{ windows: ICodeWindow[]; filesOpenedInWindow: ICodeWindow | undefined }> { // Keep track of used windows and remember @@ -482,12 +491,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Settings can decide if files/folders open in new window or not let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig); - // Handle folders to add by looking for the last active workspace (not on initial startup) - if (!openConfig.initialStartup && foldersToAdd.length > 0) { - const authority = foldersToAdd[0].remoteAuthority; + // Handle folders to add/remove by looking for the last active workspace (not on initial startup) + if (!openConfig.initialStartup && (foldersToAdd.length > 0 || foldersToRemove.length > 0)) { + const authority = foldersToAdd.at(0)?.remoteAuthority ?? foldersToRemove.at(0)?.remoteAuthority; const lastActiveWindow = this.getLastActiveWindowForAuthority(authority); if (lastActiveWindow) { - addUsedWindow(this.doAddFoldersToExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri))); + addUsedWindow(this.doAddRemoveFoldersInExistingWindow(lastActiveWindow, foldersToAdd.map(folderToAdd => folderToAdd.workspace.uri), foldersToRemove.map(folderToRemove => folderToRemove.workspace.uri))); } } @@ -671,13 +680,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic windowToFocus.focus(); } - private doAddFoldersToExistingWindow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { - this.logService.trace('windowsManager#doAddFoldersToExistingWindow', { foldersToAdd }); + private doAddRemoveFoldersInExistingWindow(window: ICodeWindow, foldersToAdd: URI[], foldersToRemove: URI[]): ICodeWindow { + this.logService.trace('windowsManager#doAddRemoveFoldersToExistingWindow', { foldersToAdd, foldersToRemove }); window.focus(); // make sure window has focus - const request: IAddFoldersRequest = { foldersToAdd }; - window.sendWhenReady('vscode:addFolders', CancellationToken.None, request); + const request: IAddRemoveFoldersRequest = { foldersToAdd, foldersToRemove }; + window.sendWhenReady('vscode:addRemoveFolders', CancellationToken.None, request); return window; } @@ -764,10 +773,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Handle the case of multiple folders being opened from CLI while we are - // not in `--add` mode by creating an untitled workspace, only if: + // not in `--add` or `--remove` mode by creating an untitled workspace, only if: // - they all share the same remote authority // - there is no existing workspace to open that matches these folders - if (!openConfig.addMode && isCommandLineOrAPICall) { + if (!openConfig.addMode && !openConfig.removeMode && isCommandLineOrAPICall) { const foldersToOpen = pathsToOpen.filter(path => isSingleFolderWorkspacePathToOpen(path)); if (foldersToOpen.length > 1) { const remoteAuthority = foldersToOpen[0].remoteAuthority; diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 7f20588c3bd48..0535ddd998f70 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -337,6 +337,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise { - const { fileURIs, folderURIs, forceNewWindow, diffMode, mergeMode, addMode, forceReuseWindow, gotoLineMode, waitMarkerFilePath, remoteAuthority } = data; + const { fileURIs, folderURIs, forceNewWindow, diffMode, mergeMode, addMode, removeMode, forceReuseWindow, gotoLineMode, waitMarkerFilePath, remoteAuthority } = data; const urisToOpen: IWindowOpenable[] = []; if (Array.isArray(folderURIs)) { for (const s of folderURIs) { @@ -144,8 +145,8 @@ export class CLIServerBase { } } const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined; - const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode; - const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, mergeMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI, remoteAuthority }; + const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode && !removeMode; + const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, mergeMode, addMode, removeMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI, remoteAuthority }; this._commands.executeCommand('_remoteCLI.windowOpen', urisToOpen, windowOpenArgs); } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index c186b45d2ba8f..4babd90fe708c 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -14,7 +14,7 @@ import { IFileService } from '../../platform/files/common/files.js'; import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane, isResourceEditorInput, IResourceMergeEditorInput } from '../common/editor.js'; import { IEditorService } from '../services/editor/common/editorService.js'; import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js'; -import { WindowMinimumSize, IOpenFileRequest, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from '../../platform/window/common/window.js'; +import { WindowMinimumSize, IOpenFileRequest, IAddRemoveFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest, hasNativeTitlebar } from '../../platform/window/common/window.js'; import { ITitleService } from '../services/title/browser/titleService.js'; import { IWorkbenchThemeService } from '../services/themes/common/workbenchThemeService.js'; import { ApplyZoomTarget, applyZoom } from '../../platform/window/electron-sandbox/window.js'; @@ -84,8 +84,9 @@ export class NativeWindow extends BaseWindow { private readonly customTitleContextMenuDisposable = this._register(new DisposableStore()); - private readonly addFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddFolders(), 100)); + private readonly addRemoveFoldersScheduler = this._register(new RunOnceScheduler(() => this.doAddRemoveFolders(), 100)); private pendingFoldersToAdd: URI[] = []; + private pendingFoldersToRemove: URI[] = []; private isDocumentedEdited = false; @@ -209,11 +210,11 @@ export class NativeWindow extends BaseWindow { // Support openFiles event for existing and new files ipcRenderer.on('vscode:openFiles', (event: unknown, request: IOpenFileRequest) => { this.onOpenFiles(request); }); - // Support addFolders event if we have a workspace opened - ipcRenderer.on('vscode:addFolders', (event: unknown, request: IAddFoldersRequest) => { this.onAddFoldersRequest(request); }); + // Support addRemoveFolders event for workspace management + ipcRenderer.on('vscode:addRemoveFolders', (event: unknown, request: IAddRemoveFoldersRequest) => this.onAddRemoveFoldersRequest(request)); // Message support - ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => { this.notificationService.info(message); }); + ipcRenderer.on('vscode:showInfoMessage', (event: unknown, message: string) => this.notificationService.info(message)); // Shell Environment Issue Notifications ipcRenderer.on('vscode:showResolveShellEnvError', (event: unknown, message: string) => { @@ -788,20 +789,6 @@ export class NativeWindow extends BaseWindow { }); } - private async openTunnel(address: string, port: number): Promise { - const remoteAuthority = this.environmentService.remoteAuthority; - const addressProvider: IAddressProvider | undefined = remoteAuthority ? { - getAddress: async (): Promise => { - return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; - } - } : undefined; - const tunnel = await this.tunnelService.getExistingTunnel(address, port); - if (!tunnel || (typeof tunnel === 'string')) { - return this.tunnelService.openTunnel(addressProvider, address, port); - } - return tunnel; - } - async resolveExternalUri(uri: URI, options?: OpenOptions): Promise { let queryTunnel: RemoteTunnel | string | undefined; if (options?.allowTunneling) { @@ -826,6 +813,7 @@ export class NativeWindow extends BaseWindow { } } } + if (portMappingRequest) { const tunnel = await this.openTunnel(portMappingRequest.address, portMappingRequest.port); if (tunnel && (typeof tunnel !== 'string')) { @@ -861,6 +849,22 @@ export class NativeWindow extends BaseWindow { return undefined; } + private async openTunnel(address: string, port: number): Promise { + const remoteAuthority = this.environmentService.remoteAuthority; + const addressProvider: IAddressProvider | undefined = remoteAuthority ? { + getAddress: async (): Promise => { + return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; + } + } : undefined; + + const tunnel = await this.tunnelService.getExistingTunnel(address, port); + if (!tunnel || (typeof tunnel === 'string')) { + return this.tunnelService.openTunnel(addressProvider, address, port); + } + + return tunnel; + } + private setupOpenHandlers(): void { // Handle external open() calls @@ -961,27 +965,32 @@ export class NativeWindow extends BaseWindow { //#endregion - private onAddFoldersRequest(request: IAddFoldersRequest): void { + private onAddRemoveFoldersRequest(request: IAddRemoveFoldersRequest): void { // Buffer all pending requests this.pendingFoldersToAdd.push(...request.foldersToAdd.map(folder => URI.revive(folder))); + this.pendingFoldersToRemove.push(...request.foldersToRemove.map(folder => URI.revive(folder))); // Delay the adding of folders a bit to buffer in case more requests are coming - if (!this.addFoldersScheduler.isScheduled()) { - this.addFoldersScheduler.schedule(); + if (!this.addRemoveFoldersScheduler.isScheduled()) { + this.addRemoveFoldersScheduler.schedule(); } } - private doAddFolders(): void { - const foldersToAdd: IWorkspaceFolderCreationData[] = []; - - for (const folder of this.pendingFoldersToAdd) { - foldersToAdd.push(({ uri: folder })); - } + private async doAddRemoveFolders(): Promise { + const foldersToAdd: IWorkspaceFolderCreationData[] = this.pendingFoldersToAdd.map(folder => ({ uri: folder })); + const foldersToRemove = this.pendingFoldersToRemove.slice(0); this.pendingFoldersToAdd = []; + this.pendingFoldersToRemove = []; + + if (foldersToAdd.length) { + await this.workspaceEditingService.addFolders(foldersToAdd); + } - this.workspaceEditingService.addFolders(foldersToAdd); + if (foldersToRemove.length) { + await this.workspaceEditingService.removeFolders(foldersToRemove); + } } private async onOpenFiles(request: INativeOpenFileRequest): Promise { diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index afd2a3d126737..57cb6e482cfc1 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -40,6 +40,7 @@ import { coalesce } from '../../../../base/common/arrays.js'; import { mainWindow, isAuxiliaryWindow } from '../../../../base/browser/window.js'; import { isIOS, isMacintosh } from '../../../../base/common/platform.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; +import { URI } from '../../../../base/common/uri.js'; enum HostShutdownReason { @@ -238,7 +239,9 @@ export class BrowserHostService extends Disposable implements IHostService { private async doOpenWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise { const payload = this.preservePayload(false /* not an empty window */, options); const fileOpenables: IFileToOpen[] = []; + const foldersToAdd: IWorkspaceFolderCreationData[] = []; + const foldersToRemove: URI[] = []; for (const openable of toOpen) { openable.label = openable.label || this.getRecentLabel(openable); @@ -246,7 +249,9 @@ export class BrowserHostService extends Disposable implements IHostService { // Folder if (isFolderToOpen(openable)) { if (options?.addMode) { - foldersToAdd.push(({ uri: openable.folderUri })); + foldersToAdd.push({ uri: openable.folderUri }); + } else if (options?.removeMode) { + foldersToRemove.push(openable.folderUri); } else { this.doOpen({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload }); } @@ -263,11 +268,17 @@ export class BrowserHostService extends Disposable implements IHostService { } } - // Handle Folders to Add - if (foldersToAdd.length > 0) { - this.withServices(accessor => { + // Handle Folders to add or remove + if (foldersToAdd.length > 0 || foldersToRemove.length > 0) { + this.withServices(async accessor => { const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService); - workspaceEditingService.addFolders(foldersToAdd); + if (foldersToAdd.length > 0) { + await workspaceEditingService.addFolders(foldersToAdd); + } + + if (foldersToRemove.length > 0) { + await workspaceEditingService.removeFolders(foldersToRemove); + } }); }