From 500e680350267c27df7c3bb3aaff96fb40151279 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 25 Oct 2023 10:37:59 +0200 Subject: [PATCH] Aux window: editor global stylesheet does not work (fix #195969) (#196357) * Aux window: editor global stylesheet does not work (fix #195969) * improve --- src/vs/base/browser/dom.ts | 83 +++++++++++++++++-- .../services/abstractCodeEditorService.ts | 16 ++-- .../editor/test/browser/editorTestServices.ts | 4 +- .../browser/auxiliaryWindowService.ts | 11 ++- 4 files changed, 94 insertions(+), 20 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index e6bd07fb66d..764008eaf58 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -775,16 +775,69 @@ export function focusWindow(element: Node): void { } } -export function createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0], beforeAppend?: (style: HTMLStyleElement) => void): HTMLStyleElement { +const globalStylesheets = new Map>(); + +export function createStyleSheet(container: HTMLElement = document.head, beforeAppend?: (style: HTMLStyleElement) => void): HTMLStyleElement { const style = document.createElement('style'); style.type = 'text/css'; style.media = 'screen'; beforeAppend?.(style); container.appendChild(style); + + // 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 + } + + const clone = cloneGlobalStyleSheet(style, targetWindow); + clonedGlobalStylesheets.add(clone); + + event.Event.once(onDidUnregisterWindow)(unregisteredWindow => { + if (unregisteredWindow === targetWindow) { + clonedGlobalStylesheets.delete(clone); + } + }); + } + } + return style; } -export function createMetaElement(container: HTMLElement = document.getElementsByTagName('head')[0]): HTMLMetaElement { +export function isGlobalStylesheet(node: Node): boolean { + return globalStylesheets.has(node as HTMLStyleElement); +} + +export function cloneGlobalStylesheets(targetWindow: Window & typeof globalThis): IDisposable { + const disposables = new DisposableStore(); + + for (const [globalStylesheet, clonedGlobalStylesheets] of globalStylesheets) { + const clone = cloneGlobalStyleSheet(globalStylesheet, targetWindow); + + clonedGlobalStylesheets.add(clone); + disposables.add(toDisposable(() => clonedGlobalStylesheets.delete(clone))); + } + + return disposables; +} + +function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, targetWindow: Window & typeof globalThis): HTMLStyleElement { + const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement; + targetWindow.document.head.appendChild(clone); + + for (const rule of getDynamicStyleSheetRules(globalStylesheet)) { + clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length); + } + + return clone; +} + +export function createMetaElement(container: HTMLElement = document.head): HTMLMetaElement { const meta = document.createElement('meta'); container.appendChild(meta); return meta; @@ -798,7 +851,7 @@ function getSharedStyleSheet(): HTMLStyleElement { return _sharedStyleSheet; } -function getDynamicStyleSheetRules(style: any) { +function getDynamicStyleSheetRules(style: HTMLStyleElement) { if (style?.sheet?.rules) { // Chrome, IE return style.sheet.rules; @@ -810,15 +863,20 @@ function getDynamicStyleSheetRules(style: any) { return []; } -export function createCSSRule(selector: string, cssText: string, style: HTMLStyleElement = getSharedStyleSheet()): void { +export function createCSSRule(selector: string, cssText: string, style = getSharedStyleSheet()): void { if (!style || !cssText) { return; } - (style.sheet).insertRule(selector + '{' + cssText + '}', 0); + style.sheet?.insertRule(`${selector} {${cssText}}`, 0); + + // Apply rule also to all cloned global stylesheets + for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) { + createCSSRule(selector, cssText, clonedGlobalStylesheet); + } } -export function removeCSSRulesContainingSelector(ruleName: string, style: HTMLStyleElement = getSharedStyleSheet()): void { +export function removeCSSRulesContainingSelector(ruleName: string, style = getSharedStyleSheet()): void { if (!style) { return; } @@ -827,14 +885,23 @@ export function removeCSSRulesContainingSelector(ruleName: string, style: HTMLSt const toDelete: number[] = []; for (let i = 0; i < rules.length; i++) { const rule = rules[i]; - if (rule.selectorText.indexOf(ruleName) !== -1) { + if (isCSSStyleRule(rule) && rule.selectorText.indexOf(ruleName) !== -1) { toDelete.push(i); } } for (let i = toDelete.length - 1; i >= 0; i--) { - (style.sheet).deleteRule(toDelete[i]); + style.sheet?.deleteRule(toDelete[i]); } + + // Remove rules also from all cloned global stylesheets + for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) { + removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet); + } +} + +function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule { + return typeof (rule as CSSStyleRule).selectorText === 'string'; } export function isMouseEvent(e: unknown): e is MouseEvent { diff --git a/src/vs/editor/browser/services/abstractCodeEditorService.ts b/src/vs/editor/browser/services/abstractCodeEditorService.ts index 327a5828966..b960f48191e 100644 --- a/src/vs/editor/browser/services/abstractCodeEditorService.ts +++ b/src/vs/editor/browser/services/abstractCodeEditorService.ts @@ -346,9 +346,8 @@ class RefCountedStyleSheet { } } - public insertRule(rule: string, index?: number): void { - const sheet = this._styleSheet.sheet; - sheet.insertRule(rule, index); + public insertRule(selector: string, rule: string): void { + dom.createCSSRule(selector, rule, this._styleSheet); } public removeRulesContainingSelector(ruleName: string): void { @@ -373,9 +372,8 @@ export class GlobalStyleSheet { public unref(): void { } - public insertRule(rule: string, index?: number): void { - const sheet = this._styleSheet.sheet; - sheet.insertRule(rule, index); + public insertRule(selector: string, rule: string): void { + dom.createCSSRule(selector, rule, this._styleSheet); } public removeRulesContainingSelector(ruleName: string): void { @@ -714,15 +712,15 @@ class DecorationCSSRules { let hasContent = false; if (unthemedCSS.length > 0) { - sheet.insertRule(`${this._unThemedSelector} {${unthemedCSS}}`, 0); + sheet.insertRule(this._unThemedSelector, unthemedCSS); hasContent = true; } if (lightCSS.length > 0) { - sheet.insertRule(`.vs${this._unThemedSelector}, .hc-light${this._unThemedSelector} {${lightCSS}}`, 0); + sheet.insertRule(`.vs${this._unThemedSelector}, .hc-light${this._unThemedSelector}`, lightCSS); hasContent = true; } if (darkCSS.length > 0) { - sheet.insertRule(`.vs-dark${this._unThemedSelector}, .hc-black${this._unThemedSelector} {${darkCSS}}`, 0); + sheet.insertRule(`.vs-dark${this._unThemedSelector}, .hc-black${this._unThemedSelector}`, darkCSS); hasContent = true; } this._hasContent = hasContent; diff --git a/src/vs/editor/test/browser/editorTestServices.ts b/src/vs/editor/test/browser/editorTestServices.ts index 3aad1a86547..951759f9fbf 100644 --- a/src/vs/editor/test/browser/editorTestServices.ts +++ b/src/vs/editor/test/browser/editorTestServices.ts @@ -36,8 +36,8 @@ export class TestGlobalStyleSheet extends GlobalStyleSheet { super(null!); } - public override insertRule(rule: string, index?: number): void { - this.rules.unshift(rule); + public override insertRule(selector: string, rule: string): void { + this.rules.unshift(`${selector} {${rule}}`); } public override removeRulesContainingSelector(ruleName: string): void { diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index e6bd5e9b144..c91995c436a 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; -import { Dimension, EventHelper, EventType, addDisposableListener, copyAttributes, getActiveWindow, getClientArea, position, registerWindow, size, trackAttributes } from 'vs/base/browser/dom'; +import { Dimension, EventHelper, EventType, addDisposableListener, cloneGlobalStylesheets, copyAttributes, getActiveWindow, getClientArea, isGlobalStylesheet, position, registerWindow, 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'; @@ -154,6 +154,10 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili const mapOriginalToClone = new Map(); function cloneNode(originalNode: Node): void { + if (isGlobalStylesheet(originalNode)) { + return; // global stylesheets are handled by `cloneGlobalStylesheets` below + } + const clonedNode = auxiliaryWindow.document.head.appendChild(originalNode.cloneNode(true)); mapOriginalToClone.set(originalNode, clonedNode); } @@ -163,6 +167,11 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili cloneNode(originalNode); } + // Global stylesheets in are cloned in a special way because the mutation + // observer is not firing for changes done via `style.sheet` API. Only text changes + // can be observed. + disposables.add(cloneGlobalStylesheets(auxiliaryWindow)); + // 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 => {