Skip to content

Commit 01eecb4

Browse files
authored
history - indicate if a recently opened folder/workspace is opened as window (#276351)
1 parent bb450f4 commit 01eecb4

File tree

6 files changed

+101
-17
lines changed

6 files changed

+101
-17
lines changed

src/vs/workbench/browser/actions/media/actions.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
.monaco-workbench .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before {
7-
/* Close icon flips between black dot and "X" for dirty workspaces */
6+
.monaco-workbench .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before,
7+
.monaco-workbench .quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.opened-workspace::before {
8+
/* Close icon flips between black dot and "X" some entries in the recently opened picker */
89
content: var(--vscode-icon-x-content);
910
font-family: var(--vscode-icon-x-font-family);
1011
}

src/vs/workbench/browser/actions/windowActions.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { IsMacNativeContext, IsDevelopmentContext, IsWebContext, IsIOSContext }
1313
import { Categories } from '../../../platform/action/common/actionCommonCategories.js';
1414
import { KeybindingsRegistry, KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js';
1515
import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';
16-
import { IWorkspaceContextService, IWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js';
16+
import { IWorkspaceContextService, IWorkspaceIdentifier, isWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier } from '../../../platform/workspace/common/workspace.js';
1717
import { ILabelService, Verbosity } from '../../../platform/label/common/label.js';
1818
import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';
1919
import { IModelService } from '../../../editor/common/services/model.js';
@@ -62,6 +62,17 @@ abstract class BaseOpenRecentAction extends Action2 {
6262
tooltip: localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"),
6363
};
6464

65+
private readonly windowOpenedRecentlyOpenedFolder: IQuickInputButton = {
66+
iconClass: 'opened-workspace ' + ThemeIcon.asClassName(Codicon.window),
67+
tooltip: localize('openedRecentlyOpenedFolder', "Folder Opened in a Window"),
68+
alwaysVisible: true
69+
};
70+
71+
private readonly windowOpenedRecentlyOpenedWorkspace: IQuickInputButton = {
72+
...this.windowOpenedRecentlyOpenedFolder,
73+
tooltip: localize('openedRecentlyOpenedWorkspace', "Workspace Opened in a Window"),
74+
};
75+
6576
protected abstract isQuickNavigate(): boolean;
6677

6778
override async run(accessor: ServicesAccessor): Promise<void> {
@@ -75,8 +86,11 @@ abstract class BaseOpenRecentAction extends Action2 {
7586
const hostService = accessor.get(IHostService);
7687
const dialogService = accessor.get(IDialogService);
7788

78-
const recentlyOpened = await workspacesService.getRecentlyOpened();
79-
const dirtyWorkspacesAndFolders = await workspacesService.getDirtyWorkspaces();
89+
const [mainWindows, recentlyOpened, dirtyWorkspacesAndFolders] = await Promise.all([
90+
hostService.getWindows({ includeAuxiliaryWindows: false }),
91+
workspacesService.getRecentlyOpened(),
92+
workspacesService.getDirtyWorkspaces()
93+
]);
8094

8195
let hasWorkspaces = false;
8296

@@ -92,6 +106,16 @@ abstract class BaseOpenRecentAction extends Action2 {
92106
}
93107
}
94108

109+
// Identify all folders and workspaces opened in main windows
110+
const openedInWindows = new ResourceMap<boolean>();
111+
for (const window of mainWindows) {
112+
if (isSingleFolderWorkspaceIdentifier(window.workspace)) {
113+
openedInWindows.set(window.workspace.uri, true);
114+
} else if (isWorkspaceIdentifier(window.workspace)) {
115+
openedInWindows.set(window.workspace.configPath, true);
116+
}
117+
}
118+
95119
// Identify all recently opened folders and workspaces
96120
const recentFolders = new ResourceMap<boolean>();
97121
const recentWorkspaces = new ResourceMap<IWorkspaceIdentifier>();
@@ -108,20 +132,21 @@ abstract class BaseOpenRecentAction extends Action2 {
108132
const workspacePicks: IRecentlyOpenedPick[] = [];
109133
for (const recent of recentlyOpened.workspaces) {
110134
const isDirty = isRecentFolder(recent) ? dirtyFolders.has(recent.folderUri) : dirtyWorkspaces.has(recent.workspace.configPath);
135+
const isOpenedInWindow = isRecentFolder(recent) ? openedInWindows.has(recent.folderUri) : openedInWindows.has(recent.workspace.configPath);
111136

112-
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, recent, isDirty));
137+
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, recent, { isDirty, isOpenedInWindow }));
113138
}
114139

115140
// Fill any backup workspace that is not yet shown at the end
116141
for (const dirtyWorkspaceOrFolder of dirtyWorkspacesAndFolders) {
117142
if (isFolderBackupInfo(dirtyWorkspaceOrFolder) && !recentFolders.has(dirtyWorkspaceOrFolder.folderUri)) {
118-
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true));
143+
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, { isDirty: true, isOpenedInWindow: false }));
119144
} else if (isWorkspaceBackupInfo(dirtyWorkspaceOrFolder) && !recentWorkspaces.has(dirtyWorkspaceOrFolder.workspace.configPath)) {
120-
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, true));
145+
workspacePicks.push(this.toQuickPick(modelService, languageService, labelService, dirtyWorkspaceOrFolder, { isDirty: true, isOpenedInWindow: false }));
121146
}
122147
}
123148

124-
const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, languageService, labelService, p, false));
149+
const filePicks = recentlyOpened.files.map(p => this.toQuickPick(modelService, languageService, labelService, p, { isDirty: false, isOpenedInWindow: false }));
125150

126151
// focus second entry if the first recent workspace is the current workspace
127152
const firstEntry = recentlyOpened.workspaces[0];
@@ -179,7 +204,7 @@ abstract class BaseOpenRecentAction extends Action2 {
179204
}
180205
}
181206

182-
private toQuickPick(modelService: IModelService, languageService: ILanguageService, labelService: ILabelService, recent: IRecent, isDirty: boolean): IRecentlyOpenedPick {
207+
private toQuickPick(modelService: IModelService, languageService: ILanguageService, labelService: ILabelService, recent: IRecent, kind: { isDirty: boolean; isOpenedInWindow: boolean }): IRecentlyOpenedPick {
183208
let openable: IWindowOpenable | undefined;
184209
let iconClasses: string[];
185210
let fullLabel: string | undefined;
@@ -213,12 +238,21 @@ abstract class BaseOpenRecentAction extends Action2 {
213238

214239
const { name, parentPath } = splitRecentLabel(fullLabel);
215240

241+
const buttons: IQuickInputButton[] = [];
242+
if (kind.isDirty) {
243+
buttons.push(isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder);
244+
} else if (kind.isOpenedInWindow) {
245+
buttons.push(isWorkspace ? this.windowOpenedRecentlyOpenedWorkspace : this.windowOpenedRecentlyOpenedFolder);
246+
} else {
247+
buttons.push(this.removeFromRecentlyOpened);
248+
}
249+
216250
return {
217251
iconClasses,
218252
label: name,
219-
ariaLabel: isDirty ? isWorkspace ? localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name,
253+
ariaLabel: kind.isDirty ? isWorkspace ? localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name,
220254
description: parentPath,
221-
buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened],
255+
buttons,
222256
openable,
223257
resource,
224258
remoteAuthority: recent.remoteAuthority

src/vs/workbench/services/host/browser/browserHostService.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta
99
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
1010
import { IEditorService } from '../../editor/common/editorService.js';
1111
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
12-
import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen } from '../../../../platform/window/common/window.js';
12+
import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen, IOpenedMainWindow, IOpenedAuxiliaryWindow } from '../../../../platform/window/common/window.js';
1313
import { isResourceEditorInput, pathsToEditors } from '../../../common/editor.js';
1414
import { whenEditorClosed } from '../../../browser/editor.js';
1515
import { IWorkspace, IWorkspaceProvider } from '../../../browser/web.api.js';
1616
import { IFileService } from '../../../../platform/files/common/files.js';
1717
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
18-
import { EventType, ModifierKeyEmitter, addDisposableListener, addDisposableThrottledListener, detectFullscreen, disposableWindowInterval, getActiveDocument, getWindowId, onDidRegisterWindow, trackFocus } from '../../../../base/browser/dom.js';
18+
import { EventType, ModifierKeyEmitter, addDisposableListener, addDisposableThrottledListener, detectFullscreen, disposableWindowInterval, getActiveDocument, getActiveWindow, getWindowId, onDidRegisterWindow, trackFocus, getWindows as getDOMWindows } from '../../../../base/browser/dom.js';
1919
import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
2020
import { IBrowserWorkbenchEnvironmentService } from '../../environment/browser/environmentService.js';
2121
import { memoize } from '../../../../base/common/decorators.js';
@@ -32,7 +32,7 @@ import Severity from '../../../../base/common/severity.js';
3232
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
3333
import { DomEmitter } from '../../../../base/browser/event.js';
3434
import { isUndefined } from '../../../../base/common/types.js';
35-
import { isTemporaryWorkspace, IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
35+
import { isTemporaryWorkspace, IWorkspaceContextService, toWorkspaceIdentifier } from '../../../../platform/workspace/common/workspace.js';
3636
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
3737
import { Schemas } from '../../../../base/common/network.js';
3838
import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js';
@@ -572,6 +572,37 @@ export class BrowserHostService extends Disposable implements IHostService {
572572
return undefined;
573573
}
574574

575+
getWindows(options: { includeAuxiliaryWindows: true }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;
576+
getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;
577+
async getWindows(options: { includeAuxiliaryWindows: boolean }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>> {
578+
const activeWindow = getActiveWindow();
579+
const activeWindowId = getWindowId(activeWindow);
580+
581+
// Main window
582+
const result: Array<IOpenedMainWindow | IOpenedAuxiliaryWindow> = [{
583+
id: activeWindowId,
584+
title: activeWindow.document.title,
585+
workspace: toWorkspaceIdentifier(this.contextService.getWorkspace()),
586+
dirty: false
587+
}];
588+
589+
// Auxiliary windows
590+
if (options.includeAuxiliaryWindows) {
591+
for (const { window } of getDOMWindows()) {
592+
const windowId = getWindowId(window);
593+
if (windowId !== activeWindowId && isAuxiliaryWindow(window)) {
594+
result.push({
595+
id: windowId,
596+
title: window.document.title,
597+
parentId: activeWindowId
598+
});
599+
}
600+
}
601+
}
602+
603+
return result;
604+
}
605+
575606
//#endregion
576607

577608
//#region Lifecycle

src/vs/workbench/services/host/browser/host.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js';
77
import { Event } from '../../../../base/common/event.js';
88
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
99
import { FocusMode } from '../../../../platform/native/common/native.js';
10-
import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js';
10+
import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle, IOpenedMainWindow, IOpenedAuxiliaryWindow } from '../../../../platform/window/common/window.js';
1111

1212
export const IHostService = createDecorator<IHostService>('hostService');
1313

@@ -93,6 +93,12 @@ export interface IHostService {
9393
*/
9494
getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle } | undefined>;
9595

96+
/**
97+
* Get the list of opened windows, optionally including auxiliary windows.
98+
*/
99+
getWindows(options: { includeAuxiliaryWindows: true }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;
100+
getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;
101+
96102
//#endregion
97103

98104
//#region Lifecycle

src/vs/workbench/services/host/electron-browser/nativeHostService.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { FocusMode, INativeHostService } from '../../../../platform/native/commo
99
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
1010
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
1111
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
12-
import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions, IPoint, IRectangle } from '../../../../platform/window/common/window.js';
12+
import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions, IPoint, IRectangle, IOpenedAuxiliaryWindow, IOpenedMainWindow } from '../../../../platform/window/common/window.js';
1313
import { Disposable } from '../../../../base/common/lifecycle.js';
1414
import { NativeHostService } from '../../../../platform/native/common/nativeHostService.js';
1515
import { INativeWorkbenchEnvironmentService } from '../../environment/electron-browser/environmentService.js';
@@ -162,6 +162,16 @@ class WorkbenchHostService extends Disposable implements IHostService {
162162
return this.nativeHostService.getCursorScreenPoint();
163163
}
164164

165+
getWindows(options: { includeAuxiliaryWindows: true }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>>;
166+
getWindows(options: { includeAuxiliaryWindows: false }): Promise<Array<IOpenedMainWindow>>;
167+
getWindows(options: { includeAuxiliaryWindows: boolean }): Promise<Array<IOpenedMainWindow | IOpenedAuxiliaryWindow>> {
168+
if (options.includeAuxiliaryWindows === false) {
169+
return this.nativeHostService.getWindows({ includeAuxiliaryWindows: false });
170+
}
171+
172+
return this.nativeHostService.getWindows({ includeAuxiliaryWindows: true });
173+
}
174+
165175
//#endregion
166176

167177
//#region Lifecycle

src/vs/workbench/test/browser/workbenchTestServices.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,8 @@ export class TestHostService implements IHostService {
14311431
async moveTop(): Promise<void> { }
14321432
async getCursorScreenPoint(): Promise<undefined> { return undefined; }
14331433

1434+
async getWindows(options: unknown) { return []; }
1435+
14341436
async openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise<void> { }
14351437

14361438
async toggleFullScreen(): Promise<void> { }

0 commit comments

Comments
 (0)