mirror of
https://github.com/microsoft/vscode.git
synced 2026-06-03 22:25:48 +01:00
history - indicate if a recently opened folder/workspace is opened as window (#276351)
This commit is contained in:
@@ -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> { }
|
||||
|
||||
Reference in New Issue
Block a user