From b2f5137f6538aa1590ed82f5ae89d63758f4a641 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 9 Nov 2025 11:05:24 +0100 Subject: [PATCH] history - indicate if a recently opened folder/workspace is opened as window --- .../browser/actions/media/actions.css | 5 +- .../browser/actions/windowActions.ts | 54 +++++++++++++++---- .../host/browser/browserHostService.ts | 37 +++++++++++-- .../workbench/services/host/browser/host.ts | 8 ++- .../electron-browser/nativeHostService.ts | 12 ++++- .../test/browser/workbenchTestServices.ts | 2 + 6 files changed, 101 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/browser/actions/media/actions.css b/src/vs/workbench/browser/actions/media/actions.css index 3fe27d61027b0..7c6dce32a5947 100644 --- a/src/vs/workbench/browser/actions/media/actions.css +++ b/src/vs/workbench/browser/actions/media/actions.css @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before { - /* Close icon flips between black dot and "X" for dirty workspaces */ +.monaco-workbench .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before, +.monaco-workbench .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.opened-workspace::before { + /* Close icon flips between black dot and "X" some entries in the recently opened picker */ content: var(--vscode-icon-x-content); font-family: var(--vscode-icon-x-font-family); } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 808d5e717ca48..bb978cbbff6b8 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -13,7 +13,7 @@ import { IsMacNativeContext, IsDevelopmentContext, IsWebContext, IsIOSContext } import { Categories } from '../../../platform/action/common/actionCommonCategories.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js'; -import { IWorkspaceContextService, IWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js'; +import { IWorkspaceContextService, IWorkspaceIdentifier, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js'; import { ILabelService, Verbosity } from '../../../platform/label/common/label.js'; import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; import { IModelService } from '../../../editor/common/services/model.js'; @@ -62,6 +62,17 @@ abstract class BaseOpenRecentAction extends Action2 { tooltip: localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"), }; + private readonly windowOpenedRecentlyOpenedFolder: IQuickInputButton = { + iconClass: 'opened-workspace ' + ThemeIcon.asClassName(Codicon.window), + tooltip: localize('openedRecentlyOpenedFolder', "Folder Opened in a Window"), + alwaysVisible: true + }; + + private readonly windowOpenedRecentlyOpenedWorkspace: IQuickInputButton = { + ...this.windowOpenedRecentlyOpenedFolder, + tooltip: localize('openedRecentlyOpenedWorkspace', "Workspace Opened in a Window"), + }; + protected abstract isQuickNavigate(): boolean; override async run(accessor: ServicesAccessor): Promise { @@ -75,8 +86,11 @@ abstract class BaseOpenRecentAction extends Action2 { const hostService = accessor.get(IHostService); const dialogService = accessor.get(IDialogService); - const recentlyOpened = await workspacesService.getRecentlyOpened(); - const dirtyWorkspacesAndFolders = await workspacesService.getDirtyWorkspaces(); + const [mainWindows, recentlyOpened, dirtyWorkspacesAndFolders] = await Promise.all([ + hostService.getWindows({ includeAuxiliaryWindows: false }), + workspacesService.getRecentlyOpened(), + workspacesService.getDirtyWorkspaces() + ]); let hasWorkspaces = false; @@ -92,6 +106,16 @@ abstract class BaseOpenRecentAction extends Action2 { } } + // Identify all folders and workspaces opened in main windows + const openedInWindows = new ResourceMap(); + for (const window of mainWindows) { + if (isSingleFolderWorkspaceIdentifier(window.workspace)) { + openedInWindows.set(window.workspace.uri, true); + } else if (isWorkspaceIdentifier(window.workspace)) { + openedInWindows.set(window.workspace.configPath, true); + } + } + // Identify all recently opened folders and workspaces const recentFolders = new ResourceMap(); const recentWorkspaces = new ResourceMap(); @@ -108,20 +132,21 @@ abstract class BaseOpenRecentAction extends Action2 { const workspacePicks: IRecentlyOpenedPick[] = []; for (const recent of recentlyOpened.workspaces) { const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath); + const isOpenedInWindow = isRecentFolder(recent) ? openedInWindows.has(recent.folderUri) : openedInWindows.has(recent.workspace.configPath); - workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, recent, isDirty)); + workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, recent, { isDirty, isOpenedInWindow })); } // Fill any backup workspace that is not yet shown at the end for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) { if (isFolderBackupInfo(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder.folderUri)) { - workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true)); + workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, { isDirty: true, isOpenedInWindow: false })); } else if (isWorkspaceBackupInfo(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.workspace.configPath)) { - workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true)); + workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, { isDirty: true, isOpenedInWindow: false })); } } - const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, languageService, labelService, p, false)); + const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, languageService, labelService, p, { isDirty: false, isOpenedInWindow: false })); // focus second entry if the first recent workspace is the current workspace const firstEntry = recentlyOpened.workspaces[0]; @@ -179,7 +204,7 @@ abstract class BaseOpenRecentAction extends Action2 { } } - private toQuickPick(modelService: IModelService, languageService: ILanguageService, labelService: ILabelService, recent: IRecent, isDirty: boolean): IRecentlyOpenedPick { + private toQuickPick(modelService: IModelService, languageService: ILanguageService, labelService: ILabelService, recent: IRecent, kind: { isDirty: boolean; isOpenedInWindow: boolean }): IRecentlyOpenedPick { let openable: IWindowOpenable | undefined; let iconClasses: string[]; let fullLabel: string | undefined; @@ -213,12 +238,21 @@ abstract class BaseOpenRecentAction extends Action2 { const { name, parentPath } = splitRecentLabel(fullLabel); + const buttons: IQuickInputButton[] = []; + if (kind.isDirty) { + buttons.push(isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder); + } else if (kind.isOpenedInWindow) { + buttons.push(isWorkspace ? this.windowOpenedRecentlyOpenedWorkspace : this.windowOpenedRecentlyOpenedFolder); + } else { + buttons.push(this.removeFromRecentlyOpened); + } + return { iconClasses, label: name, - ariaLabel: isDirty ? isWorkspace ? localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name, + ariaLabel: kind.isDirty ? isWorkspace ? localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name, description: parentPath, - buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened], + buttons, openable, resource, remoteAuthority: recent.remoteAuthority diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 02e6d4591aa21..974646d0f95a6 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -9,13 +9,13 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { IEditorService } from '../../editor/common/editorService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen } from '../../../../platform/window/common/window.js'; +import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen, IOpenedMainWindow, IOpenedAuxiliaryWindow } from '../../../../platform/window/common/window.js'; import { isResourceEditorInput, pathsToEditors } from '../../../common/editor.js'; import { whenEditorClosed } from '../../../browser/editor.js'; import { IWorkspace, IWorkspaceProvider } from '../../../browser/web.api.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; -import { EventType, ModifierKeyEmitter, addDisposableListener, addDisposableThrottledListener, detectFullscreen, disposableWindowInterval, getActiveDocument, getWindowId, onDidRegisterWindow, trackFocus } from '../../../../base/browser/dom.js'; +import { EventType, ModifierKeyEmitter, addDisposableListener, addDisposableThrottledListener, detectFullscreen, disposableWindowInterval, getActiveDocument, getActiveWindow, getWindowId, onDidRegisterWindow, trackFocus, getWindows as getDOMWindows } from '../../../../base/browser/dom.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js'; import { memoize } from '../../../../base/common/decorators.js'; @@ -32,7 +32,7 @@ import Severity from '../../../../base/common/severity.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { DomEmitter } from '../../../../base/browser/event.js'; import { isUndefined } from '../../../../base/common/types.js'; -import { isTemporaryWorkspace, IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { isTemporaryWorkspace, IWorkspaceContextService, toWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { Schemas } from '../../../../base/common/network.js'; import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js'; @@ -572,6 +572,37 @@ export class BrowserHostService extends Disposable implements IHostService { return undefined; } + getWindows(options: { includeAuxiliaryWindows: true }): Promise>; + getWindows(options: { includeAuxiliaryWindows: false }): Promise>; + async getWindows(options: { includeAuxiliaryWindows: boolean }): Promise> { + const activeWindow = getActiveWindow(); + const activeWindowId = getWindowId(activeWindow); + + // Main window + const result: Array = [{ + id: activeWindowId, + title: activeWindow.document.title, + workspace: toWorkspaceIdentifier(this.contextService.getWorkspace()), + dirty: false + }]; + + // Auxiliary windows + if (options.includeAuxiliaryWindows) { + for (const { window } of getDOMWindows()) { + const windowId = getWindowId(window); + if (windowId !== activeWindowId && isAuxiliaryWindow(window)) { + result.push({ + id: windowId, + title: window.document.title, + parentId: activeWindowId + }); + } + } + } + + return result; + } + //#endregion //#region Lifecycle diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index f83c4b79e849d..4ac35c9240c21 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -7,7 +7,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { Event } from '../../../../base/common/event.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { FocusMode } from '../../../../platform/native/common/native.js'; -import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js'; +import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle, IOpenedMainWindow, IOpenedAuxiliaryWindow } from '../../../../platform/window/common/window.js'; export const IHostService = createDecorator('hostService'); @@ -93,6 +93,12 @@ export interface IHostService { */ getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle } | undefined>; + /** + * Get the list of opened windows, optionally including auxiliary windows. + */ + getWindows(options: { includeAuxiliaryWindows: true }): Promise>; + getWindows(options: { includeAuxiliaryWindows: false }): Promise>; + //#endregion //#region Lifecycle diff --git a/src/vs/workbench/services/host/electron-browser/nativeHostService.ts b/src/vs/workbench/services/host/electron-browser/nativeHostService.ts index e0e7d669aa2ef..9ca38b242864b 100644 --- a/src/vs/workbench/services/host/electron-browser/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-browser/nativeHostService.ts @@ -9,7 +9,7 @@ import { FocusMode, INativeHostService } from '../../../../platform/native/commo import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; -import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js'; +import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions, IPoint, IRectangle, IOpenedAuxiliaryWindow, IOpenedMainWindow } from '../../../../platform/window/common/window.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { NativeHostService } from '../../../../platform/native/common/nativeHostService.js'; import { INativeWorkbenchEnvironmentService } from '../../environment/electron-browser/environmentService.js'; @@ -162,6 +162,16 @@ class WorkbenchHostService extends Disposable implements IHostService { return this.nativeHostService.getCursorScreenPoint(); } + getWindows(options: { includeAuxiliaryWindows: true }): Promise>; + getWindows(options: { includeAuxiliaryWindows: false }): Promise>; + getWindows(options: { includeAuxiliaryWindows: boolean }): Promise> { + if (options.includeAuxiliaryWindows === false) { + return this.nativeHostService.getWindows({ includeAuxiliaryWindows: false }); + } + + return this.nativeHostService.getWindows({ includeAuxiliaryWindows: true }); + } + //#endregion //#region Lifecycle diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index bcf4471762a4b..b2cc3ecc7efe4 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1431,6 +1431,8 @@ export class TestHostService implements IHostService { async moveTop(): Promise { } async getCursorScreenPoint(): Promise { return undefined; } + async getWindows(options: unknown) { return []; } + async openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { } async toggleFullScreen(): Promise { }