From 609e1c5ad9aa737ff68fb11dc6bd5e0ec659e9e9 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 28 Oct 2023 06:45:26 +0200 Subject: [PATCH 01/13] aux window - fix focus issues --- src/vs/base/browser/dom.ts | 19 +++++++---- src/vs/workbench/browser/layout.ts | 27 +++++++++++---- .../browser/parts/editor/editorGroupView.ts | 27 +++++++-------- .../electron-sandbox/actions/windowActions.ts | 2 +- .../browser/auxiliaryWindowService.ts | 34 +++++++++++++++---- .../auxiliaryWindowService.ts | 32 +++++------------ .../electron-sandbox/nativeHostService.ts | 2 +- 7 files changed, 84 insertions(+), 59 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index aaf950b4c1fd1..4a6be70c63347 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -749,9 +749,14 @@ export function isActiveDocument(element: Element): boolean { /** * Returns the active document across all child windows. - * Use this instead of `document` when reacting to dom events to handle multiple windows. + * Use this instead of `document` when reacting to dom + * events to handle multiple windows. */ export function getActiveDocument(): Document { + if (getWindowsCount() <= 1) { + return document; + } + const documents = Array.from(getWindows()).map(window => window.document); return documents.find(doc => doc.hasFocus()) ?? document; } @@ -797,9 +802,6 @@ export function createStyleSheet(container: HTMLElement = document.head, beforeA // With as container, the stylesheet becomes global and is tracked // to support auxiliary windows to clone the stylesheet. if (container === document.head) { - const clonedGlobalStylesheets = new Set(); - globalStylesheets.set(style, clonedGlobalStylesheets); - for (const targetWindow of getWindows()) { if (targetWindow === window) { continue; // main window is already tracked @@ -845,13 +847,18 @@ function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, targetWindow: }); observer.observe(globalStylesheet, { childList: true }); - globalStylesheets.get(globalStylesheet)?.add(clone); + let clonedGlobalStylesheets = globalStylesheets.get(globalStylesheet); + if (!clonedGlobalStylesheets) { + clonedGlobalStylesheets = new Set(); + globalStylesheets.set(globalStylesheet, clonedGlobalStylesheets); + } + clonedGlobalStylesheets.add(clone); return toDisposable(() => { observer.disconnect(); targetWindow.document.head.removeChild(clone); - globalStylesheets.get(globalStylesheet)?.delete(clone); + clonedGlobalStylesheets?.delete(clone); }); } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 1554dfade7bdf..9eb840c07071c 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isNative, isIOS } from 'vs/base/common/platform'; @@ -47,11 +47,12 @@ import { IBannerService } from 'vs/workbench/services/banner/browser/bannerServi import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { AuxiliaryBarPart } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { IAuxiliaryWindowService, isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; //#region Layout Implementation interface ILayoutRuntimeState { + activeContainer: 'main' | number /* window ID */; fullscreen: boolean; maximized: boolean; hasFocus: boolean; @@ -451,10 +452,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private onWindowFocusChanged(hasFocus: boolean): void { if (hasFocus) { - // This is a bit simplified: we assume that the active container - // has changed when receiving focus, but we might end up with - // the same active container as before... - this._onDidChangeActiveContainer.fire(); + const activeContainerId = this.getActiveContainerId(); + if (this.state.runtime.activeContainer !== activeContainerId) { + this.state.runtime.activeContainer = activeContainerId; + this._onDidChangeActiveContainer.fire(); + } } if (this.state.runtime.hasFocus !== hasFocus) { @@ -463,6 +465,18 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } } + private getActiveContainerId(): 'main' | number { + const activeContainer = this.activeContainer; + if (activeContainer !== this.container) { + const containerWindow = getWindow(activeContainer); + if (isAuxiliaryWindow(containerWindow)) { + return containerWindow.vscodeWindowId; + } + } + + return 'main'; + } + private doUpdateLayoutConfiguration(skipLayout?: boolean): void { // Menubar visibility @@ -601,6 +615,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Layout Runtime State const layoutRuntimeState: ILayoutRuntimeState = { + activeContainer: this.getActiveContainerId(), fullscreen: isFullscreen(), hasFocus: this.hostService.hasFocus, maximized: false, diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 30219f5b97db8..963596b3eec94 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -680,12 +680,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close all inactive editors first to prevent UI flicker for (const inactiveEditor of inactiveEditors) { - this.doCloseEditor(inactiveEditor, false); + this.doCloseEditor(inactiveEditor, true); } // Close active one last if (activeEditor) { - this.doCloseEditor(activeEditor, false); + this.doCloseEditor(activeEditor, true); } } @@ -1108,8 +1108,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Without an editor pane, recover by closing the active editor // (if the input is still the active one) if (!pane && this.activeEditor === editor) { - const focusNext = !options || !options.preserveFocus; - this.doCloseEditor(editor, focusNext, { fromError: true }); + this.doCloseEditor(editor, options?.preserveFocus, { fromError: true }); } return pane; @@ -1283,7 +1282,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // ...and a close afterwards (unless we copy) if (!keepCopy) { - this.doCloseEditor(editor, false /* do not focus next one behind if any */, { ...internalOptions, context: EditorCloseContext.MOVE }); + this.doCloseEditor(editor, true /* do not focus next one behind if any */, { ...internalOptions, context: EditorCloseContext.MOVE }); } } @@ -1348,12 +1347,12 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Do close - this.doCloseEditor(editor, options?.preserveFocus ? false : undefined, internalOptions); + this.doCloseEditor(editor, options?.preserveFocus, internalOptions); return true; } - private doCloseEditor(editor: EditorInput, focusNext = (this.groupsView.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { + private doCloseEditor(editor: EditorInput, preserveFocus = (this.groupsView.activeGroup !== this), internalOptions?: IInternalEditorCloseOptions): void { // Forward to title control unless skipped via internal options if (!internalOptions?.skipTitleUpdate) { @@ -1362,7 +1361,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Closing the active editor of the group is a bit more work if (this.model.isActive(editor)) { - this.doCloseActiveEditor(focusNext, internalOptions); + this.doCloseActiveEditor(preserveFocus, internalOptions); } // Closing inactive editor is just a model update @@ -1376,9 +1375,9 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } } - private doCloseActiveEditor(focusNext = (this.groupsView.activeGroup === this), internalOptions?: IInternalEditorCloseOptions): void { + private doCloseActiveEditor(preserveFocus = (this.groupsView.activeGroup !== this), internalOptions?: IInternalEditorCloseOptions): void { const editorToClose = this.activeEditor; - const restoreFocus = this.shouldRestoreFocus(this.element); + const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.element); // Optimization: if we are about to close the last editor in this group and settings // are configured to close the group since it will be empty, we first set the last @@ -1408,8 +1407,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Open next active if there are more to show const nextActiveEditor = this.model.activeEditor; if (nextActiveEditor) { - const preserveFocus = !focusNext; - let activation: EditorActivation | undefined = undefined; if (preserveFocus && this.groupsView.activeGroup !== this) { // If we are opening the next editor in an inactive group @@ -1723,7 +1720,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close active editor last if contained in editors list to close if (closeActiveEditor) { - this.doCloseActiveEditor(options?.preserveFocus ? false : undefined); + this.doCloseActiveEditor(options?.preserveFocus); } // Forward to title control @@ -1827,7 +1824,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (!editor.matches(replacement)) { let closed = false; if (forceReplaceDirty) { - this.doCloseEditor(editor, false, { context: EditorCloseContext.REPLACE }); + this.doCloseEditor(editor, true, { context: EditorCloseContext.REPLACE }); closed = true; } else { closed = await this.doCloseEditorWithConfirmationHandling(editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); @@ -1848,7 +1845,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Close replaced active editor unless they match if (!activeReplacement.editor.matches(activeReplacement.replacement)) { if (activeReplacement.forceReplaceDirty) { - this.doCloseEditor(activeReplacement.editor, false, { context: EditorCloseContext.REPLACE }); + this.doCloseEditor(activeReplacement.editor, true, { context: EditorCloseContext.REPLACE }); } else { await this.doCloseEditorWithConfirmationHandling(activeReplacement.editor, { preserveFocus: true }, { context: EditorCloseContext.REPLACE }); } diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 6535966e5b9ed..930279405895f 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -29,7 +29,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { getActiveWindow } from 'vs/base/browser/dom'; -import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService'; +import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; export class CloseWindowAction extends Action2 { diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index 8c1a57c8aa806..f29a470f3a8f8 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -37,13 +37,21 @@ export interface IAuxiliaryWindow extends IDisposable { readonly onDidLayout: Event; readonly onDidClose: Event; - readonly window: Window & typeof globalThis; + readonly window: AuxiliaryWindow; readonly container: HTMLElement; layout(): void; } -export type AuxiliaryWindow = Window & typeof globalThis; +export type AuxiliaryWindow = Window & typeof globalThis & { + readonly vscodeWindowId: number; +}; + +export function isAuxiliaryWindow(obj: unknown): obj is AuxiliaryWindow { + const candidate = obj as AuxiliaryWindow | undefined; + + return !!candidate && Object.hasOwn(candidate, 'vscodeWindowId'); +} export class BrowserAuxiliaryWindowService extends Disposable implements IAuxiliaryWindowService { @@ -51,6 +59,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili private static readonly DEFAULT_SIZE = { width: 800, height: 600 }; + private static WINDOW_IDS = 0; + private readonly _onDidOpenAuxiliaryWindow = this._register(new Emitter()); readonly onDidOpenAuxiliaryWindow = this._onDidOpenAuxiliaryWindow.event; @@ -72,7 +82,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili disposables.add(registerWindow(auxiliaryWindow)); disposables.add(toDisposable(() => auxiliaryWindow.close())); - const { container, onDidLayout, onDidClose } = this.create(auxiliaryWindow, disposables); + const { container, onDidLayout, onDidClose } = await this.create(auxiliaryWindow, disposables); const result: IAuxiliaryWindow = { window: auxiliaryWindow, @@ -118,11 +128,11 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili })).result; } - return auxiliaryWindow?.window; + return auxiliaryWindow?.window as AuxiliaryWindow | undefined; } - protected create(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore) { - this.patchMethods(auxiliaryWindow); + protected async create(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore) { + await this.patchMethods(auxiliaryWindow); this.applyMeta(auxiliaryWindow); this.applyCSS(auxiliaryWindow, disposables); @@ -263,7 +273,17 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili return { onDidLayout, onDidClose }; } - protected patchMethods(auxiliaryWindow: AuxiliaryWindow): void { + protected async resolveWindowId(auxiliaryWindow: AuxiliaryWindow): Promise { + return BrowserAuxiliaryWindowService.WINDOW_IDS++; + } + + protected async patchMethods(auxiliaryWindow: AuxiliaryWindow): Promise { + + // Add a `vscodeWindowId` property to identify auxiliary windows + const resolvedWindowId = await this.resolveWindowId(auxiliaryWindow); + Object.defineProperty(auxiliaryWindow, 'vscodeWindowId', { + get: () => resolvedWindowId + }); // Disallow `createElement` because it would create // HTML Elements in the "wrong" context and break diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index 717e29eabd973..e871ae529742a 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -5,7 +5,7 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { BrowserAuxiliaryWindowService, IAuxiliaryWindowService, AuxiliaryWindow as BaseAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; +import { BrowserAuxiliaryWindowService, IAuxiliaryWindowService, AuxiliaryWindow as BrowserAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { ISandboxGlobals } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowsConfiguration } from 'vs/platform/window/common/window'; @@ -14,17 +14,10 @@ import { INativeHostService } from 'vs/platform/native/common/native'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { getActiveWindow } from 'vs/base/browser/dom'; -type AuxiliaryWindow = BaseAuxiliaryWindow & { +type NativeAuxiliaryWindow = BrowserAuxiliaryWindow & { readonly vscode: ISandboxGlobals; - readonly vscodeWindowId: number; }; -export function isAuxiliaryWindow(obj: unknown): obj is AuxiliaryWindow { - const candidate = obj as AuxiliaryWindow | undefined; - - return !!candidate?.vscode && Object.hasOwn(candidate, 'vscodeWindowId'); -} - export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService { constructor( @@ -36,7 +29,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService super(layoutService, dialogService); } - protected override create(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore) { + protected override create(auxiliaryWindow: NativeAuxiliaryWindow, disposables: DisposableStore) { // Zoom level const windowConfig = this.configurationService.getValue(); @@ -46,19 +39,12 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.create(auxiliaryWindow, disposables); } - protected override patchMethods(auxiliaryWindow: AuxiliaryWindow): void { - super.patchMethods(auxiliaryWindow); - - // Obtain window identifier - let resolvedWindowId: number; - (async () => { - resolvedWindowId = await auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:getWindowId'); - })(); + protected override resolveWindowId(auxiliaryWindow: NativeAuxiliaryWindow): Promise { + return auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:getWindowId'); + } - // Add a `windowId` property - Object.defineProperty(auxiliaryWindow, 'vscodeWindowId', { - get: () => resolvedWindowId - }); + protected override async patchMethods(auxiliaryWindow: NativeAuxiliaryWindow): Promise { + await super.patchMethods(auxiliaryWindow); // Enable `window.focus()` to work in Electron by // asking the main process to focus the window. @@ -69,7 +55,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService originalWindowFocus(); if (getActiveWindow() !== auxiliaryWindow) { - that.nativeHostService.focusWindow({ targetWindowId: resolvedWindowId }); + that.nativeHostService.focusWindow({ targetWindowId: auxiliaryWindow.vscodeWindowId }); } }; } diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 8909c6bd0f5a5..430d0c6c6491a 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -14,7 +14,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { NativeHostService } from 'vs/platform/native/electron-sandbox/nativeHostService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { IMainProcessService } from 'vs/platform/ipc/common/mainProcessService'; -import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService'; +import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { getActiveDocument, getWindowsCount, onDidRegisterWindow, trackFocus } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { memoize } from 'vs/base/common/decorators'; From de231c5aa757f768e379058e86c8f174b68b9e1c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 28 Oct 2023 07:05:12 +0200 Subject: [PATCH 02/13] aux window - fix more focus issues --- .../workbench/browser/parts/editor/editor.ts | 4 +-- .../browser/parts/editor/editorGroupView.ts | 4 +-- .../browser/parts/editor/editorPart.ts | 33 ++++++++++--------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index e37f6f54a1f16..de768f434cb7b 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -223,7 +223,7 @@ export interface IEditorGroupsView { getGroup(identifier: GroupIdentifier): IEditorGroupView | undefined; getGroups(order: GroupsOrder): IEditorGroupView[]; - activateGroup(identifier: IEditorGroupView | GroupIdentifier): IEditorGroupView; + activateGroup(identifier: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean): IEditorGroupView; restoreGroup(identifier: IEditorGroupView | GroupIdentifier): IEditorGroupView; addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, groupToCopy?: IEditorGroupView): IEditorGroupView; @@ -232,7 +232,7 @@ export interface IEditorGroupsView { moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView; copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView; - removeGroup(group: IEditorGroupView | GroupIdentifier): void; + removeGroup(group: IEditorGroupView | GroupIdentifier, preserveFocus?: boolean): void; arrangeGroups(arrangement: GroupsArrangement, target?: IEditorGroupView | GroupIdentifier): void; toggleMaximizeGroup(group?: IEditorGroupView | GroupIdentifier): void; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 963596b3eec94..bc5cd6777e6b9 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1394,7 +1394,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { if (restoreFocus) { nextActiveGroup.focus(); } else { - this.groupsView.activateGroup(nextActiveGroup); + this.groupsView.activateGroup(nextActiveGroup, true); } } } @@ -1455,7 +1455,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Remove empty group if we should if (closeEmptyGroup) { - this.groupsView.removeGroup(this); + this.groupsView.removeGroup(this, preserveFocus); } } } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 5c4832ccd65c3..bc3c683d128b4 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -14,7 +14,7 @@ import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGr import { GroupIdentifier, EditorInputWithOptions, IEditorPartOptions, IEditorPartOptionsChangeEvent, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions, IEditorPartsView, IEditorGroupsView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -79,7 +79,7 @@ class GridWidgetView implements IView { } } -export class EditorPart extends Part implements IEditorPart { +export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { private static readonly EDITOR_PART_UI_STATE_STORAGE_KEY = 'editorpart.state'; private static readonly EDITOR_PART_CENTERED_VIEW_STORAGE_KEY = 'editorpart.centeredview'; @@ -337,10 +337,15 @@ export class EditorPart extends Part implements IEditorPart { } } - activateGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView { + activateGroup(group: IEditorGroupView | GroupIdentifier, preserveWindowOrder?: boolean): IEditorGroupView { const groupView = this.assertGroupView(group); this.doSetGroupActive(groupView); + // Ensure window on top unless disabled + if (!preserveWindowOrder) { + this.hostService.moveTop(getWindow(this.element)); + } + return groupView; } @@ -667,10 +672,6 @@ export class EditorPart extends Part implements IEditorPart { } private doSetGroupActive(group: IEditorGroupView): void { - - // Ensure window on top - this.hostService.moveTop(getWindow(this.element)); - if (this._activeGroup !== group) { const previousActiveGroup = this._activeGroup; this._activeGroup = group; @@ -743,7 +744,7 @@ export class EditorPart extends Part implements IEditorPart { return fallback; } - removeGroup(group: IEditorGroupView | GroupIdentifier): void { + removeGroup(group: IEditorGroupView | GroupIdentifier, preserveFocus?: boolean): void { const groupView = this.assertGroupView(group); if (this.count === 1) { return; // Cannot remove the last root group @@ -751,14 +752,14 @@ export class EditorPart extends Part implements IEditorPart { // Remove empty group if (groupView.isEmpty) { - return this.doRemoveEmptyGroup(groupView); + return this.doRemoveEmptyGroup(groupView, preserveFocus); } // Remove group with editors - this.doRemoveGroupWithEditors(groupView); + this.doRemoveGroupWithEditors(groupView, preserveFocus); } - private doRemoveGroupWithEditors(groupView: IEditorGroupView): void { + private doRemoveGroupWithEditors(groupView: IEditorGroupView, preserveFocus?: boolean): void { const mostRecentlyActiveGroups = this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); let lastActiveGroup: IEditorGroupView; @@ -773,14 +774,14 @@ export class EditorPart extends Part implements IEditorPart { this.mergeGroup(groupView, lastActiveGroup); } - private doRemoveEmptyGroup(groupView: IEditorGroupView): void { - const restoreFocus = this.shouldRestoreFocus(this.container); + private doRemoveEmptyGroup(groupView: IEditorGroupView, preserveFocus?: boolean): void { + const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.container); // Activate next group if the removed one was active if (this._activeGroup === groupView) { const mostRecentlyActiveGroups = this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current group we are about to dispose - this.activateGroup(nextActiveGroup); + this.doSetGroupActive(nextActiveGroup); } // Remove from grid widget & dispose @@ -830,7 +831,7 @@ export class EditorPart extends Part implements IEditorPart { else { movedView = targetView.groupsView.addGroup(targetView, direction, sourceView); sourceView.closeAllEditors(); - this.removeGroup(sourceView); + this.removeGroup(sourceView, restoreFocus); } // Restore focus if we had it previously after completing the grid @@ -892,7 +893,7 @@ export class EditorPart extends Part implements IEditorPart { // Remove source if the view is now empty and not already removed if (sourceView.isEmpty && !sourceView.disposed /* could have been disposed already via workbench.editor.closeEmptyGroups setting */) { - this.removeGroup(sourceView); + this.removeGroup(sourceView, true); } return targetView; From 7d37e1ed59b295b926f9da2d3f3117bbeb6b6cfb Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 28 Oct 2023 07:54:27 +0200 Subject: [PATCH 03/13] debt - allow to pass in disposables to `createStyleSheet` --- src/vs/base/browser/dom.ts | 16 +++++++++++----- src/vs/editor/browser/editorDom.ts | 14 +++++++------- .../browser/actions/developerActions.ts | 5 +---- .../browser/unfocusedViewDimmingContribution.ts | 9 ++++++--- .../mergeEditor/browser/view/conflictActions.ts | 8 ++------ .../contrib/scm/browser/dirtydiffDecorator.ts | 6 ++---- .../contrib/terminal/browser/terminalIcon.ts | 7 ++++--- .../webviewPanel/browser/webviewIconManager.ts | 9 ++++++--- .../decorations/browser/decorationsService.ts | 5 ++--- 9 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 4a6be70c63347..a843f9f014459 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -792,13 +792,17 @@ export function focusWindow(element: Node): void { const globalStylesheets = new Map>(); -export function createStyleSheet(container: HTMLElement = document.head, beforeAppend?: (style: HTMLStyleElement) => void): HTMLStyleElement { +export function createStyleSheet(container: HTMLElement = document.head, beforeAppend?: (style: HTMLStyleElement) => void, disposableStore?: DisposableStore): HTMLStyleElement { const style = document.createElement('style'); style.type = 'text/css'; style.media = 'screen'; beforeAppend?.(style); container.appendChild(style); + if (disposableStore) { + disposableStore.add(toDisposable(() => container.removeChild(style))); + } + // With as container, the stylesheet becomes global and is tracked // to support auxiliary windows to clone the stylesheet. if (container === document.head) { @@ -807,14 +811,16 @@ export function createStyleSheet(container: HTMLElement = document.head, beforeA continue; // main window is already tracked } - const disposable = cloneGlobalStyleSheet(style, targetWindow); + const cloneDisposable = cloneGlobalStyleSheet(style, targetWindow); + disposableStore?.add(cloneDisposable); - event.Event.once(onDidUnregisterWindow)(unregisteredWindow => { + disposableStore?.add(event.Event.once(onDidUnregisterWindow)(unregisteredWindow => { if (unregisteredWindow === targetWindow) { - disposable.dispose(); + cloneDisposable.dispose(); } - }); + })); } + } return style; diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 6037946742616..e533d4d13ca0e 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -7,7 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { GlobalPointerMoveMonitor } from 'vs/base/browser/globalPointerMoveMonitor'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { asCssVariable } from 'vs/platform/theme/common/colorRegistry'; import { ThemeColor } from 'vs/base/common/themables'; @@ -358,7 +358,8 @@ export interface CssProperties { class RefCountedCssRule { private _referenceCount: number = 0; - private _styleElement: HTMLStyleElement; + private _styleElement: HTMLStyleElement | undefined; + private _styleElementDisposables: DisposableStore; constructor( public readonly key: string, @@ -366,10 +367,8 @@ class RefCountedCssRule { _containerElement: HTMLElement | undefined, public readonly properties: CssProperties, ) { - this._styleElement = dom.createStyleSheet( - _containerElement - ); - + this._styleElementDisposables = new DisposableStore(); + this._styleElement = dom.createStyleSheet(_containerElement, undefined, this._styleElementDisposables); this._styleElement.textContent = this.getCssText(this.className, this.properties); } @@ -392,7 +391,8 @@ class RefCountedCssRule { } public dispose(): void { - this._styleElement.remove(); + this._styleElementDisposables.dispose(); + this._styleElement = undefined; } public increaseRefCount(): void { diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 49144b6102d7e..18d160704a641 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -56,10 +56,7 @@ class InspectContextKeysAction extends Action2 { const disposables = new DisposableStore(); - const stylesheet = createStyleSheet(); - disposables.add(toDisposable(() => { - stylesheet.parentNode?.removeChild(stylesheet); - })); + const stylesheet = createStyleSheet(undefined, undefined, disposables); createCSSRule('*', 'cursor: crosshair !important;', stylesheet); const hoverFeedback = document.createElement('div'); diff --git a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts index 9319c21af7f9f..013341c5f1818 100644 --- a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts @@ -5,7 +5,7 @@ import { createStyleSheet } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { clamp } from 'vs/base/common/numbers'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -13,6 +13,7 @@ import { AccessibilityWorkbenchSettingId, ViewDimUnfocusedOpacityProperties } fr export class UnfocusedViewDimmingContribution extends Disposable implements IWorkbenchContribution { private _styleElement?: HTMLStyleElement; + private _styleElementDisposables: DisposableStore | undefined = undefined; constructor( @IConfigurationService configurationService: IConfigurationService, @@ -74,14 +75,16 @@ export class UnfocusedViewDimmingContribution extends Disposable implements IWor private _getStyleElement(): HTMLStyleElement { if (!this._styleElement) { - this._styleElement = createStyleSheet(); + this._styleElementDisposables = new DisposableStore(); + this._styleElement = createStyleSheet(undefined, undefined, this._styleElementDisposables); this._styleElement.className = 'accessibilityUnfocusedViewOpacity'; } return this._styleElement; } private _removeStyleElement(): void { - this._styleElement?.remove(); + this._styleElementDisposables?.dispose(); + this._styleElementDisposables = undefined; this._styleElement = undefined; } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts index 69b4fac15a9cc..8d88a891a5b79 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/conflictActions.ts @@ -6,7 +6,7 @@ import { $, createStyleSheet, h, isInShadowDOM, reset } from 'vs/base/browser/dom'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { hash } from 'vs/base/common/hash'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, IObservable, transaction } from 'vs/base/common/observable'; import { ICodeEditor, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser'; import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; @@ -32,13 +32,9 @@ export class ConflictActionsFactory extends Disposable { this._styleElement = createStyleSheet( isInShadowDOM(this._editor.getContainerDomNode()) ? this._editor.getContainerDomNode() - : undefined + : undefined, undefined, this._store ); - this._register(toDisposable(() => { - this._styleElement.remove(); - })); - this._updateLensStyle(); } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 6fc13ccd0efa9..f330ff686e156 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -723,8 +723,7 @@ export class DirtyDiffController extends Disposable implements DirtyDiffContribu ) { super(); this.enabled = !contextKeyService.getContextKeyValue('isInDiffEditor'); - this.stylesheet = dom.createStyleSheet(); - this._register(toDisposable(() => this.stylesheet.remove())); + this.stylesheet = dom.createStyleSheet(undefined, undefined, this._store); if (this.enabled) { this.isDirtyDiffVisible = isDirtyDiffVisible.bindTo(contextKeyService); @@ -1557,8 +1556,7 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor @ITextFileService private readonly textFileService: ITextFileService ) { super(); - this.stylesheet = dom.createStyleSheet(); - this._register(toDisposable(() => this.stylesheet.parentElement!.removeChild(this.stylesheet))); + this.stylesheet = dom.createStyleSheet(undefined, undefined, this._store); const onDidChangeConfiguration = Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.diffDecorations')); this._register(onDidChangeConfiguration(this.onDidChangeConfiguration, this)); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts index 07b474a831fe7..1ea310c533f5a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts @@ -15,7 +15,7 @@ import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/termina import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { ansiColorMap } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { createStyleSheet } from 'vs/base/browser/dom'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; export function getColorClass(colorKey: string): string; @@ -50,8 +50,9 @@ export function getStandardColors(colorTheme: IColorTheme): string[] { } export function createColorStyleElement(colorTheme: IColorTheme): IDisposable { + const disposable = new DisposableStore(); const standardColors = getStandardColors(colorTheme); - const styleElement = createStyleSheet(); + const styleElement = createStyleSheet(undefined, undefined, disposable); let css = ''; for (const colorKey of standardColors) { const colorClass = getColorClass(colorKey); @@ -64,7 +65,7 @@ export function createColorStyleElement(colorTheme: IColorTheme): IDisposable { } } styleElement.textContent = css; - return toDisposable(() => styleElement.remove()); + return disposable; } export function getColorStyleContent(colorTheme: IColorTheme, editor?: boolean): string { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts index 7ffb3adf21346..f24731711d3ca 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -19,6 +19,7 @@ export class WebviewIconManager implements IDisposable { private readonly _icons = new Map(); private _styleElement: HTMLStyleElement | undefined; + private _styleElementDisposable: DisposableStore | undefined; constructor( @ILifecycleService private readonly _lifecycleService: ILifecycleService, @@ -32,13 +33,15 @@ export class WebviewIconManager implements IDisposable { } dispose() { - this._styleElement?.remove(); + this._styleElementDisposable?.dispose(); + this._styleElementDisposable = undefined; this._styleElement = undefined; } private get styleElement(): HTMLStyleElement { if (!this._styleElement) { - this._styleElement = dom.createStyleSheet(); + this._styleElementDisposable = new DisposableStore(); + this._styleElement = dom.createStyleSheet(undefined, undefined, this._styleElementDisposable); this._styleElement.className = 'webview-icons'; } return this._styleElement; diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index e725eccdd1bf7..31f1057999d9a 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -158,16 +158,15 @@ class DecorationRule { class DecorationStyles { - private readonly _styleElement = createStyleSheet(); - private readonly _decorationRules = new Map(); private readonly _dispoables = new DisposableStore(); + private readonly _styleElement = createStyleSheet(undefined, undefined, this._dispoables); + private readonly _decorationRules = new Map(); constructor(private readonly _themeService: IThemeService) { } dispose(): void { this._dispoables.dispose(); - this._styleElement.remove(); } asDecoration(data: IDecorationData[], onlyChildren: boolean): IDecoration { From b50b0ff9cb259fe69eeef1953e6b1d7fcc40775a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 28 Oct 2023 08:00:09 +0200 Subject: [PATCH 04/13] aux window - add perf marks for opening --- .../browser/auxiliaryWindowService.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index f29a470f3a8f8..53fde2cafa962 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; +import { mark } from 'vs/base/common/performance'; import { Emitter, Event } from 'vs/base/common/event'; import { Dimension, EventHelper, EventType, addDisposableListener, cloneGlobalStylesheets, copyAttributes, createMetaElement, getActiveWindow, getClientArea, isGlobalStylesheet, position, registerWindow, size, trackAttributes } from 'vs/base/browser/dom'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -72,6 +73,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } async open(options?: { position?: IRectangle }): Promise { + mark('code/auxiliaryWindow/willOpen'); + const disposables = new DisposableStore(); const auxiliaryWindow = await this.doOpen(options); @@ -97,6 +100,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili disposables.add(eventDisposables); this._onDidOpenAuxiliaryWindow.fire({ window: result, disposables: eventDisposables }); + mark('code/auxiliaryWindow/didOpen'); + return result; } @@ -161,6 +166,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected applyCSS(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore): void { + mark('code/auxiliaryWindow/willApplyCSS'); + const mapOriginalToClone = new Map(); function cloneNode(originalNode: Node): void { @@ -223,9 +230,12 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili observer.observe(document.head, { childList: true, subtree: true }); disposables.add(toDisposable(() => observer.disconnect())); + + mark('code/auxiliaryWindow/didApplyCSS'); } private applyHTML(auxiliaryWindow: AuxiliaryWindow, disposables: DisposableStore): HTMLElement { + mark('code/auxiliaryWindow/willApplyHTML'); // Create workbench container and apply classes const container = document.createElement('div'); @@ -236,6 +246,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili disposables.add(trackAttributes(document.body, auxiliaryWindow.document.body)); disposables.add(trackAttributes(this.layoutService.container, container, ['class'])); // only class attribute + mark('code/auxiliaryWindow/didApplyHTML'); + return container; } @@ -278,6 +290,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected async patchMethods(auxiliaryWindow: AuxiliaryWindow): Promise { + mark('code/auxiliaryWindow/willPatchMethods'); // Add a `vscodeWindowId` property to identify auxiliary windows const resolvedWindowId = await this.resolveWindowId(auxiliaryWindow); @@ -291,6 +304,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili auxiliaryWindow.document.createElement = function () { throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); }; + + mark('code/auxiliaryWindow/didPatchMethods'); } } From f39a6b1072b6fd20062943907e65d9e76d8a84a3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sat, 28 Oct 2023 19:58:29 +0200 Subject: [PATCH 05/13] aux window - require window context in more places --- src/vs/base/browser/dom.ts | 4 ++-- src/vs/workbench/browser/dnd.ts | 7 ++----- .../workbench/browser/parts/editor/editorDropTarget.ts | 2 +- .../browser/parts/editor/multiEditorTabsControl.ts | 4 ++-- src/vs/workbench/browser/parts/titlebar/windowTitle.ts | 2 +- src/vs/workbench/contrib/debug/browser/debugSession.ts | 2 +- .../extensions/browser/extensionsWorkbenchService.ts | 2 +- .../workbench/contrib/files/browser/fileImportExport.ts | 8 ++++---- .../workbench/contrib/files/browser/views/emptyView.ts | 4 ++-- .../contrib/files/browser/views/explorerViewer.ts | 2 +- .../contrib/files/browser/views/openEditorsView.ts | 2 +- src/vs/workbench/electron-sandbox/window.ts | 2 +- .../services/host/browser/browserHostService.ts | 8 ++++---- src/vs/workbench/services/host/browser/host.ts | 4 ++-- .../services/host/electron-sandbox/nativeHostService.ts | 9 ++++++--- .../services/url/electron-sandbox/urlService.ts | 2 +- src/vs/workbench/test/browser/workbenchTestServices.ts | 2 +- 17 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index a843f9f014459..89c196bd81ef5 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -830,7 +830,7 @@ export function isGlobalStylesheet(node: Node): boolean { return globalStylesheets.has(node as HTMLStyleElement); } -export function cloneGlobalStylesheets(targetWindow: Window & typeof globalThis): IDisposable { +export function cloneGlobalStylesheets(targetWindow: Window): IDisposable { const disposables = new DisposableStore(); for (const [globalStylesheet] of globalStylesheets) { @@ -840,7 +840,7 @@ export function cloneGlobalStylesheets(targetWindow: Window & typeof globalThis) return disposables; } -function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, targetWindow: Window & typeof globalThis): IDisposable { +function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, targetWindow: Window): IDisposable { const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement; targetWindow.document.head.appendChild(clone); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 5a9796b18db1f..3a7ba7f708d4f 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -96,14 +96,14 @@ export class ResourcesDropHandler { ) { } - async handleDrop(event: DragEvent, resolveTargetGroup?: () => IEditorGroup | undefined, afterDrop?: (targetGroup: IEditorGroup | undefined) => void, options?: IEditorOptions): Promise { + async handleDrop(event: DragEvent, targetWindow: Window, resolveTargetGroup?: () => IEditorGroup | undefined, afterDrop?: (targetGroup: IEditorGroup | undefined) => void, options?: IEditorOptions): Promise { const editors = await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, event)); if (!editors.length) { return; } // Make the window active to handle the drop properly within - await this.hostService.focus(); + await this.hostService.focus(targetWindow); // Check for workspace file / folder being dropped if we are allowed to do so if (this.options.allowWorkspaceOpen) { @@ -168,9 +168,6 @@ export class ResourcesDropHandler { return false; } - // Pass focus to window - this.hostService.focus(); - // Open in separate windows if we drop workspaces or just one folder if (toOpen.length > folderURIs.length || folderURIs.length === 1) { await this.hostService.openWindow(toOpen); diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 64221b9382dcd..646f5869387c3 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -373,7 +373,7 @@ class DropOverlay extends Themable { // Check for URI transfer else { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: !isWeb || isTemporaryWorkspace(this.contextService.getWorkspace()) }); - dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => targetGroup?.focus()); + dropHandler.handleDrop(event, getWindow(this.groupView.element), () => ensureTargetGroup(), targetGroup => targetGroup?.focus()); } } diff --git a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts index a9ed03f97c231..834e2d666440c 100644 --- a/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts @@ -32,7 +32,7 @@ import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdenti import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { MergeGroupMode, IMergeGroupOptions, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver, isMouseEvent, getWindow } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsView, EditorServiceImpl, IEditorGroupView, IInternalEditorOpenOptions, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; @@ -2069,7 +2069,7 @@ export class MultiEditorTabsControl extends EditorTabsControl { // Check for URI transfer else { const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false }); - dropHandler.handleDrop(e, () => this.groupView, () => this.groupView.focus(), options); + dropHandler.handleDrop(e, getWindow(this.titleContainer), () => this.groupView, () => this.groupView.focus(), options); } } diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 5e97cbebd8571..48b791514f2c0 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -54,7 +54,7 @@ export class WindowTitle extends Disposable { private readonly editorService: IEditorService; constructor( - private readonly targetWindow: Window & typeof globalThis, + private readonly targetWindow: Window, editorGroupsContainer: IEditorGroupsContainer | 'main', @IConfigurationService protected readonly configurationService: IConfigurationService, @IEditorService editorService: IEditorService, diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 8dbd2c26bfce5..48edae1a29a7f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -1012,7 +1012,7 @@ export class DebugSession implements IDebugSession, IDisposable { } if (this.configurationService.getValue('debug').focusWindowOnBreak && !this.workbenchEnvironmentService.extensionTestsLocationURI) { - await this.hostService.focus({ force: true /* Application may not be active */ }); + await this.hostService.focus(window, { force: true /* Application may not be active */ }); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 3bb04e433a22d..55c422a93102e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -1928,7 +1928,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension [extension] = await this.getExtensions([{ id: extensionId }], { source: 'uri' }, CancellationToken.None); } if (extension) { - await this.hostService.focus(); + await this.hostService.focus(window); await this.open(extension); } }).then(undefined, error => this.onError(error)); diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 0ad3bfa636a0a..10a1453fd054e 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -402,7 +402,7 @@ export class ExternalFileImport { ) { } - async import(target: ExplorerItem, source: DragEvent): Promise { + async import(target: ExplorerItem, source: DragEvent, targetWindow: Window): Promise { const cts = new CancellationTokenSource(); // Indicate progress globally @@ -413,7 +413,7 @@ export class ExternalFileImport { cancellable: true, title: localize('copyingFiles', "Copying...") }, - async () => await this.doImport(target, source, cts.token), + async () => await this.doImport(target, source, targetWindow, cts.token), () => cts.dispose(true) ); @@ -423,7 +423,7 @@ export class ExternalFileImport { return importPromise; } - private async doImport(target: ExplorerItem, source: DragEvent, token: CancellationToken): Promise { + private async doImport(target: ExplorerItem, source: DragEvent, targetWindow: Window, token: CancellationToken): Promise { // Activate all providers for the resources dropped const candidateFiles = coalesce((await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, source))).map(editor => editor.resource)); @@ -438,7 +438,7 @@ export class ExternalFileImport { } // Pass focus to window - this.hostService.focus(); + this.hostService.focus(targetWindow); // Handle folders by adding to workspace if we are in workspace context and if dropped on top const folders = resolvedFiles.filter(resolvedFile => resolvedFile.success && resolvedFile.stat?.isDirectory).map(resolvedFile => ({ uri: resolvedFile.stat!.resource })); diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index 4f450ff50e075..aba7df4c9427a 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -20,7 +20,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isWeb } from 'vs/base/common/platform'; -import { DragAndDropObserver } from 'vs/base/browser/dom'; +import { DragAndDropObserver, getWindow } from 'vs/base/browser/dom'; import { ILocalizedString } from 'vs/platform/action/common/action'; export class EmptyView extends ViewPane { @@ -60,7 +60,7 @@ export class EmptyView extends ViewPane { onDrop: e => { container.style.backgroundColor = ''; const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: !isWeb || isTemporaryWorkspace(this.contextService.getWorkspace()) }); - dropHandler.handleDrop(e); + dropHandler.handleDrop(e, getWindow(container)); }, onDragEnter: () => { const color = this.themeService.getColorTheme().getColor(listDropBackground); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index d567ec809169a..ab9459d78a8a5 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -1260,7 +1260,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Use local file import when supported if (!isWeb || (isTemporaryWorkspace(this.contextService.getWorkspace()) && WebFileSystemAccess.supported(window))) { const fileImport = this.instantiationService.createInstance(ExternalFileImport); - await fileImport.import(resolvedTarget, originalEvent); + await fileImport.import(resolvedTarget, originalEvent, window); } // Otherwise fallback to browser based file upload else { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 5a881944d4fb1..96edb1c4abea4 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -718,7 +718,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop group, () => group.focus(), { index }); + this.dropHandler.handleDrop(originalEvent, window, () => group, () => group.focus(), { index }); } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 0c606c68a2c91..0f5e52f53429b 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -683,7 +683,7 @@ export class NativeWindow extends Disposable { originalWindowFocus(); if (getActiveWindow() !== window) { - that.nativeHostService.focusWindow(); + that.nativeHostService.focusWindow({ targetWindowId: that.nativeHostService.windowId }); } }; } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 4f56301f2f9af..40f79b6a74c2b 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -14,7 +14,7 @@ import { isResourceEditorInput, pathsToEditors } from 'vs/workbench/common/edito import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService, Verbosity } from 'vs/platform/label/common/label'; -import { ModifierKeyEmitter, getActiveDocument, getActiveWindow, onDidRegisterWindow, trackFocus } from 'vs/base/browser/dom'; +import { ModifierKeyEmitter, getActiveDocument, onDidRegisterWindow, trackFocus } from 'vs/base/browser/dom'; import { Disposable } from 'vs/base/common/lifecycle'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { memoize } from 'vs/base/common/decorators'; @@ -201,8 +201,8 @@ export class BrowserHostService extends Disposable implements IHostService { return true; } - async focus(): Promise { - getActiveWindow().focus(); + async focus(window: Window): Promise { + window.focus(); } //#endregion @@ -505,7 +505,7 @@ export class BrowserHostService extends Disposable implements IHostService { } } - async moveTop(window: Window & typeof globalThis): Promise { + async moveTop(window: Window): Promise { // There seems to be no API to bring a window to front in browsers } diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 569f40e6c6035..fb2eb102556f9 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -46,7 +46,7 @@ export interface IHostService { * focused application which may not be VSCode. It may not be supported * in all environments. */ - focus(options?: { force: boolean }): Promise; + focus(window: Window, options?: { force: boolean }): Promise; //#endregion @@ -72,7 +72,7 @@ export interface IHostService { /** * Bring a window to the front and restore it if needed. */ - moveTop(window: Window & typeof globalThis): Promise; + moveTop(window: Window): Promise; //#endregion diff --git a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 430d0c6c6491a..d33a776105ada 100644 --- a/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -133,7 +133,7 @@ class WorkbenchHostService extends Disposable implements IHostService { return this.nativeHostService.toggleFullScreen(); } - async moveTop(window: Window & typeof globalThis): Promise { + async moveTop(window: Window): Promise { if (getWindowsCount() <= 1) { return; // does not apply when only one window is opened } @@ -146,8 +146,11 @@ class WorkbenchHostService extends Disposable implements IHostService { //#region Lifecycle - focus(options?: { force: boolean }): Promise { - return this.nativeHostService.focusWindow(options); + focus(window: Window, options?: { force: boolean }): Promise { + return this.nativeHostService.focusWindow({ + force: options?.force, + targetWindowId: isAuxiliaryWindow(window) ? window.vscodeWindowId : this.nativeHostService.windowId + }); } restart(): Promise { diff --git a/src/vs/workbench/services/url/electron-sandbox/urlService.ts b/src/vs/workbench/services/url/electron-sandbox/urlService.ts index 3b3000abce61a..6e3662033e61c 100644 --- a/src/vs/workbench/services/url/electron-sandbox/urlService.ts +++ b/src/vs/workbench/services/url/electron-sandbox/urlService.ts @@ -70,7 +70,7 @@ export class RelayURLService extends NativeURLService implements IURLHandler, IO if (result) { this.logService.trace('URLService#handleURL(): handled', uri.toString(true)); - await this.nativeHostService.focusWindow({ force: true /* Application may not be active */ }); + await this.nativeHostService.focusWindow({ force: true /* Application may not be active */, targetWindowId: this.nativeHostService.windowId }); } else { this.logService.trace('URLService#handleURL(): not handled', uri.toString(true)); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index c225393802587..bbbe0bdd5fcd3 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1485,7 +1485,7 @@ export class TestHostService implements IHostService { return await expectedShutdownTask(); } - async focus(options?: { force: boolean }): Promise { } + async focus(): Promise { } async moveTop(): Promise { } async openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { } From a1eda721a74dfcdab7eecdc0237420afa976754f Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 29 Oct 2023 06:00:08 +0100 Subject: [PATCH 06/13] aux window - share mutation observers for global elements --- src/vs/base/browser/dom.ts | 113 +++++++++++------- .../browser/auxiliaryWindowService.ts | 9 +- 2 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 89c196bd81ef5..fb4988c2d3477 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -12,10 +12,11 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import * as event from 'vs/base/common/event'; import * as dompurify from 'vs/base/browser/dompurify/dompurify'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { Disposable, DisposableStore, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess, RemoteAuthorities, Schemas } from 'vs/base/common/network'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; +import { hash } from 'vs/base/common/hash'; export const { registerWindow, getWindows, getWindowsCount, onDidRegisterWindow, onWillUnregisterWindow, onDidUnregisterWindow } = (function () { const windows = new Set([window]); @@ -841,17 +842,19 @@ export function cloneGlobalStylesheets(targetWindow: Window): IDisposable { } function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, targetWindow: Window): IDisposable { + const disposables = new DisposableStore(); + const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement; targetWindow.document.head.appendChild(clone); + disposables.add(toDisposable(() => targetWindow.document.head.removeChild(clone))); for (const rule of getDynamicStyleSheetRules(globalStylesheet)) { clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length); } - const observer = new MutationObserver(() => { + disposables.add(sharedMutationObserver.observe(globalStylesheet, disposables, { childList: true })(() => { clone.textContent = globalStylesheet.textContent; - }); - observer.observe(globalStylesheet, { childList: true }); + })); let clonedGlobalStylesheets = globalStylesheets.get(globalStylesheet); if (!clonedGlobalStylesheets) { @@ -859,15 +862,63 @@ function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, targetWindow: globalStylesheets.set(globalStylesheet, clonedGlobalStylesheets); } clonedGlobalStylesheets.add(clone); + disposables.add(toDisposable(() => clonedGlobalStylesheets?.delete(clone))); - return toDisposable(() => { - observer.disconnect(); - targetWindow.document.head.removeChild(clone); + return disposables; +} - clonedGlobalStylesheets?.delete(clone); - }); +interface IMutationObserver { + users: number; + readonly observer: MutationObserver; + readonly onDidMutate: event.Event; } +export const sharedMutationObserver = new class { + + private readonly mutationObservers = new Map>(); + + observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event { + let mutationObserversPerTarget = this.mutationObservers.get(target); + if (!mutationObserversPerTarget) { + mutationObserversPerTarget = new Map(); + this.mutationObservers.set(target, mutationObserversPerTarget); + } + + const optionsHash = hash(options); + let mutationObserverPerOptions = mutationObserversPerTarget.get(optionsHash); + if (!mutationObserverPerOptions) { + const onDidMutate = new event.Emitter(); + const observer = new MutationObserver(mutations => onDidMutate.fire(mutations)); + observer.observe(target, options); + + const resolvedMutationObserverPerOptions = mutationObserverPerOptions = { + users: 1, + observer, + onDidMutate: onDidMutate.event + }; + + disposables.add(toDisposable(() => { + resolvedMutationObserverPerOptions.users -= 1; + + if (resolvedMutationObserverPerOptions.users === 0) { + onDidMutate.dispose(); + observer.disconnect(); + + mutationObserversPerTarget?.delete(optionsHash); + + if (mutationObserversPerTarget?.size === 0) { + this.mutationObservers.delete(target); + } + } + })); + + mutationObserversPerTarget.set(optionsHash, mutationObserverPerOptions); + } + + return mutationObserverPerOptions.onDidMutate; + } +}; + export function createMetaElement(container: HTMLElement = document.head): HTMLMetaElement { const meta = document.createElement('meta'); container.appendChild(meta); @@ -2105,35 +2156,6 @@ function camelCaseToHyphenCase(str: string) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } -interface IObserver extends IDisposable { - readonly onDidChangeAttribute: event.Event; -} - -function observeAttributes(element: Element, filter?: string[]): IObserver { - const onDidChangeAttribute = new event.Emitter(); - - const observer = new MutationObserver(mutations => { - for (const mutation of mutations) { - if (mutation.type === 'attributes' && mutation.attributeName) { - onDidChangeAttribute.fire(mutation.attributeName); - } - } - }); - - observer.observe(element, { - attributes: true, - attributeFilter: filter - }); - - return { - onDidChangeAttribute: onDidChangeAttribute.event, - dispose: () => { - observer.disconnect(); - onDidChangeAttribute.dispose(); - } - }; -} - export function copyAttributes(from: Element, to: Element): void { for (const { name, value } of from.attributes) { to.setAttribute(name, value); @@ -2152,10 +2174,15 @@ function copyAttribute(from: Element, to: Element, name: string): void { export function trackAttributes(from: Element, to: Element, filter?: string[]): IDisposable { copyAttributes(from, to); - const observer = observeAttributes(from, filter); + const disposables = new DisposableStore(); - return combinedDisposable( - observer, - observer.onDidChangeAttribute(name => copyAttribute(from, to, name)) - ); + disposables.add(sharedMutationObserver.observe(from, disposables, { attributes: true, attributeFilter: filter })(mutations => { + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName) { + copyAttribute(from, to, mutation.attributeName); + } + } + })); + + return disposables; } diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index 53fde2cafa962..86782a5b6fbdc 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { mark } from 'vs/base/common/performance'; import { Emitter, Event } from 'vs/base/common/event'; -import { Dimension, EventHelper, EventType, addDisposableListener, cloneGlobalStylesheets, copyAttributes, createMetaElement, getActiveWindow, getClientArea, isGlobalStylesheet, position, registerWindow, size, trackAttributes } from 'vs/base/browser/dom'; +import { Dimension, EventHelper, EventType, addDisposableListener, cloneGlobalStylesheets, copyAttributes, createMetaElement, getActiveWindow, getClientArea, isGlobalStylesheet, position, registerWindow, sharedMutationObserver, size, trackAttributes } from 'vs/base/browser/dom'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -191,7 +191,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili // Listen to new stylesheets as they are being added or removed in the main window // and apply to child window (including changes to existing stylesheets elements) - const observer = new MutationObserver(mutations => { + disposables.add(sharedMutationObserver.observe(document.head, disposables, { childList: true, subtree: true })(mutations => { for (const mutation of mutations) { if ( mutation.type !== 'childList' || // only interested in added/removed nodes @@ -226,10 +226,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } } } - }); - - observer.observe(document.head, { childList: true, subtree: true }); - disposables.add(toDisposable(() => observer.disconnect())); + })); mark('code/auxiliaryWindow/didApplyCSS'); } From babc7996ad72f05511543623307017b73815a258 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 29 Oct 2023 07:04:40 +0100 Subject: [PATCH 07/13] aux window - disable menu on macOS --- src/vs/platform/menubar/electron-main/menubar.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 0ef1220538968..f2424c81382fb 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -242,7 +242,8 @@ export class Menubar { return; } - this.noActiveWindow = !BrowserWindow.getFocusedWindow(); + const focusedWindow = BrowserWindow.getFocusedWindow(); + this.noActiveWindow = !focusedWindow || !!this.auxiliaryWindowsMainService.getWindowById(focusedWindow.id); this.scheduleUpdateMenu(); } @@ -377,8 +378,10 @@ export class Menubar { Menu.setApplicationMenu(menu); - for (const window of this.auxiliaryWindowsMainService.getWindows()) { - window.win?.setMenu(null); + if (menu) { + for (const window of this.auxiliaryWindowsMainService.getWindows()) { + window.win?.setMenu(null); + } } } From 5fae19dc5ba65a83dfa0e1c88ef403f715b27d8e Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 29 Oct 2023 08:54:03 +0100 Subject: [PATCH 08/13] aux window - fix compile --- src/vs/base/browser/dom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index fb4988c2d3477..c398b0afc51cf 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -875,7 +875,7 @@ interface IMutationObserver { export const sharedMutationObserver = new class { - private readonly mutationObservers = new Map>(); + readonly mutationObservers = new Map>(); observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event { let mutationObserversPerTarget = this.mutationObservers.get(target); From dcc78a45fc148f51f2bc64166a19d5952476aaac Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 29 Oct 2023 14:32:09 +0100 Subject: [PATCH 09/13] aux window - enable some menu actions when aux window has focus --- src/vs/code/electron-main/app.ts | 2 -- .../electron-main/auxiliaryWindow.ts | 4 +++ .../auxiliaryWindowsMainService.ts | 12 ++++++++- .../platform/menubar/electron-main/menubar.ts | 27 ++++++++++++++++--- .../auxiliaryWindowService.ts | 4 +-- 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 5eef587493f58..6cb6877f8b394 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -449,8 +449,6 @@ export class CodeApplication extends Disposable { //#region Bootstrap IPC Handlers - validatedIpcMain.handle('vscode:getWindowId', event => Promise.resolve(event.sender.id)); - validatedIpcMain.handle('vscode:fetchShellEnv', event => { // Prefer to use the args and env from the target window diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts index 1b1271068a4e5..97ee7fc0ac1b2 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts @@ -15,6 +15,8 @@ export interface IAuxiliaryWindow { readonly id: number; readonly win: BrowserWindow | null; + readonly parentId: number; + readonly lastFocusTime: number; focus(options?: { force: boolean }): void; @@ -24,6 +26,8 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { readonly id = this.contents.id; + parentId = -1; + private readonly _onDidClose = this._register(new Emitter()); readonly onDidClose = this._onDidClose.event; diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts index 983494e1cf6d7..aae6d0f62b4f9 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindowsMainService.ts @@ -6,6 +6,7 @@ import { BrowserWindow, BrowserWindowConstructorOptions, WebContents, app } from 'electron'; import { Event } from 'vs/base/common/event'; import { FileAccess } from 'vs/base/common/network'; +import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { AuxiliaryWindow, IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow'; import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -26,7 +27,7 @@ export class AuxiliaryWindowsMainService implements IAuxiliaryWindowsMainService private registerListeners(): void { // We have to ensure that an auxiliary window gets to know its - // parent `BrowserWindow` so that it can apply listeners to it + // containing `BrowserWindow` so that it can apply listeners to it // Unfortunately we cannot rely on static `BrowserWindow` methods // because we might call the methods too early before the window // is created. @@ -37,6 +38,15 @@ export class AuxiliaryWindowsMainService implements IAuxiliaryWindowsMainService auxiliaryWindow.tryClaimWindow(); } }); + + validatedIpcMain.handle('vscode:registerAuxiliaryWindow', async (event, mainWindowId: number) => { + const auxiliaryWindow = this.getWindowById(event.sender.id); + if (auxiliaryWindow) { + auxiliaryWindow.parentId = mainWindowId; + } + + return event.sender.id; + }); } createWindow(): BrowserWindowConstructorOptions { diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index f2424c81382fb..78eb3f1e00a37 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -117,7 +117,11 @@ export class Menubar { private addFallbackHandlers(): void { // File Menu Items - this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id }); + this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => { + if (!this.runActionInRenderer({ type: 'commandId', commandId: 'workbench.action.files.newUntitledFile' })) { // this is one of the few supported actions when aux window has focus + this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id }); + } + }; this.fallbackMenuHandlers['workbench.action.newWindow'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win?.id }); this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFileFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); this.fallbackMenuHandlers['workbench.action.files.openFolder'] = (menuItem, win, event) => this.nativeHostMainService.pickFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); @@ -750,13 +754,24 @@ export class Menubar { }; } - private runActionInRenderer(invocation: IMenuItemInvocation): void { + private runActionInRenderer(invocation: IMenuItemInvocation): boolean { + + // We want to support auxililary windows that may have focus by + // returning their parent windows as target to support running + // actions via the main window. + let activeBrowserWindow = BrowserWindow.getFocusedWindow(); + if (activeBrowserWindow) { + const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowById(activeBrowserWindow.id); + if (auxiliaryWindowCandidate) { + activeBrowserWindow = this.windowsMainService.getWindowById(auxiliaryWindowCandidate.parentId)?.win ?? null; + } + } + // We make sure to not run actions when the window has no focus, this helps // for https://github.com/microsoft/vscode/issues/25907 and specifically for // https://github.com/microsoft/vscode/issues/11928 // Still allow to run when the last active window is minimized though for // https://github.com/microsoft/vscode/issues/63000 - let activeBrowserWindow = BrowserWindow.getFocusedWindow(); if (!activeBrowserWindow) { const lastActiveWindow = this.windowsMainService.getLastActiveWindow(); if (lastActiveWindow?.isMinimized()) { @@ -773,7 +788,7 @@ export class Menubar { // prevent this action from running twice on macOS (https://github.com/microsoft/vscode/issues/62719) // we already register a keybinding in bootstrap-window.js for opening developer tools in case something // goes wrong and that keybinding is only removed when the application has loaded (= window ready). - return; + return false; } } @@ -784,8 +799,12 @@ export class Menubar { const runKeybindingPayload: INativeRunKeybindingInWindowRequest = { userSettingsLabel: invocation.userSettingsLabel }; activeWindow.sendWhenReady('vscode:runKeybinding', CancellationToken.None, runKeybindingPayload); } + + return true; } else { this.logService.trace('menubar#runActionInRenderer: no active window found', invocation); + + return false; } } diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index e871ae529742a..eb51b35529554 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -39,8 +39,8 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.create(auxiliaryWindow, disposables); } - protected override resolveWindowId(auxiliaryWindow: NativeAuxiliaryWindow): Promise { - return auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:getWindowId'); + protected override resolveWindowId(auxiliaryWindow: NativeAuxiliaryWindow): Promise { + return auxiliaryWindow.vscode.ipcRenderer.invoke('vscode:registerAuxiliaryWindow', this.nativeHostService.windowId); } protected override async patchMethods(auxiliaryWindow: NativeAuxiliaryWindow): Promise { From c411f24dd1249da7d6d8fed21f6ca4650886bb92 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 29 Oct 2023 14:41:32 +0100 Subject: [PATCH 10/13] aux window - ensure "Open Recent" works when aux window has focus (fix #195876) --- .../electron-main/windowsMainService.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 89b2e26c6459c..a2f3527ee5045 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -54,6 +54,8 @@ import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataPro import { IPolicyService } from 'vs/platform/policy/common/policy'; import { IUserDataProfilesMainService } from 'vs/platform/userDataProfile/electron-main/userDataProfile'; import { ILoggerMainService } from 'vs/platform/log/electron-main/loggerService'; +import { IAuxiliaryWindowsMainService } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindows'; +import { IAuxiliaryWindow } from 'vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow'; //#region Helper Interfaces @@ -216,7 +218,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic @IDialogMainService private readonly dialogMainService: IDialogMainService, @IFileService private readonly fileService: IFileService, @IProtocolMainService private readonly protocolMainService: IProtocolMainService, - @IThemeMainService private readonly themeMainService: IThemeMainService + @IThemeMainService private readonly themeMainService: IThemeMainService, + @IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService ) { super(); @@ -636,7 +639,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private doOpenFilesInExistingWindow(configuration: IOpenConfiguration, window: ICodeWindow, filesToOpen?: IFilesToOpen): ICodeWindow { this.logService.trace('windowsManager#doOpenFilesInExistingWindow', { filesToOpen }); - window.focus(); // make sure window has focus + this.focusMainOrChildWindow(window); // make sure window or any of the children has focus const params: INativeOpenFileRequest = { filesToOpenOrCreate: filesToOpen?.filesToOpenOrCreate, @@ -650,6 +653,20 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return window; } + private focusMainOrChildWindow(mainWindow: ICodeWindow): void { + let windowToFocus: ICodeWindow | IAuxiliaryWindow = mainWindow; + + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow && focusedWindow.id !== mainWindow.id) { + const auxiliaryWindowCandidate = this.auxiliaryWindowsMainService.getWindowById(focusedWindow.id); + if (auxiliaryWindowCandidate && auxiliaryWindowCandidate.parentId === mainWindow.id) { + windowToFocus = auxiliaryWindowCandidate; + } + } + + windowToFocus.focus(); + } + private doAddFoldersToExistingWindow(window: ICodeWindow, foldersToAdd: URI[]): ICodeWindow { this.logService.trace('windowsManager#doAddFoldersToExistingWindow', { foldersToAdd }); From e93bfea53b0ef2a3d027730fe085c037963cc871 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 30 Oct 2023 06:21:32 +0100 Subject: [PATCH 11/13] aux window - first cut of window switcher support (fix #196912) --- .../contrib/storageDataCleaner.ts | 2 +- .../electron-main/auxiliaryWindow.ts | 6 ++ src/vs/platform/native/common/native.ts | 9 +-- .../electron-main/nativeHostMainService.ts | 38 ++++++---- src/vs/platform/window/common/window.ts | 13 +++- .../platform/window/electron-main/window.ts | 12 ++-- .../windows/electron-main/windowImpl.ts | 70 +++++++++---------- .../electron-sandbox/actions/windowActions.ts | 70 ++++++++++++++++--- src/vs/workbench/electron-sandbox/window.ts | 35 +++++++--- .../editor/common/editorGroupsService.ts | 5 ++ .../workspaceEditingService.ts | 2 +- .../electron-sandbox/workbenchTestServices.ts | 4 +- 12 files changed, 183 insertions(+), 83 deletions(-) diff --git a/src/vs/code/node/sharedProcess/contrib/storageDataCleaner.ts b/src/vs/code/node/sharedProcess/contrib/storageDataCleaner.ts index da67be66109e8..23af4d227b94f 100644 --- a/src/vs/code/node/sharedProcess/contrib/storageDataCleaner.ts +++ b/src/vs/code/node/sharedProcess/contrib/storageDataCleaner.ts @@ -52,7 +52,7 @@ export class UnusedWorkspaceStorageDataCleaner extends Disposable { return; // keep workspace storage for empty extension development workspaces } - const windows = await this.nativeHostService.getWindows(); + const windows = await this.nativeHostService.getWindows({ includeAuxiliaryWindows: false }); if (windows.some(window => window.workspace?.id === workspaceStorageFolder)) { return; // keep workspace storage for empty workspaces opened as window } diff --git a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts index 97ee7fc0ac1b2..a73b50a66830d 100644 --- a/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts +++ b/src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts @@ -20,6 +20,12 @@ export interface IAuxiliaryWindow { readonly lastFocusTime: number; focus(options?: { force: boolean }): void; + + setRepresentedFilename(name: string): void; + getRepresentedFilename(): string | undefined; + + setDocumentEdited(edited: boolean): void; + isDocumentEdited(): boolean; } export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 6b2d0f538b494..59218bc0fe846 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -12,7 +12,7 @@ import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; -import { IColorScheme, IOpenedWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; +import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; export interface ICPUProperties { model: string; @@ -60,7 +60,8 @@ export interface ICommonNativeHostService { readonly onDidTriggerSystemContextMenu: Event<{ windowId: number; x: number; y: number }>; // Window - getWindows(): Promise; + getWindows(options: { includeAuxiliaryWindows: true }): Promise>; + getWindows(options: { includeAuxiliaryWindows: false }): Promise>; getWindowCount(): Promise; getActiveWindowId(): Promise; @@ -111,8 +112,8 @@ export interface ICommonNativeHostService { // OS showItemInFolder(path: string): Promise; - setRepresentedFilename(path: string): Promise; - setDocumentEdited(edited: boolean): Promise; + setRepresentedFilename(path: string, options?: { targetWindowId?: number }): Promise; + setDocumentEdited(edited: boolean, options?: { targetWindowId?: number }): Promise; openExternal(url: string): Promise; moveItemToTrash(fullPath: string): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 92855b5ae2441..43b3fd9591778 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -33,7 +33,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { ICodeWindow } from 'vs/platform/window/electron-main/window'; -import { IColorScheme, IOpenedWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; +import { IColorScheme, IOpenedAuxiliaryWindow, IOpenedMainWindow, IOpenEmptyWindowOptions, IOpenWindowOptions, IRectangle, IWindowOpenable } from 'vs/platform/window/common/window'; import { getFocusedOrLastActiveWindow, IWindowsMainService, OpenContext } from 'vs/platform/windows/electron-main/windows'; import { isWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; @@ -112,16 +112,28 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#region Window - async getWindows(): Promise { - const windows = this.windowsMainService.getWindows(); - - return windows.map(window => ({ + getWindows(windowId: number | undefined, options: { includeAuxiliaryWindows: true }): Promise>; + getWindows(windowId: number | undefined, options: { includeAuxiliaryWindows: false }): Promise>; + async getWindows(windowId: number | undefined, options: { includeAuxiliaryWindows: boolean }): Promise> { + const mainWindows = this.windowsMainService.getWindows().map(window => ({ id: window.id, workspace: window.openedWorkspace ?? toWorkspaceIdentifier(window.backupPath, window.isExtensionDevelopmentHost), title: window.win?.getTitle() ?? '', filename: window.getRepresentedFilename(), dirty: window.isDocumentEdited() })); + + const auxiliaryWindows = []; + if (options.includeAuxiliaryWindows) { + auxiliaryWindows.push(...this.auxiliaryWindowsMainService.getWindows().map(window => ({ + id: window.id, + parentId: window.parentId, + title: window.win?.getTitle() ?? '', + filename: window.getRepresentedFilename() + }))); + } + + return [...mainWindows, ...auxiliaryWindows]; } async getWindowCount(windowId: number | undefined): Promise { @@ -218,7 +230,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } async moveWindowTop(windowId: number | undefined, options?: { targetWindowId?: number }): Promise { - const window = this.windowById(options?.targetWindowId) ?? this.codeWindowById(windowId); + const window = this.windowById(options?.targetWindowId, windowId); if (window?.win) { window.win.moveTop(); } @@ -245,7 +257,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } async focusWindow(windowId: number | undefined, options?: { targetWindowId?: number; force?: boolean }): Promise { - const window = this.windowById(options?.targetWindowId) ?? this.codeWindowById(windowId); + const window = this.windowById(options?.targetWindowId, windowId); window?.focus({ force: options?.force ?? false }); } @@ -439,13 +451,13 @@ export class NativeHostMainService extends Disposable implements INativeHostMain shell.showItemInFolder(path); } - async setRepresentedFilename(windowId: number | undefined, path: string): Promise { - const window = this.codeWindowById(windowId); + async setRepresentedFilename(windowId: number | undefined, path: string, options?: { targetWindowId?: number }): Promise { + const window = this.windowById(options?.targetWindowId, windowId); window?.setRepresentedFilename(path); } - async setDocumentEdited(windowId: number | undefined, edited: boolean): Promise { - const window = this.codeWindowById(windowId); + async setDocumentEdited(windowId: number | undefined, edited: boolean, options?: { targetWindowId?: number }): Promise { + const window = this.windowById(options?.targetWindowId, windowId); window?.setDocumentEdited(edited); } @@ -797,8 +809,8 @@ export class NativeHostMainService extends Disposable implements INativeHostMain //#endregion - private windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined { - return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId); + private windowById(windowId: number | undefined, fallbackCodeWindowId?: number): ICodeWindow | IAuxiliaryWindow | undefined { + return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId) ?? this.codeWindowById(fallbackCodeWindowId); } private codeWindowById(windowId: number | undefined): ICodeWindow | undefined { diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 4e7a1872b0eac..5648bec827903 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -70,7 +70,7 @@ export interface IAddFoldersRequest { readonly foldersToAdd: UriComponents[]; } -export interface IOpenedWindow { +export interface IOpenedMainWindow { readonly id: number; readonly workspace?: IAnyWorkspaceIdentifier; readonly title: string; @@ -78,6 +78,17 @@ export interface IOpenedWindow { readonly dirty: boolean; } +export interface IOpenedAuxiliaryWindow { + readonly id: number; + readonly parentId: number; + readonly title: string; + readonly filename?: string; +} + +export function isOpenedAuxiliaryWindow(candidate: IOpenedMainWindow | IOpenedAuxiliaryWindow): candidate is IOpenedAuxiliaryWindow { + return typeof (candidate as IOpenedAuxiliaryWindow).parentId === 'number'; +} + export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { } export type IWindowOpenable = IWorkspaceToOpen | IFolderToOpen | IFileToOpen; diff --git a/src/vs/platform/window/electron-main/window.ts b/src/vs/platform/window/electron-main/window.ts index 379bdd161f63e..ac0eda4c73e6d 100644 --- a/src/vs/platform/window/electron-main/window.ts +++ b/src/vs/platform/window/electron-main/window.ts @@ -15,6 +15,12 @@ import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platf export interface IBaseWindow extends IDisposable { focus(options?: { force: boolean }): void; + + setRepresentedFilename(name: string): void; + getRepresentedFilename(): string | undefined; + + setDocumentEdited(edited: boolean): void; + isDocumentEdited(): boolean; } export interface ICodeWindow extends IBaseWindow { @@ -65,12 +71,6 @@ export interface ICodeWindow extends IBaseWindow { isMinimized(): boolean; - setRepresentedFilename(name: string): void; - getRepresentedFilename(): string | undefined; - - setDocumentEdited(edited: boolean): void; - isDocumentEdited(): boolean; - handleTitleDoubleClick(): void; updateTouchBar(items: ISerializableCommandAction[][]): void; diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 3b87cdf60856d..2bdd624c092dc 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -86,6 +86,41 @@ export abstract class BaseWindow extends Disposable implements IBaseWindow { protected abstract getWin(): BrowserWindow | null; + private representedFilename: string | undefined; + private documentEdited: boolean | undefined; + + setRepresentedFilename(filename: string): void { + if (isMacintosh) { + this.getWin()?.setRepresentedFilename(filename); + } else { + this.representedFilename = filename; + } + } + + getRepresentedFilename(): string | undefined { + if (isMacintosh) { + return this.getWin()?.getRepresentedFilename(); + } + + return this.representedFilename; + } + + setDocumentEdited(edited: boolean): void { + if (isMacintosh) { + this.getWin()?.setDocumentEdited(edited); + } + + this.documentEdited = edited; + } + + isDocumentEdited(): boolean { + if (isMacintosh) { + return Boolean(this.getWin()?.isDocumentEdited()); + } + + return !!this.documentEdited; + } + focus(options?: { force: boolean }): void { if (isMacintosh && options?.force) { app.focus({ steal: true }); @@ -182,9 +217,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { private transientIsNativeFullScreen: boolean | undefined = undefined; private joinNativeFullScreenTransition: DeferredPromise | undefined = undefined; - private representedFilename: string | undefined; - private documentEdited: boolean | undefined; - private readonly hasWindowControlOverlay: boolean = false; private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[] = []; @@ -402,38 +434,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { this.registerListeners(); } - setRepresentedFilename(filename: string): void { - if (isMacintosh) { - this._win.setRepresentedFilename(filename); - } else { - this.representedFilename = filename; - } - } - - getRepresentedFilename(): string | undefined { - if (isMacintosh) { - return this._win.getRepresentedFilename(); - } - - return this.representedFilename; - } - - setDocumentEdited(edited: boolean): void { - if (isMacintosh) { - this._win.setDocumentEdited(edited); - } - - this.documentEdited = edited; - } - - isDocumentEdited(): boolean { - if (isMacintosh) { - return this._win.isDocumentEdited(); - } - - return !!this.documentEdited; - } - private readyState = ReadyState.NONE; setReady(): void { diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 930279405895f..2e1902cd78729 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -12,7 +12,7 @@ import { getZoomLevel } from 'vs/base/browser/browser'; import { FileKind } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { IQuickInputService, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickInputButton, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -31,6 +31,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { getActiveWindow } from 'vs/base/browser/dom'; import { isAuxiliaryWindow } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { IOpenedAuxiliaryWindow, IOpenedMainWindow, isOpenedAuxiliaryWindow } from 'vs/platform/window/common/window'; export class CloseWindowAction extends Action2 { @@ -216,23 +217,70 @@ abstract class BaseSwitchWindow extends Action2 { const languageService = accessor.get(ILanguageService); const nativeHostService = accessor.get(INativeHostService); - const currentWindowId = nativeHostService.windowId; + let currentWindowId: number; + const activeWindow = getActiveWindow(); + if (isAuxiliaryWindow(activeWindow)) { + currentWindowId = activeWindow.vscodeWindowId; + } else { + currentWindowId = nativeHostService.windowId; + } + + const windows = await nativeHostService.getWindows({ includeAuxiliaryWindows: true }); + + const mainWindows = new Set(); + const mapMainWindowToAuxiliaryWindows = new Map>(); + for (const window of windows) { + if (isOpenedAuxiliaryWindow(window)) { + let auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.parentId); + if (!auxiliaryWindows) { + auxiliaryWindows = new Set(); + mapMainWindowToAuxiliaryWindows.set(window.parentId, auxiliaryWindows); + } + auxiliaryWindows.add(window); + } else { + mainWindows.add(window); + } + } + + interface IWindowPickItem extends IQuickPickItem { + readonly windowId: number; + } + + const picks: Array = []; + for (const window of mainWindows) { + const auxiliaryWindows = mapMainWindowToAuxiliaryWindows.get(window.id); + if (mapMainWindowToAuxiliaryWindows.size > 0) { + picks.push({ type: 'separator', payload: -1, label: auxiliaryWindows ? localize('windowGroup', "Window Group") : undefined } as unknown as IWindowPickItem); + } - const windows = await nativeHostService.getWindows(); - const placeHolder = localize('switchWindowPlaceHolder', "Select a window to switch to"); - const picks = windows.map(window => { const resource = window.filename ? URI.file(window.filename) : isSingleFolderWorkspaceIdentifier(window.workspace) ? window.workspace.uri : isWorkspaceIdentifier(window.workspace) ? window.workspace.configPath : undefined; const fileKind = window.filename ? FileKind.FILE : isSingleFolderWorkspaceIdentifier(window.workspace) ? FileKind.FOLDER : isWorkspaceIdentifier(window.workspace) ? FileKind.ROOT_FOLDER : FileKind.FILE; - return { - payload: window.id, + const pick: IWindowPickItem = { + windowId: window.id, label: window.title, ariaLabel: window.dirty ? localize('windowDirtyAriaLabel', "{0}, window with unsaved changes", window.title) : window.title, iconClasses: getIconClasses(modelService, languageService, resource, fileKind), description: (currentWindowId === window.id) ? localize('current', "Current Window") : undefined, buttons: currentWindowId !== window.id ? window.dirty ? [this.closeDirtyWindowAction] : [this.closeWindowAction] : undefined }; - }); - const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.payload === currentWindowId)[0]) + 1) % picks.length; + picks.push(pick); + + if (auxiliaryWindows) { + for (const auxiliaryWindow of auxiliaryWindows) { + const pick: IWindowPickItem = { + windowId: auxiliaryWindow.id, + label: auxiliaryWindow.title, + iconClasses: getIconClasses(modelService, languageService, auxiliaryWindow.filename ? URI.file(auxiliaryWindow.filename) : undefined, FileKind.FILE), + description: (currentWindowId === auxiliaryWindow.id) ? localize('current', "Current Window") : undefined, + buttons: [this.closeWindowAction] + }; + picks.push(pick); + } + } + } + + const placeHolder = localize('switchWindowPlaceHolder', "Select a window to switch to"); + const autoFocusIndex = (picks.indexOf(picks.filter(pick => pick.windowId === currentWindowId)[0]) + 1) % picks.length; const pick = await quickInputService.pick(picks, { contextKey: 'inWindowsPicker', @@ -241,13 +289,13 @@ abstract class BaseSwitchWindow extends Action2 { quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, hideInput: this.isQuickNavigate(), onDidTriggerItemButton: async context => { - await nativeHostService.closeWindowById(context.item.payload); + await nativeHostService.closeWindowById(context.item.windowId); context.removeItem(); } }); if (pick) { - nativeHostService.focusWindow({ targetWindowId: pick.payload }); + nativeHostService.focusWindow({ targetWindowId: pick.windowId }); } } } diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 0f5e52f53429b..93f2c0e3c840f 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -70,6 +70,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IUtilityProcessWorkerWorkbenchService } from 'vs/workbench/services/utilityProcess/electron-sandbox/utilityProcessWorkerWorkbenchService'; import { registerWindowDriver } from 'vs/workbench/services/driver/electron-sandbox/driver'; +import { IAuxiliaryWindowService } from 'vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService'; export class NativeWindow extends Disposable { @@ -86,6 +87,8 @@ export class NativeWindow extends Disposable { private isDocumentedEdited = false; + private readonly mainPartEditorService: IEditorService; + constructor( @IEditorService private readonly editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @@ -122,10 +125,13 @@ export class NativeWindow extends Disposable { @IBannerService private readonly bannerService: IBannerService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IPreferencesService private readonly preferencesService: IPreferencesService, - @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService + @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, + @IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService ) { super(); + this.mainPartEditorService = editorService.createScoped('main', this._store); + this.registerListeners(); this.create(); } @@ -334,7 +340,7 @@ export class NativeWindow extends Disposable { })); // Listen to visible editor changes - this._register(this.editorService.onDidVisibleEditorsChange(() => this.onDidChangeVisibleEditors())); + this._register(this.mainPartEditorService.onDidVisibleEditorsChange(() => this.onDidChangeVisibleEditors())); // Listen to editor closing (if we run with --wait) const filesToWait = this.environmentService.filesToWait; @@ -344,14 +350,25 @@ export class NativeWindow extends Disposable { // macOS OS integration if (isMacintosh) { - this._register(this.editorService.onDidActiveEditorChange(() => { - const file = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, filterByScheme: Schemas.file }); + const updateRepresentedFilename = (editorService: IEditorService, targetWindowId: number | undefined) => { + const file = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, filterByScheme: Schemas.file }); // Represented Filename - this.nativeHostService.setRepresentedFilename(file?.fsPath ?? ''); + this.nativeHostService.setRepresentedFilename(file?.fsPath ?? '', { targetWindowId }); - // Custom title menu - this.provideCustomTitleContextMenu(file?.fsPath); + // Custom title menu (main window only currently) + if (typeof targetWindowId !== 'number') { + this.provideCustomTitleContextMenu(file?.fsPath); + } + }; + + this._register(this.mainPartEditorService.onDidActiveEditorChange(() => updateRepresentedFilename(this.mainPartEditorService, undefined))); + + this._register(this.auxiliaryWindowService.onDidOpenAuxiliaryWindow(({ window, disposables }) => { + const auxiliaryWindowEditorPart = this.editorGroupService.getPart(window.container); + if (auxiliaryWindowEditorPart) { + disposables.add(this.editorService.createScoped(auxiliaryWindowEditorPart, disposables).onDidActiveEditorChange(() => updateRepresentedFilename(this.mainPartEditorService, window.window.vscodeWindowId))); + } })); } @@ -585,7 +602,7 @@ export class NativeWindow extends Disposable { // Close when empty: check if we should close the window based on the setting // Overruled by: window has a workspace opened or this window is for extension development // or setting is disabled. Also enabled when running with --wait from the command line. - const visibleEditorPanes = this.editorService.visibleEditorPanes; + const visibleEditorPanes = this.mainPartEditorService.visibleEditorPanes; if (visibleEditorPanes.length === 0 && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY && !this.environmentService.isExtensionDevelopment) { const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty'); if (closeWhenEmpty || this.environmentService.args.wait) { @@ -595,7 +612,7 @@ export class NativeWindow extends Disposable { } private onDidAllEditorsClose(): void { - const visibleEditorPanes = this.editorService.visibleEditorPanes.length; + const visibleEditorPanes = this.mainPartEditorService.visibleEditorPanes.length; if (visibleEditorPanes === 0) { this.nativeHostService.closeWindow(); } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index ba0188a48b873..047a7e177c603 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -492,6 +492,11 @@ export interface IEditorGroupsService extends IEditorGroupsContainer { */ getPart(group: IEditorGroup | GroupIdentifier): IEditorPart; + /** + * Get the editor part that is rooted in the provided container. + */ + getPart(container: unknown /* HTMLElement */): IEditorPart | undefined; + /** * Opens a new window with a full editor part instantiated * in there at the optional position on screen. diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index a354835131419..ad00669d146cd 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -138,7 +138,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi } override async isValidTargetWorkspacePath(workspaceUri: URI): Promise { - const windows = await this.nativeHostService.getWindows(); + const windows = await this.nativeHostService.getWindows({ includeAuxiliaryWindows: false }); // Prevent overwriting a workspace that is currently opened in another window if (windows.some(window => isWorkspaceIdentifier(window.workspace) && this.uriIdentityService.extUri.isEqual(window.workspace.configPath, workspaceUri))) { diff --git a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts index a6e43ee500099..c378338205760 100644 --- a/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-sandbox/workbenchTestServices.ts @@ -12,7 +12,7 @@ import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IFileDialogService, INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; -import { IOpenedWindow, IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions, IColorScheme, IRectangle } from 'vs/platform/window/common/window'; +import { IOpenedMainWindow, IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions, IColorScheme, IRectangle } from 'vs/platform/window/common/window'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -78,7 +78,7 @@ export class TestNativeHostService implements INativeHostService { windowCount = Promise.resolve(1); getWindowCount(): Promise { return this.windowCount; } - async getWindows(): Promise { return []; } + async getWindows(): Promise { return []; } async getActiveWindowId(): Promise { return undefined; } openWindow(options?: IOpenEmptyWindowOptions): Promise; From fdf12e8adc069d471563ce9c6c207a775f7c6154 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 30 Oct 2023 06:32:50 +0100 Subject: [PATCH 12/13] aux window - properly implement `ILayoutService.focus()` (fix #196763) --- src/vs/platform/layout/browser/layoutService.ts | 2 +- src/vs/workbench/browser/layout.ts | 9 ++++++++- .../auxiliaryWindow/browser/auxiliaryWindowService.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/layout/browser/layoutService.ts b/src/vs/platform/layout/browser/layoutService.ts index 74d4ef4b801ac..8ba84c5d9e532 100644 --- a/src/vs/platform/layout/browser/layoutService.ts +++ b/src/vs/platform/layout/browser/layoutService.ts @@ -92,7 +92,7 @@ export interface ILayoutService { readonly activeContainerOffset: ILayoutOffsetInfo; /** - * Focus the primary component of the container. + * Focus the primary component of the active container. */ focus(): void; } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 9eb840c07071c..19553fffaa94e 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1230,7 +1230,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } focus(): void { - this.focusPart(Parts.EDITOR_PART); + const activeContainer = this.activeContainer; + if (activeContainer === this.container) { + // main window + this.focusPart(Parts.EDITOR_PART); + } else { + // auxiliary window + this.editorGroupService.getPart(activeContainer)?.activeGroup.focus(); + } } getDimension(part: Parts): Dimension | undefined { diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index 86782a5b6fbdc..909a16a2afd59 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -268,7 +268,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili onDidLayout.fire(dimension); })); - this._register(addDisposableListener(container, EventType.SCROLL, () => container.scrollTop = 0)); // // Prevent container from scrolling (#55456) + this._register(addDisposableListener(container, EventType.SCROLL, () => container.scrollTop = 0)); // Prevent container from scrolling (#55456) if (isWeb) { disposables.add(addDisposableListener(container, EventType.DROP, e => EventHelper.stop(e, true))); // Prevent default navigation on drop From 99d59d3985f48feb6f5b29939d6ea636170febfe Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 30 Oct 2023 07:54:51 +0100 Subject: [PATCH 13/13] aux window - :lipstick: --- src/vs/base/browser/dom.ts | 7 ++++--- src/vs/platform/menubar/electron-main/menubar.ts | 12 ++++++------ src/vs/platform/window/common/window.ts | 12 ++++++------ src/vs/workbench/browser/parts/editor/editorPart.ts | 4 ++-- src/vs/workbench/electron-sandbox/window.ts | 3 ++- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index c398b0afc51cf..13ff65cfb4eb6 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -791,8 +791,6 @@ export function focusWindow(element: Node): void { } } -const globalStylesheets = new Map>(); - export function createStyleSheet(container: HTMLElement = document.head, beforeAppend?: (style: HTMLStyleElement) => void, disposableStore?: DisposableStore): HTMLStyleElement { const style = document.createElement('style'); style.type = 'text/css'; @@ -827,6 +825,8 @@ export function createStyleSheet(container: HTMLElement = document.head, beforeA return style; } +const globalStylesheets = new Map>(); + export function isGlobalStylesheet(node: Node): boolean { return globalStylesheets.has(node as HTMLStyleElement); } @@ -905,7 +905,6 @@ export const sharedMutationObserver = new class { observer.disconnect(); mutationObserversPerTarget?.delete(optionsHash); - if (mutationObserversPerTarget?.size === 0) { this.mutationObservers.delete(target); } @@ -913,6 +912,8 @@ export const sharedMutationObserver = new class { })); mutationObserversPerTarget.set(optionsHash, mutationObserverPerOptions); + } else { + mutationObserverPerOptions.users += 1; } return mutationObserverPerOptions.onDidMutate; diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 78eb3f1e00a37..ed5141b1d0a3e 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -49,7 +49,7 @@ export class Menubar { private willShutdown: boolean | undefined; private appMenuInstalled: boolean | undefined; private closedLastWindow: boolean; - private noActiveWindow: boolean; + private noActiveMainWindow: boolean; private menuUpdater: RunOnceScheduler; private menuGC: RunOnceScheduler; @@ -92,7 +92,7 @@ export class Menubar { this.addFallbackHandlers(); this.closedLastWindow = false; - this.noActiveWindow = false; + this.noActiveMainWindow = false; this.oldMenus = []; @@ -247,7 +247,7 @@ export class Menubar { } const focusedWindow = BrowserWindow.getFocusedWindow(); - this.noActiveWindow = !focusedWindow || !!this.auxiliaryWindowsMainService.getWindowById(focusedWindow.id); + this.noActiveMainWindow = !focusedWindow || !!this.auxiliaryWindowsMainService.getWindowById(focusedWindow.id); this.scheduleUpdateMenu(); } @@ -477,12 +477,12 @@ export class Menubar { case 'File': case 'Help': if (isMacintosh) { - return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveWindow) || (!!this.menubarMenus && !!this.menubarMenus[menuId]); + return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow) || (!!this.menubarMenus && !!this.menubarMenus[menuId]); } case 'Window': if (isMacintosh) { - return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveWindow) || !!this.menubarMenus; + return (this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || (this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow) || !!this.menubarMenus; } default: @@ -509,7 +509,7 @@ export class Menubar { if (isMacintosh) { if ((this.windowsMainService.getWindowCount() === 0 && this.closedLastWindow) || - (this.windowsMainService.getWindowCount() > 0 && this.noActiveWindow)) { + (this.windowsMainService.getWindowCount() > 0 && this.noActiveMainWindow)) { // In the fallback scenario, we are either disabled or using a fallback handler if (this.fallbackMenuHandlers[item.id]) { menu.append(new MenuItem(this.likeAction(item.id, { label: this.mnemonicLabel(item.label), click: this.fallbackMenuHandlers[item.id] }))); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 5648bec827903..442c421ef5ef3 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -70,19 +70,19 @@ export interface IAddFoldersRequest { readonly foldersToAdd: UriComponents[]; } -export interface IOpenedMainWindow { +interface IOpenedWindow { readonly id: number; - readonly workspace?: IAnyWorkspaceIdentifier; readonly title: string; readonly filename?: string; +} + +export interface IOpenedMainWindow extends IOpenedWindow { + readonly workspace?: IAnyWorkspaceIdentifier; readonly dirty: boolean; } -export interface IOpenedAuxiliaryWindow { - readonly id: number; +export interface IOpenedAuxiliaryWindow extends IOpenedWindow { readonly parentId: number; - readonly title: string; - readonly filename?: string; } export function isOpenedAuxiliaryWindow(candidate: IOpenedMainWindow | IOpenedAuxiliaryWindow): candidate is IOpenedAuxiliaryWindow { diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index bc3c683d128b4..57e9e784a1b7f 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -756,10 +756,10 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupsView { } // Remove group with editors - this.doRemoveGroupWithEditors(groupView, preserveFocus); + this.doRemoveGroupWithEditors(groupView); } - private doRemoveGroupWithEditors(groupView: IEditorGroupView, preserveFocus?: boolean): void { + private doRemoveGroupWithEditors(groupView: IEditorGroupView): void { const mostRecentlyActiveGroups = this.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); let lastActiveGroup: IEditorGroupView; diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 93f2c0e3c840f..bddfc483eb496 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -367,7 +367,8 @@ export class NativeWindow extends Disposable { this._register(this.auxiliaryWindowService.onDidOpenAuxiliaryWindow(({ window, disposables }) => { const auxiliaryWindowEditorPart = this.editorGroupService.getPart(window.container); if (auxiliaryWindowEditorPart) { - disposables.add(this.editorService.createScoped(auxiliaryWindowEditorPart, disposables).onDidActiveEditorChange(() => updateRepresentedFilename(this.mainPartEditorService, window.window.vscodeWindowId))); + const auxiliaryEditorService = this.editorService.createScoped(auxiliaryWindowEditorPart, disposables); + disposables.add(auxiliaryEditorService.onDidActiveEditorChange(() => updateRepresentedFilename(auxiliaryEditorService, window.window.vscodeWindowId))); } })); }