Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aux window - debt changes #196910

Merged
merged 13 commits into from Oct 30, 2023
Merged
155 changes: 98 additions & 57 deletions src/vs/base/browser/dom.ts
Expand Up @@ -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]);
Expand Down Expand Up @@ -749,9 +750,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;
}
Expand Down Expand Up @@ -785,44 +791,47 @@ export function focusWindow(element: Node): void {
}
}

const globalStylesheets = new Map<HTMLStyleElement /* main stylesheet */, Set<HTMLStyleElement /* aux window clones that track the main stylesheet */>>();

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 <head> 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<HTMLStyleElement>();
globalStylesheets.set(style, clonedGlobalStylesheets);

for (const targetWindow of getWindows()) {
if (targetWindow === window) {
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;
}

const globalStylesheets = new Map<HTMLStyleElement /* main stylesheet */, Set<HTMLStyleElement /* aux window clones that track the main stylesheet */>>();

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) {
Expand All @@ -832,29 +841,85 @@ 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 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 });
}));

globalStylesheets.get(globalStylesheet)?.add(clone);
let clonedGlobalStylesheets = globalStylesheets.get(globalStylesheet);
if (!clonedGlobalStylesheets) {
clonedGlobalStylesheets = new Set<HTMLStyleElement>();
globalStylesheets.set(globalStylesheet, clonedGlobalStylesheets);
}
clonedGlobalStylesheets.add(clone);
disposables.add(toDisposable(() => clonedGlobalStylesheets?.delete(clone)));

return toDisposable(() => {
observer.disconnect();
targetWindow.document.head.removeChild(clone);
return disposables;
}

globalStylesheets.get(globalStylesheet)?.delete(clone);
});
interface IMutationObserver {
users: number;
readonly observer: MutationObserver;
readonly onDidMutate: event.Event<MutationRecord[]>;
}

export const sharedMutationObserver = new class {

readonly mutationObservers = new Map<Node, Map<number, IMutationObserver>>();

observe(target: Node, disposables: DisposableStore, options?: MutationObserverInit): event.Event<MutationRecord[]> {
let mutationObserversPerTarget = this.mutationObservers.get(target);
if (!mutationObserversPerTarget) {
mutationObserversPerTarget = new Map<number, IMutationObserver>();
this.mutationObservers.set(target, mutationObserversPerTarget);
}

const optionsHash = hash(options);
let mutationObserverPerOptions = mutationObserversPerTarget.get(optionsHash);
if (!mutationObserverPerOptions) {
const onDidMutate = new event.Emitter<MutationRecord[]>();
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);
} else {
mutationObserverPerOptions.users += 1;
}

return mutationObserverPerOptions.onDidMutate;
}
};

export function createMetaElement(container: HTMLElement = document.head): HTMLMetaElement {
const meta = document.createElement('meta');
container.appendChild(meta);
Expand Down Expand Up @@ -2092,35 +2157,6 @@ function camelCaseToHyphenCase(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}

interface IObserver extends IDisposable {
readonly onDidChangeAttribute: event.Event<string>;
}

function observeAttributes(element: Element, filter?: string[]): IObserver {
const onDidChangeAttribute = new event.Emitter<string>();

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);
Expand All @@ -2139,10 +2175,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;
}
2 changes: 0 additions & 2 deletions src/vs/code/electron-main/app.ts
Expand Up @@ -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
Expand Down
Expand Up @@ -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
}
Expand Down
14 changes: 7 additions & 7 deletions src/vs/editor/browser/editorDom.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -358,18 +358,17 @@ export interface CssProperties {

class RefCountedCssRule {
private _referenceCount: number = 0;
private _styleElement: HTMLStyleElement;
private _styleElement: HTMLStyleElement | undefined;
private _styleElementDisposables: DisposableStore;

constructor(
public readonly key: string,
public readonly className: string,
_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);
}

Expand All @@ -392,7 +391,8 @@ class RefCountedCssRule {
}

public dispose(): void {
this._styleElement.remove();
this._styleElementDisposables.dispose();
this._styleElement = undefined;
}

public increaseRefCount(): void {
Expand Down
10 changes: 10 additions & 0 deletions src/vs/platform/auxiliaryWindow/electron-main/auxiliaryWindow.ts
Expand Up @@ -15,15 +15,25 @@ export interface IAuxiliaryWindow {
readonly id: number;
readonly win: BrowserWindow | null;

readonly parentId: number;

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 {

readonly id = this.contents.id;

parentId = -1;

private readonly _onDidClose = this._register(new Emitter<void>());
readonly onDidClose = this._onDidClose.event;

Expand Down
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/layout/browser/layoutService.ts
Expand Up @@ -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;
}