Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/vs/workbench/browser/actions/media/actions.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
54 changes: 44 additions & 10 deletions src/vs/workbench/browser/actions/windowActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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> {
Expand All @@ -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;

Expand All @@ -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>();
Expand All @@ -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];
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
37 changes: 34 additions & 3 deletions src/vs/workbench/services/host/browser/browserHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/vs/workbench/services/host/browser/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/test/browser/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> { }
Expand Down
Loading