multi root - allow `--remove` for removal of workspace folders (fix #204770) (#236580)

pull/236584/head^2
Benjamin Pasero 2024-12-19 13:02:00 +01:00 committed by GitHub
parent 41a58be380
commit 2ba0803abf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 91 additions and 53 deletions

View File

@ -36,6 +36,7 @@ export interface NativeParsedArgs {
diff?: boolean;
merge?: boolean;
add?: boolean;
remove?: boolean;
goto?: boolean;
'new-window'?: boolean;
'reuse-window'?: boolean;

View File

@ -81,6 +81,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'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.") },

View File

@ -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
});

View File

@ -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,

View File

@ -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 {

View File

@ -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;

View File

@ -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<ICodeWindow[]> {
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;

View File

@ -337,6 +337,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise<vo
diffMode: parsedArgs.diff,
mergeMode: parsedArgs.merge,
addMode: parsedArgs.add,
removeMode: parsedArgs.remove,
gotoLineMode: parsedArgs.goto,
forceReuseWindow: parsedArgs['reuse-window'],
forceNewWindow: parsedArgs['new-window'],

View File

@ -20,6 +20,7 @@ export interface OpenCommandPipeArgs {
diffMode?: boolean;
mergeMode?: boolean;
addMode?: boolean;
removeMode?: boolean;
gotoLineMode?: boolean;
forceReuseWindow?: boolean;
waitMarkerFilePath?: string;
@ -119,7 +120,7 @@ export class CLIServerBase {
}
private async open(data: OpenCommandPipeArgs): Promise<undefined> {
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);
}

View File

@ -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<RemoteTunnel | string | undefined> {
const remoteAuthority = this.environmentService.remoteAuthority;
const addressProvider: IAddressProvider | undefined = remoteAuthority ? {
getAddress: async (): Promise<IAddress> => {
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<IResolvedExternalUri | undefined> {
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<RemoteTunnel | string | undefined> {
const remoteAuthority = this.environmentService.remoteAuthority;
const addressProvider: IAddressProvider | undefined = remoteAuthority ? {
getAddress: async (): Promise<IAddress> => {
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<void> {
const foldersToAdd: IWorkspaceFolderCreationData[] = this.pendingFoldersToAdd.map(folder => ({ uri: folder }));
const foldersToRemove = this.pendingFoldersToRemove.slice(0);
this.pendingFoldersToAdd = [];
this.pendingFoldersToRemove = [];
this.workspaceEditingService.addFolders(foldersToAdd);
if (foldersToAdd.length) {
await this.workspaceEditingService.addFolders(foldersToAdd);
}
if (foldersToRemove.length) {
await this.workspaceEditingService.removeFolders(foldersToRemove);
}
}
private async onOpenFiles(request: INativeOpenFileRequest): Promise<void> {

View File

@ -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<void> {
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);
}
});
}