mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-02 22:41:31 +01:00
aux window: add barrier waiting for styles to load (#199882)
* wip * aux window: add barrier waiting for styles to load The debug toolbar needs to compute its size and positioning, which without this fails if executed in onDidChangeActiveContainer. The result of these changes is a new property on the ILayoutService. * address comments * address comments --------- Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
This commit is contained in:
@@ -426,7 +426,6 @@ export class ThrottledDelayer<T> {
|
||||
* A barrier that is initially closed and then becomes opened permanently.
|
||||
*/
|
||||
export class Barrier {
|
||||
|
||||
private _isOpen: boolean;
|
||||
private _promise: Promise<boolean>;
|
||||
private _completePromise!: (v: boolean) => void;
|
||||
|
||||
@@ -50,6 +50,7 @@ class EditorScopedQuickInputService extends QuickInputService {
|
||||
get onDidLayoutContainer() { return Event.map(editor.onDidLayoutChange, dimension => ({ container: widget.getDomNode(), dimension })); },
|
||||
get onDidChangeActiveContainer() { return Event.None; },
|
||||
get onDidAddContainer() { return Event.None; },
|
||||
get whenActiveContainerStylesLoaded() { return Promise.resolve(); },
|
||||
get mainContainerOffset() { return { top: 0, quickPickTop: 0 }; },
|
||||
get activeContainerOffset() { return { top: 0, quickPickTop: 0 }; },
|
||||
focus: () => editor.focus()
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { mainWindow } from 'vs/base/browser/window';
|
||||
import { coalesce, firstOrDefault } from 'vs/base/common/arrays';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ILayoutService, ILayoutOffsetInfo } from 'vs/platform/layout/browser/layoutService';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { coalesce, firstOrDefault } from 'vs/base/common/arrays';
|
||||
import { mainWindow } from 'vs/base/browser/window';
|
||||
import { ILayoutOffsetInfo, ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
|
||||
class StandaloneLayoutService implements ILayoutService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
@@ -19,6 +19,7 @@ class StandaloneLayoutService implements ILayoutService {
|
||||
readonly onDidLayoutContainer = Event.None;
|
||||
readonly onDidChangeActiveContainer = Event.None;
|
||||
readonly onDidAddContainer = Event.None;
|
||||
readonly whenActiveContainerStylesLoaded = Promise.resolve();
|
||||
|
||||
get mainContainer(): HTMLElement {
|
||||
return firstOrDefault(this._codeEditorService.listCodeEditors())?.getContainerDomNode() ?? mainWindow.document.body;
|
||||
|
||||
@@ -94,6 +94,13 @@ export interface ILayoutService {
|
||||
*/
|
||||
readonly activeContainerOffset: ILayoutOffsetInfo;
|
||||
|
||||
/**
|
||||
* A promise resolved when the stylesheets for the active container have been
|
||||
* loaded. Aux windows load their styles asynchronously, so there may be
|
||||
* an initial delay before resolution happens.
|
||||
*/
|
||||
readonly whenActiveContainerStylesLoaded: Promise<void>;
|
||||
|
||||
/**
|
||||
* Focus the primary component of the active container.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom';
|
||||
import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser';
|
||||
@@ -212,6 +212,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
|
||||
return this.computeContainerOffset(getWindow(this.activeContainer));
|
||||
}
|
||||
|
||||
get whenActiveContainerStylesLoaded() {
|
||||
const active = this.activeContainer;
|
||||
return this.auxWindowStylesLoaded.get(active) || Promise.resolve();
|
||||
}
|
||||
|
||||
private computeContainerOffset(targetWindow: Window) {
|
||||
let top = 0;
|
||||
let quickPickTop = 0;
|
||||
@@ -240,6 +245,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
|
||||
//#endregion
|
||||
|
||||
private readonly parts = new Map<string, Part>();
|
||||
private readonly auxWindowStylesLoaded = new Map</* container */ HTMLElement, Promise<void>>();
|
||||
|
||||
private initialized = false;
|
||||
private workbenchGrid!: SerializableGrid<ISerializableView>;
|
||||
@@ -382,9 +388,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
|
||||
// Auxiliary windows
|
||||
this._register(this.auxiliaryWindowService.onDidOpenAuxiliaryWindow(({ window, disposables }) => {
|
||||
const eventDisposables = disposables.add(new DisposableStore());
|
||||
this.auxWindowStylesLoaded.set(window.container, window.whenStylesHaveLoaded);
|
||||
this._onDidAddContainer.fire({ container: window.container, disposables: eventDisposables });
|
||||
|
||||
disposables.add(window.onDidLayout(dimension => this.handleContainerDidLayout(window.container, dimension)));
|
||||
disposables.add(toDisposable(() => this.auxWindowStylesLoaded.delete(window.container)));
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import Severity from 'vs/base/common/severity';
|
||||
import { BaseWindow } from 'vs/workbench/browser/window';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
|
||||
export const IAuxiliaryWindowService = createDecorator<IAuxiliaryWindowService>('auxiliaryWindowService');
|
||||
|
||||
@@ -45,6 +46,7 @@ export interface IAuxiliaryWindow extends IDisposable {
|
||||
|
||||
readonly onDidLayout: Event<Dimension>;
|
||||
readonly onWillClose: Event<void>;
|
||||
readonly whenStylesHaveLoaded: Promise<void>;
|
||||
|
||||
readonly window: CodeWindow;
|
||||
readonly container: HTMLElement;
|
||||
@@ -63,13 +65,17 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow {
|
||||
private readonly _onWillDispose = this._register(new Emitter<void>());
|
||||
readonly onWillDispose = this._onWillDispose.event;
|
||||
|
||||
readonly whenStylesHaveLoaded: Promise<void>;
|
||||
|
||||
constructor(
|
||||
readonly window: CodeWindow,
|
||||
readonly container: HTMLElement,
|
||||
stylesHaveLoaded: Barrier,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
super(window);
|
||||
|
||||
this.whenStylesHaveLoaded = stylesHaveLoaded.wait().then(() => { });
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
@@ -165,9 +171,9 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili
|
||||
ensureCodeWindow(targetWindow, resolvedWindowId);
|
||||
|
||||
const containerDisposables = new DisposableStore();
|
||||
const container = this.createContainer(targetWindow, containerDisposables);
|
||||
const { container, stylesLoaded } = this.createContainer(targetWindow, containerDisposables);
|
||||
|
||||
const auxiliaryWindow = this.createAuxiliaryWindow(targetWindow, container);
|
||||
const auxiliaryWindow = this.createAuxiliaryWindow(targetWindow, container, stylesLoaded);
|
||||
|
||||
const registryDisposables = new DisposableStore();
|
||||
this.windows.set(targetWindow.vscodeWindowId, auxiliaryWindow);
|
||||
@@ -201,8 +207,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili
|
||||
return auxiliaryWindow;
|
||||
}
|
||||
|
||||
protected createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement): AuxiliaryWindow {
|
||||
return new AuxiliaryWindow(targetWindow, container, this.configurationService);
|
||||
protected createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesLoaded: Barrier): AuxiliaryWindow {
|
||||
return new AuxiliaryWindow(targetWindow, container, stylesLoaded, this.configurationService);
|
||||
}
|
||||
|
||||
private async openWindow(options?: IAuxiliaryWindowOpenOptions): Promise<Window | undefined> {
|
||||
@@ -257,13 +263,13 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili
|
||||
return BrowserAuxiliaryWindowService.WINDOW_IDS++;
|
||||
}
|
||||
|
||||
protected createContainer(auxiliaryWindow: CodeWindow, disposables: DisposableStore): HTMLElement {
|
||||
protected createContainer(auxiliaryWindow: CodeWindow, disposables: DisposableStore): { stylesLoaded: Barrier; container: HTMLElement } {
|
||||
this.patchMethods(auxiliaryWindow);
|
||||
|
||||
this.applyMeta(auxiliaryWindow);
|
||||
this.applyCSS(auxiliaryWindow, disposables);
|
||||
|
||||
return this.applyHTML(auxiliaryWindow, disposables);
|
||||
const { stylesLoaded } = this.applyCSS(auxiliaryWindow, disposables);
|
||||
const container = this.applyHTML(auxiliaryWindow, disposables);
|
||||
return { stylesLoaded, container };
|
||||
}
|
||||
|
||||
protected patchMethods(auxiliaryWindow: CodeWindow): void {
|
||||
@@ -292,24 +298,44 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili
|
||||
}
|
||||
}
|
||||
|
||||
protected applyCSS(auxiliaryWindow: CodeWindow, disposables: DisposableStore): void {
|
||||
protected applyCSS(auxiliaryWindow: CodeWindow, disposables: DisposableStore) {
|
||||
mark('code/auxiliaryWindow/willApplyCSS');
|
||||
|
||||
const mapOriginalToClone = new Map<Node /* original */, Node /* clone */>();
|
||||
|
||||
function cloneNode(originalNode: Node): void {
|
||||
const stylesLoaded = new Barrier();
|
||||
stylesLoaded.wait().then(() => mark('code/auxiliaryWindow/didLoadCSSStyles'));
|
||||
|
||||
let pendingLinkSettles = 0;
|
||||
function onLinkSettled(_event?: globalThis.Event) {
|
||||
// network errors from loading stylesheets will be written to the console
|
||||
// already, we probably don't need to log them manually.
|
||||
if (!--pendingLinkSettles) {
|
||||
stylesLoaded.open();
|
||||
}
|
||||
}
|
||||
|
||||
function cloneNode(originalNode: Element): void {
|
||||
if (isGlobalStylesheet(originalNode)) {
|
||||
return; // global stylesheets are handled by `cloneGlobalStylesheets` below
|
||||
}
|
||||
|
||||
const clonedNode = auxiliaryWindow.document.head.appendChild(originalNode.cloneNode(true));
|
||||
if (originalNode.tagName === 'LINK') {
|
||||
pendingLinkSettles++;
|
||||
disposables.add(addDisposableListener(clonedNode, 'load', onLinkSettled));
|
||||
disposables.add(addDisposableListener(clonedNode, 'error', onLinkSettled));
|
||||
}
|
||||
|
||||
mapOriginalToClone.set(originalNode, clonedNode);
|
||||
}
|
||||
|
||||
// Clone all style elements and stylesheet links from the window to the child window
|
||||
pendingLinkSettles++; // outer increment handles cases where there's nothing to load, and ensures it can't settle prematurely
|
||||
for (const originalNode of mainWindow.document.head.querySelectorAll('link[rel="stylesheet"], style')) {
|
||||
cloneNode(originalNode);
|
||||
}
|
||||
onLinkSettled();
|
||||
|
||||
// Global stylesheets in <head> are cloned in a special way because the mutation
|
||||
// observer is not firing for changes done via `style.sheet` API. Only text changes
|
||||
@@ -356,6 +382,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili
|
||||
}));
|
||||
|
||||
mark('code/auxiliaryWindow/didApplyCSS');
|
||||
|
||||
return { stylesLoaded };
|
||||
}
|
||||
|
||||
private applyHTML(auxiliaryWindow: CodeWindow, disposables: DisposableStore): HTMLElement {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { NativeWindow } from 'vs/workbench/electron-sandbox/window';
|
||||
import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
|
||||
type NativeCodeWindow = CodeWindow & {
|
||||
readonly vscode: ISandboxGlobals;
|
||||
@@ -30,11 +31,12 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow {
|
||||
constructor(
|
||||
window: CodeWindow,
|
||||
container: HTMLElement,
|
||||
stylesHaveLoaded: Barrier,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@INativeHostService private readonly nativeHostService: INativeHostService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
super(window, container, configurationService);
|
||||
super(window, container, stylesHaveLoaded, configurationService);
|
||||
}
|
||||
|
||||
protected override async confirmBeforeClose(e: BeforeUnloadEvent): Promise<void> {
|
||||
@@ -73,7 +75,7 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService
|
||||
return windowId;
|
||||
}
|
||||
|
||||
protected override createContainer(auxiliaryWindow: NativeCodeWindow, disposables: DisposableStore): HTMLElement {
|
||||
protected override createContainer(auxiliaryWindow: NativeCodeWindow, disposables: DisposableStore) {
|
||||
|
||||
// Zoom level
|
||||
const windowConfig = this.configurationService.getValue<IWindowsConfiguration>();
|
||||
@@ -100,8 +102,8 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService
|
||||
};
|
||||
}
|
||||
|
||||
protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement): AuxiliaryWindow {
|
||||
return new NativeAuxiliaryWindow(targetWindow, container, this.configurationService, this.nativeHostService, this.instantiationService);
|
||||
protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesHaveLoaded: Barrier,): AuxiliaryWindow {
|
||||
return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -593,6 +593,7 @@ export class TestLayoutService implements IWorkbenchLayoutService {
|
||||
activeContainerDimension: IDimension = { width: 800, height: 600 };
|
||||
mainContainerOffset: ILayoutOffsetInfo = { top: 0, quickPickTop: 0 };
|
||||
activeContainerOffset: ILayoutOffsetInfo = { top: 0, quickPickTop: 0 };
|
||||
whenActiveContainerStylesLoaded = Promise.resolve();
|
||||
|
||||
mainContainer: HTMLElement = mainWindow.document.body;
|
||||
containers = [mainWindow.document.body];
|
||||
|
||||
Reference in New Issue
Block a user