history - indicate if a recently opened folder/workspace is opened as window (#276351)

This commit is contained in:
Benjamin Pasero
2025-11-09 15:23:05 +01:00
committed by GitHub
parent bb450f4e41
commit 01eecb44db
6 changed files with 101 additions and 17 deletions
@@ -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);
}
@@ -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<void> {
@@ -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<boolean>();
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<boolean>();
const recentWorkspaces = new ResourceMap<IWorkspaceIdentifier>();
@@ -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
@@ -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<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;
getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;
async getWindows(options: { includeAuxiliaryWindows: boolean }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>> {
const activeWindow = getActiveWindow();
const activeWindowId = getWindowId(activeWindow);
// Main window
const result: Array<IOpenedMainWindow | IOpenedAuxiliaryWindow> = [{
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
@@ -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<IHostService>('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<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;
getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;
//#endregion
//#region Lifecycle
@@ -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<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;
getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;
getWindows(options: { includeAuxiliaryWindows: boolean }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>> {
if (options.includeAuxiliaryWindows === false) {
return this.nativeHostService.getWindows({ includeAuxiliaryWindows: false });
}
return this.nativeHostService.getWindows({ includeAuxiliaryWindows: true });
}
//#endregion
//#region Lifecycle
@@ -1431,6 +1431,8 @@ export class TestHostService implements IHostService {
async moveTop(): Promise<void> { }
async getCursorScreenPoint(): Promise<undefined> { return undefined; }
async getWindows(options: unknown) { return []; }
async openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> { }
async toggleFullScreen(): Promise<void> { }