mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-17 12:35:56 +01:00
Fix DOM creation in auxiliary windows (#330777)
* Fix DOM creation in auxiliary windows Create workbench DOM nodes in the main JavaScript realm and avoid xterm custom glyph rasterization in auxiliary windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * PR feedback --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
594d846898
commit
74f75d9ec5
@@ -6,6 +6,7 @@
|
||||
import './textAreaEditContext.css';
|
||||
import * as nls from '../../../../../nls.js';
|
||||
import * as browser from '../../../../../base/browser/browser.js';
|
||||
import { $ } from '../../../../../base/browser/dom.js';
|
||||
import { FastDomNode, createFastDomNode } from '../../../../../base/browser/fastDomNode.js';
|
||||
import { IKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
|
||||
import * as platform from '../../../../../base/common/platform.js';
|
||||
@@ -896,12 +897,12 @@ function measureText(targetDocument: Document, text: string, fontInfo: FontInfo,
|
||||
return 0;
|
||||
}
|
||||
|
||||
const container = targetDocument.createElement('div');
|
||||
const container = $<HTMLDivElement>('div');
|
||||
container.style.position = 'absolute';
|
||||
container.style.top = '-50000px';
|
||||
container.style.width = '50000px';
|
||||
|
||||
const regularDomNode = targetDocument.createElement('span');
|
||||
const regularDomNode = $<HTMLSpanElement>('span');
|
||||
applyFontInfo(regularDomNode, fontInfo);
|
||||
regularDomNode.style.whiteSpace = 'pre'; // just like the textarea
|
||||
regularDomNode.style.tabSize = `${tabSize * fontInfo.spaceWidth}px`; // just like the textarea
|
||||
|
||||
@@ -952,7 +952,7 @@ export class SuggestWidget implements IDisposable {
|
||||
}
|
||||
|
||||
if (this._measureContext === undefined) {
|
||||
this._measureContext = this.element.domNode.ownerDocument.createElement('canvas').getContext('2d');
|
||||
this._measureContext = dom.$<HTMLCanvasElement>('canvas').getContext('2d');
|
||||
}
|
||||
|
||||
let maxTextWidth: number;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { IMenu, IMenuService } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
|
||||
import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';
|
||||
import { CodeEditorWidget } from '../../../../browser/widget/codeEditor/codeEditorWidget.js';
|
||||
import { EditorOption } from '../../../../common/config/editorOptions.js';
|
||||
import { CompletionItemKind, CompletionItemProvider } from '../../../../common/languages.js';
|
||||
import { createCodeEditorServices } from '../../../../test/browser/testCodeEditor.js';
|
||||
import { createTextModel } from '../../../../test/common/testTextModel.js';
|
||||
import { CompletionModel } from '../../browser/completionModel.js';
|
||||
import { CompletionItem } from '../../browser/suggest.js';
|
||||
import { SuggestWidget } from '../../browser/suggestWidget.js';
|
||||
import { WordDistance } from '../../browser/wordDistance.js';
|
||||
|
||||
suite('SuggestWidget', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('measures suggestions in an auxiliary window', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
store.add(toDisposable(() => iframe.remove()));
|
||||
|
||||
const auxiliaryDocument = iframe.contentDocument!;
|
||||
const container = document.createElement('div');
|
||||
container.style.width = '500px';
|
||||
container.style.height = '300px';
|
||||
auxiliaryDocument.body.appendChild(container);
|
||||
|
||||
const createElement = auxiliaryDocument.createElement;
|
||||
auxiliaryDocument.createElement = () => {
|
||||
throw new Error('Not allowed to create elements in child window JavaScript context.');
|
||||
};
|
||||
store.add(toDisposable(() => auxiliaryDocument.createElement = createElement));
|
||||
|
||||
const services = new ServiceCollection(
|
||||
[IStorageService, store.add(new InMemoryStorageService())],
|
||||
[IMarkdownRendererService, new class extends mock<IMarkdownRendererService>() { }],
|
||||
[IMenuService, new class extends mock<IMenuService>() {
|
||||
override createMenu(): IMenu {
|
||||
return new class extends mock<IMenu>() {
|
||||
override readonly onDidChange = Event.None;
|
||||
override dispose(): void { }
|
||||
};
|
||||
}
|
||||
}],
|
||||
);
|
||||
const instantiationService = createCodeEditorServices(store, services);
|
||||
const editor = store.add(instantiationService.createInstance(
|
||||
CodeEditorWidget,
|
||||
container,
|
||||
{ suggest: { fitWidthToDetails: true } },
|
||||
{ contributions: [] },
|
||||
));
|
||||
const textModel = store.add(createTextModel('a'));
|
||||
editor.setModel(textModel);
|
||||
editor.layout({ width: 500, height: 300 });
|
||||
|
||||
const position = { lineNumber: 1, column: 2 };
|
||||
const completion = {
|
||||
label: { label: 'agent', detail: ' with a detailed description' },
|
||||
insertText: 'agent',
|
||||
kind: CompletionItemKind.Function,
|
||||
range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 2 },
|
||||
};
|
||||
const completionList = { suggestions: [completion] };
|
||||
const provider: CompletionItemProvider = {
|
||||
_debugDisplayName: 'test',
|
||||
provideCompletionItems: () => completionList,
|
||||
};
|
||||
const completionModel = new CompletionModel(
|
||||
[new CompletionItem(position, completion, completionList, provider)],
|
||||
position.column,
|
||||
{ leadingLineContent: 'a', characterCountDelta: 0 },
|
||||
WordDistance.None,
|
||||
editor.getOption(EditorOption.suggest),
|
||||
editor.getOption(EditorOption.snippetSuggestions),
|
||||
);
|
||||
const widget = store.add(instantiationService.createInstance(SuggestWidget, editor));
|
||||
|
||||
widget.showSuggestions(completionModel, 0, false, false, false);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
ownerDocument: widget.element.domNode.ownerDocument === auxiliaryDocument,
|
||||
mainRealmElement: widget.element.domNode instanceof HTMLElement,
|
||||
attached: auxiliaryDocument.body.contains(widget.element.domNode),
|
||||
}, {
|
||||
ownerDocument: true,
|
||||
mainRealmElement: true,
|
||||
attached: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { addDisposableGenericMouseDownListener, addDisposableGenericMouseMoveListener, addDisposableListener, EventType, getWindow, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js';
|
||||
import { $, addDisposableGenericMouseDownListener, addDisposableGenericMouseMoveListener, addDisposableListener, EventType, getWindow, scheduleAtNextAnimationFrame } from '../../../../base/browser/dom.js';
|
||||
import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
|
||||
import { RunOnceScheduler } from '../../../../base/common/async.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
@@ -167,9 +167,7 @@ export class AquariumService extends Disposable implements IAquariumService {
|
||||
}
|
||||
|
||||
mountToggle(parent: HTMLElement): IMountedToggleHandle {
|
||||
const doc = parent.ownerDocument;
|
||||
const button = doc.createElement('button');
|
||||
button.className = 'agents-aquarium-toggle';
|
||||
const button = $<HTMLButtonElement>('button.agents-aquarium-toggle');
|
||||
button.type = 'button';
|
||||
this.updateToggleButtonVisual(button, !!this.activeRef.value);
|
||||
|
||||
@@ -304,7 +302,7 @@ export class AquariumService extends Disposable implements IAquariumService {
|
||||
|
||||
// Build the icon as a real DOM child instead of innerHTML to satisfy Trusted Types.
|
||||
button.replaceChildren();
|
||||
const iconSpan = button.ownerDocument.createElement('span');
|
||||
const iconSpan = $<HTMLSpanElement>('span');
|
||||
// The icon is purely decorative; the button already has an aria-label.
|
||||
iconSpan.setAttribute('aria-hidden', 'true');
|
||||
addIconClasses(iconSpan, icon);
|
||||
@@ -318,11 +316,11 @@ export class AquariumService extends Disposable implements IAquariumService {
|
||||
const showStreak = streak > 0 || revivable > 0;
|
||||
button.classList.toggle('has-streak', showStreak);
|
||||
if (showStreak) {
|
||||
const streakSpan = button.ownerDocument.createElement('span');
|
||||
const streakSpan = $<HTMLSpanElement>('span');
|
||||
streakSpan.className = 'agents-aquarium-toggle-streak';
|
||||
streakSpan.setAttribute('aria-hidden', 'true');
|
||||
if (active) {
|
||||
const hungerIconSpan = button.ownerDocument.createElement('span');
|
||||
const hungerIconSpan = $<HTMLSpanElement>('span');
|
||||
addIconClasses(hungerIconSpan, hungerIcon);
|
||||
streakSpan.appendChild(hungerIconSpan);
|
||||
}
|
||||
@@ -508,9 +506,7 @@ function createActiveAquarium(mainContainer: HTMLElement, layoutService: IWorkbe
|
||||
}
|
||||
|
||||
const store = new DisposableStore();
|
||||
const doc = targetWindow.document;
|
||||
const water = doc.createElement('div');
|
||||
water.className = 'agents-aquarium-water';
|
||||
const water = $('.agents-aquarium-water');
|
||||
// Decorative: hide the entire subtree from a11y tree.
|
||||
water.setAttribute('aria-hidden', 'true');
|
||||
// First child so subsequent chat bar content paints over it.
|
||||
@@ -524,12 +520,10 @@ function createActiveAquarium(mainContainer: HTMLElement, layoutService: IWorkbe
|
||||
sessionsContainer.classList.remove('aquarium-active');
|
||||
}));
|
||||
|
||||
const fishLayer = doc.createElement('div');
|
||||
fishLayer.className = 'agents-aquarium-fish-layer';
|
||||
const fishLayer = $('.agents-aquarium-fish-layer');
|
||||
water.appendChild(fishLayer);
|
||||
|
||||
const foodLayer = doc.createElement('div');
|
||||
foodLayer.className = 'agents-aquarium-food-layer';
|
||||
const foodLayer = $('.agents-aquarium-food-layer');
|
||||
water.appendChild(foodLayer);
|
||||
|
||||
const bounds = { width: 0, height: 0 };
|
||||
@@ -675,8 +669,7 @@ function createActiveAquarium(mainContainer: HTMLElement, layoutService: IWorkbe
|
||||
const oldest = food[0];
|
||||
removeFood(oldest);
|
||||
}
|
||||
const el = doc.createElement('div');
|
||||
el.className = 'agents-aquarium-food';
|
||||
const el = $<HTMLDivElement>('.agents-aquarium-food');
|
||||
el.style.transform = `translate(${dropX}px, ${dropY}px)`;
|
||||
foodLayer.appendChild(el);
|
||||
food.push({ element: el, positionX: dropX, positionY: dropY, fallSpeed: randomBetween(20, 35) });
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $ } from '../../../../base/browser/dom.js';
|
||||
import { VSCODE_LOGO_PATH } from './vscodeLogoPath.js';
|
||||
|
||||
/**
|
||||
@@ -98,16 +99,14 @@ export class Fish {
|
||||
this.size = opts.size;
|
||||
this.wanderAngle = Math.atan2(opts.velocityY, opts.velocityX);
|
||||
|
||||
this.element = targetDocument.createElement('div');
|
||||
this.element.className = 'agents-aquarium-fish';
|
||||
this.element = $<HTMLDivElement>('.agents-aquarium-fish');
|
||||
this.element.style.width = `${opts.size}px`;
|
||||
this.element.style.height = `${opts.size}px`;
|
||||
this.element.style.color = SPECIES_COLOR[opts.species];
|
||||
|
||||
// Inner element receives the directional flip so the body strip animations
|
||||
// (driven by --agents-aquarium-strip-index) are unaffected by direction changes.
|
||||
this.innerElement = targetDocument.createElement('div');
|
||||
this.innerElement.className = 'agents-aquarium-fish-inner';
|
||||
this.innerElement = $<HTMLDivElement>('.agents-aquarium-fish-inner');
|
||||
this.innerElement.appendChild(buildFishSvg(targetDocument));
|
||||
this.element.appendChild(this.innerElement);
|
||||
|
||||
@@ -171,8 +170,6 @@ export class Fish {
|
||||
}
|
||||
}
|
||||
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
/**
|
||||
* Number of vertical strips the body is sliced into. More strips = smoother
|
||||
* wave, but each strip is one `<use>` node and one CSS animation per fish.
|
||||
@@ -204,8 +201,8 @@ function ensureSharedDefs(targetDocument: Document): void {
|
||||
return;
|
||||
}
|
||||
const stripWidth = (BODY_X_END - BODY_X_START) / NUM_BODY_STRIPS;
|
||||
const container = targetDocument.createElementNS(SVG_NS, 'svg');
|
||||
container.setAttribute('xmlns', SVG_NS);
|
||||
const container = $.SVG<SVGSVGElement>('svg');
|
||||
container.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
container.setAttribute('width', '0');
|
||||
container.setAttribute('height', '0');
|
||||
container.setAttribute('aria-hidden', 'true');
|
||||
@@ -217,14 +214,14 @@ function ensureSharedDefs(targetDocument: Document): void {
|
||||
|
||||
// All strips reference this symbol via `<use href="#agents-aquarium-fish-logo">`,
|
||||
// so the path data is parsed exactly ONCE per session instead of FISH_COUNT * NUM_STRIPS.
|
||||
container.appendChild(createVSCodeLogoSymbol(targetDocument));
|
||||
container.appendChild(createVSCodeLogoSymbol());
|
||||
|
||||
const defs = targetDocument.createElementNS(SVG_NS, 'defs');
|
||||
const defs = $.SVG<SVGDefsElement>('defs');
|
||||
for (let i = 0; i < NUM_BODY_STRIPS; i++) {
|
||||
const clip = targetDocument.createElementNS(SVG_NS, 'clipPath');
|
||||
const clip = $.SVG<SVGClipPathElement>('clipPath');
|
||||
clip.setAttribute('id', `agents-aquarium-fish-clip-${i}`);
|
||||
clip.setAttribute('clipPathUnits', 'userSpaceOnUse');
|
||||
const rect = targetDocument.createElementNS(SVG_NS, 'rect');
|
||||
const rect = $.SVG<SVGRectElement>('rect');
|
||||
rect.setAttribute('x', String(BODY_X_START + i * stripWidth));
|
||||
rect.setAttribute('y', '-20');
|
||||
// Larger overlap (0.8 user-units) hides seams when adjacent strips
|
||||
@@ -239,13 +236,13 @@ function ensureSharedDefs(targetDocument: Document): void {
|
||||
sharedDefsByDocument.set(targetDocument, container);
|
||||
}
|
||||
|
||||
function createVSCodeLogoSymbol(targetDocument: Document): SVGSymbolElement {
|
||||
const symbol = targetDocument.createElementNS(SVG_NS, 'symbol');
|
||||
function createVSCodeLogoSymbol(): SVGSymbolElement {
|
||||
const symbol = $.SVG<SVGSymbolElement>('symbol');
|
||||
symbol.setAttribute('id', SHARED_LOGO_SYMBOL_ID);
|
||||
symbol.setAttribute('viewBox', '0 0 96 96');
|
||||
symbol.setAttribute('overflow', 'visible');
|
||||
|
||||
const logoPath = targetDocument.createElementNS(SVG_NS, 'path');
|
||||
const logoPath = $.SVG<SVGPathElement>('path');
|
||||
logoPath.setAttribute('d', VSCODE_LOGO_PATH);
|
||||
logoPath.setAttribute('fill', 'currentColor');
|
||||
logoPath.setAttribute('fill-rule', 'evenodd');
|
||||
@@ -259,8 +256,8 @@ function createVSCodeLogoSymbol(targetDocument: Document): SVGSymbolElement {
|
||||
* - VS Code logo body, sliced into N vertical strips that each oscillate in
|
||||
* Y with a phase-offset CSS animation (the "swimming" sine wave)
|
||||
*
|
||||
* Colors come from `currentColor` on the parent element. Built with
|
||||
* `document.createElementNS` (no innerHTML) to satisfy Trusted Types.
|
||||
* Colors come from `currentColor` on the parent element. Built without
|
||||
* `innerHTML` to satisfy Trusted Types.
|
||||
*
|
||||
* The strip clipPath defs and the logo symbol are shared across all fish via
|
||||
* {@link ensureSharedDefs}.
|
||||
@@ -268,8 +265,8 @@ function createVSCodeLogoSymbol(targetDocument: Document): SVGSymbolElement {
|
||||
function buildFishSvg(targetDocument: Document): SVGSVGElement {
|
||||
ensureSharedDefs(targetDocument);
|
||||
|
||||
const svg = targetDocument.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('xmlns', SVG_NS);
|
||||
const svg = $.SVG<SVGSVGElement>('svg');
|
||||
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
||||
svg.setAttribute('focusable', 'false');
|
||||
// viewBox 0..96 matches the original VS Code icon.
|
||||
svg.setAttribute('viewBox', '0 0 96 96');
|
||||
@@ -281,13 +278,13 @@ function buildFishSvg(targetDocument: Document): SVGSVGElement {
|
||||
// Body: NUM_BODY_STRIPS overlapping references to the shared logo symbol,
|
||||
// each clipped to its vertical band via shared clipPath defs. Each strip
|
||||
// animates translateY with a phase offset driven by --agents-aquarium-strip-index.
|
||||
const bodyGroup = targetDocument.createElementNS(SVG_NS, 'g');
|
||||
const bodyGroup = $.SVG<SVGGElement>('g');
|
||||
bodyGroup.setAttribute('class', 'agents-aquarium-fish-body');
|
||||
for (let i = 0; i < NUM_BODY_STRIPS; i++) {
|
||||
const stripG = targetDocument.createElementNS(SVG_NS, 'g');
|
||||
const stripG = $.SVG<SVGGElement>('g');
|
||||
stripG.setAttribute('class', 'agents-aquarium-fish-strip');
|
||||
stripG.style.setProperty('--agents-aquarium-strip-index', String(i));
|
||||
const stripUse = targetDocument.createElementNS(SVG_NS, 'use');
|
||||
const stripUse = $.SVG<SVGUseElement>('use');
|
||||
stripUse.setAttribute('href', `#${SHARED_LOGO_SYMBOL_ID}`);
|
||||
stripUse.setAttribute('clip-path', `url(#agents-aquarium-fish-clip-${i})`);
|
||||
stripG.appendChild(stripUse);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { InMemoryStorageService, StorageScope } from '../../../../../platform/st
|
||||
import { NullTelemetryServiceShape } from '../../../../../platform/telemetry/common/telemetryUtils.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { AquariumService, SESSIONS_DEVELOPER_JOY_ENABLED_SETTING } from '../../browser/aquariumOverlay.js';
|
||||
import { disposeSharedFishDefs, Fish, FishSpecies } from '../../browser/fish.js';
|
||||
|
||||
suite('AquariumService', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -87,4 +88,75 @@ suite('AquariumService', () => {
|
||||
afterShow: { visible: true, display: '' },
|
||||
});
|
||||
});
|
||||
|
||||
test('creates aquarium elements in the main realm for an auxiliary window', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
store.add(toDisposable(() => iframe.remove()));
|
||||
|
||||
const auxiliaryDocument = iframe.contentDocument!;
|
||||
const toggleContainer = document.createElement('div');
|
||||
auxiliaryDocument.body.appendChild(toggleContainer);
|
||||
const createElement = auxiliaryDocument.createElement;
|
||||
auxiliaryDocument.createElement = () => {
|
||||
throw new Error('Not allowed to create elements in child window JavaScript context.');
|
||||
};
|
||||
store.add(toDisposable(() => auxiliaryDocument.createElement = createElement));
|
||||
|
||||
const storageService = store.add(new InMemoryStorageService());
|
||||
const layoutService = new class extends mock<IWorkbenchLayoutService>() {
|
||||
override readonly mainContainer = document.createElement('div');
|
||||
}();
|
||||
const hoverService = new class extends mock<IHoverService>() {
|
||||
override setupManagedHover(): IManagedHover {
|
||||
return {
|
||||
dispose() { },
|
||||
show() { },
|
||||
hide() { },
|
||||
update() { },
|
||||
};
|
||||
}
|
||||
}();
|
||||
const configurationService = new TestConfigurationService({ [SESSIONS_DEVELOPER_JOY_ENABLED_SETTING]: true });
|
||||
store.add(configurationService.onDidChangeConfigurationEmitter);
|
||||
const service = store.add(new AquariumService(
|
||||
layoutService,
|
||||
new MockContextKeyService(),
|
||||
hoverService,
|
||||
storageService,
|
||||
configurationService,
|
||||
new TestAccessibilityService(),
|
||||
new NullTelemetryServiceShape(),
|
||||
));
|
||||
store.add(service.mountToggle(toggleContainer));
|
||||
const fish = new Fish({
|
||||
species: FishSpecies.Stable,
|
||||
size: 24,
|
||||
positionX: 0,
|
||||
positionY: 0,
|
||||
velocityX: 1,
|
||||
velocityY: 0,
|
||||
}, auxiliaryDocument);
|
||||
auxiliaryDocument.body.appendChild(fish.element);
|
||||
store.add(toDisposable(() => {
|
||||
fish.element.remove();
|
||||
disposeSharedFishDefs(auxiliaryDocument);
|
||||
}));
|
||||
|
||||
const button = toggleContainer.querySelector('.agents-aquarium-toggle');
|
||||
const svg = fish.element.querySelector('svg');
|
||||
assert.deepStrictEqual({
|
||||
buttonOwnerDocument: button?.ownerDocument === auxiliaryDocument,
|
||||
fishOwnerDocument: fish.element.ownerDocument === auxiliaryDocument,
|
||||
mainRealmButton: button instanceof HTMLButtonElement,
|
||||
mainRealmFish: fish.element instanceof HTMLDivElement,
|
||||
mainRealmSvg: svg instanceof SVGSVGElement,
|
||||
}, {
|
||||
buttonOwnerDocument: true,
|
||||
fishOwnerDocument: true,
|
||||
mainRealmButton: true,
|
||||
mainRealmFish: true,
|
||||
mainRealmSvg: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $ } from '../../../../../base/browser/dom.js';
|
||||
import { IManagedHoverContent } from '../../../../../base/browser/ui/hover/hover.js';
|
||||
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { IChatSpeechToTextService } from './chatSpeechToTextService.js';
|
||||
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
|
||||
/** Radius of the progress ring in the 16×16 viewBox used for the toolbar icon. */
|
||||
const RING_RADIUS = 7;
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
|
||||
@@ -30,19 +29,18 @@ export class DictationDownloadRing extends Disposable {
|
||||
) {
|
||||
super();
|
||||
|
||||
const ownerDocument = container.ownerDocument;
|
||||
const svg = ownerDocument.createElementNS(SVG_NS, 'svg') as SVGSVGElement;
|
||||
const svg = $.SVG<SVGSVGElement>('svg');
|
||||
svg.classList.add('dictation-download-ring');
|
||||
svg.setAttribute('viewBox', '0 0 16 16');
|
||||
svg.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const track = ownerDocument.createElementNS(SVG_NS, 'circle') as SVGCircleElement;
|
||||
const track = $.SVG<SVGCircleElement>('circle');
|
||||
track.classList.add('dictation-download-ring-track');
|
||||
track.setAttribute('cx', '8');
|
||||
track.setAttribute('cy', '8');
|
||||
track.setAttribute('r', String(RING_RADIUS));
|
||||
|
||||
const progress = ownerDocument.createElementNS(SVG_NS, 'circle') as SVGCircleElement;
|
||||
const progress = $.SVG<SVGCircleElement>('circle');
|
||||
progress.classList.add('dictation-download-ring-progress');
|
||||
progress.setAttribute('cx', '8');
|
||||
progress.setAttribute('cy', '8');
|
||||
|
||||
@@ -278,13 +278,11 @@ const RIM_SIZE_FLOOR = 0.35;
|
||||
*/
|
||||
export function createVoiceRimLight(target: HTMLElement, accent: Color, theme: GlowThemeKind, mood: VoiceRimMood = 'cool', background?: Color): IVoiceRimLight {
|
||||
const store = new DisposableStore();
|
||||
const doc = target.ownerDocument;
|
||||
|
||||
if (!target.style.position) {
|
||||
target.style.position = 'relative';
|
||||
}
|
||||
const slot = doc.createElement('div');
|
||||
slot.className = 'voice-glow-slot voice-glow-slot-inline';
|
||||
const slot = $('.voice-glow-slot.voice-glow-slot-inline');
|
||||
target.appendChild(slot);
|
||||
store.add(toDisposable(() => slot.remove()));
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { $ } from '../../../../../base/browser/dom.js';
|
||||
import { appendEscapedMarkdownInlineCode, isPortableLinkTarget, isPortableMarkdownTarget } from '../../../../../base/common/htmlContent.js';
|
||||
import * as marked from '../../../../../base/common/marked/marked.js';
|
||||
import { IMarkdownEdit, markdownTokensToPlainText, rewriteMarkdownLinks } from '../../../../../base/common/markdownLinks.js';
|
||||
@@ -22,7 +23,7 @@ function replaceWithLabel(element: Element, label: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const code = element.ownerDocument.createElement('code');
|
||||
const code = $('code');
|
||||
code.textContent = label;
|
||||
element.replaceWith(code);
|
||||
}
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ export class ChatProgressContentPart extends Disposable implements IChatContentP
|
||||
|
||||
const shimmerText = text.slice(0, remaining);
|
||||
const suffixText = text.slice(remaining);
|
||||
const span = element.ownerDocument.createElement('span');
|
||||
const span = $<HTMLSpanElement>('span');
|
||||
span.classList.add('chat-progress-shimmer-text');
|
||||
span.textContent = shimmerText;
|
||||
node.parentNode?.insertBefore(span, node);
|
||||
|
||||
@@ -681,7 +681,7 @@ export class ChatListWidget extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const holder = this._container.ownerDocument.createElement('div');
|
||||
const holder = dom.$('div');
|
||||
for (const fragment of fragments) {
|
||||
holder.appendChild(fragment);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { addDisposableListener, EventType, isAncestorOfActiveElement, setVisibility } from '../../../../../../base/browser/dom.js';
|
||||
import { $, addDisposableListener, EventType, isAncestorOfActiveElement, setVisibility } from '../../../../../../base/browser/dom.js';
|
||||
import { ActionBar } from '../../../../../../base/browser/ui/actionbar/actionbar.js';
|
||||
import { mainWindow } from '../../../../../../base/browser/window.js';
|
||||
import { alert, status } from '../../../../../../base/browser/ui/aria/aria.js';
|
||||
import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js';
|
||||
import { Action } from '../../../../../../base/common/actions.js';
|
||||
@@ -102,9 +101,7 @@ export class ChatInputNoticeWidget extends Disposable implements IChatInputNotic
|
||||
this._variant = options.variant;
|
||||
this._ariaRoleDescription = options.ariaRoleDescription;
|
||||
|
||||
// Detached notices are created in the main window's document, the same as
|
||||
// `dom.$` does, and adopted when their owner parents them.
|
||||
this.domNode = (options.container?.ownerDocument ?? mainWindow.document).createElement('div');
|
||||
this.domNode = $('div');
|
||||
this.domNode.classList.add('chat-input-notice', `chat-input-notice-${options.variant}`);
|
||||
if (options.className) {
|
||||
this.domNode.classList.add(options.className);
|
||||
@@ -216,7 +213,7 @@ export class ChatInputNoticeWidget extends Disposable implements IChatInputNotic
|
||||
addAction(options: IChatInputNoticeActionOptions): HTMLElement {
|
||||
const register = <T extends IDisposable>(disposable: T): T => options.store ? options.store.add(disposable) : this._register(disposable);
|
||||
|
||||
const container = this.domNode.ownerDocument.createElement('div');
|
||||
const container = $('div');
|
||||
container.classList.add('chat-input-notice-action');
|
||||
(options.parent ?? this.domNode).appendChild(container);
|
||||
register(toDisposable(() => container.remove()));
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { IChatSpeechToTextService } from '../../browser/speechToText/chatSpeechToTextService.js';
|
||||
import { DictationDownloadRing } from '../../browser/speechToText/dictationDownloadRing.js';
|
||||
|
||||
suite('DictationDownloadRing', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('creates its SVG elements in the main realm for an auxiliary window', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
store.add(toDisposable(() => iframe.remove()));
|
||||
|
||||
const auxiliaryDocument = iframe.contentDocument!;
|
||||
const container = document.createElement('div');
|
||||
auxiliaryDocument.body.appendChild(container);
|
||||
const service = new class extends mock<IChatSpeechToTextService>() {
|
||||
override readonly onDidChangeModelDownloadProgress = Event.None;
|
||||
override readonly modelDownloadProgress = 0.5;
|
||||
};
|
||||
|
||||
store.add(new DictationDownloadRing(container, service));
|
||||
const ring = container.querySelector('svg');
|
||||
|
||||
assert.deepStrictEqual({
|
||||
ownerDocument: ring?.ownerDocument === auxiliaryDocument,
|
||||
mainRealmSvg: ring instanceof SVGSVGElement,
|
||||
circles: ring?.querySelectorAll('circle').length,
|
||||
}, {
|
||||
ownerDocument: true,
|
||||
mainRealmSvg: true,
|
||||
circles: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import { IColorTheme } from '../../../../../../platform/theme/common/themeServic
|
||||
import { chatDictationActiveMicGlow, chatVoiceGlowBaseColor, chatVoiceSpeakingGlow } from '../../../common/widget/chatColors.js';
|
||||
import { resolveDictationMicAccent } from '../../../browser/speechToText/dictationMicGlow.js';
|
||||
import { isGlowingVoiceState, GlowThemeKind, resolveVoiceGlowColors, resolveVoiceRimAccent, shouldRenderVoiceInputGlow, VOICE_GLOW_SPEAKING_HUE_SHIFT } from '../../../browser/voiceClient/voiceGlow.js';
|
||||
import { createVoiceGlowController } from '../../../browser/voiceClient/voiceGlowController.js';
|
||||
import { createVoiceGlowController, createVoiceRimLight } from '../../../browser/voiceClient/voiceGlowController.js';
|
||||
|
||||
suite('VoiceGlow', () => {
|
||||
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -51,17 +51,20 @@ suite('VoiceGlow', () => {
|
||||
|
||||
const controller = disposables.add(createVoiceGlowController(target));
|
||||
controller.render('listening', 0.5, false);
|
||||
disposables.add(createVoiceRimLight(target, Color.fromHex('#58A6FF'), 'dark'));
|
||||
|
||||
assert.deepStrictEqual({
|
||||
active: target.classList.contains('voice-active'),
|
||||
listening: target.classList.contains('voice-listening'),
|
||||
slots: target.querySelectorAll('.voice-glow-slot').length,
|
||||
inlineSlots: target.querySelectorAll('.voice-glow-slot-inline').length,
|
||||
layers: target.querySelectorAll('.voice-glow-rim-corners, .voice-glow-rim-bloom').length,
|
||||
}, {
|
||||
active: true,
|
||||
listening: true,
|
||||
slots: 2,
|
||||
layers: 2,
|
||||
slots: 3,
|
||||
inlineSlots: 1,
|
||||
layers: 4,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import { convertHtmlToMarkdown } from '../../../../../../base/browser/htmlToMarkdown.js';
|
||||
import { toDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { sanitizeChatClipboardFragment, toPortableMarkdown } from '../../../browser/widget/chatClipboard.js';
|
||||
|
||||
@@ -23,7 +24,7 @@ function sanitizeToHtml(html: string): string {
|
||||
}
|
||||
|
||||
suite('ChatClipboard', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('reports whether the selection had to change', () => {
|
||||
assert.deepStrictEqual(
|
||||
@@ -72,6 +73,37 @@ suite('ChatClipboard', () => {
|
||||
'<p>before after</p>');
|
||||
});
|
||||
|
||||
test('sanitizes a fragment from an auxiliary window', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
disposables.add(toDisposable(() => iframe.remove()));
|
||||
|
||||
const auxiliaryDocument = iframe.contentDocument!;
|
||||
const fragment = auxiliaryDocument.createDocumentFragment();
|
||||
const anchor = auxiliaryDocument.createElement('a');
|
||||
anchor.setAttribute('data-href', 'file:///repo/a.ts');
|
||||
anchor.textContent = 'a.ts';
|
||||
fragment.appendChild(anchor);
|
||||
const createElement = auxiliaryDocument.createElement;
|
||||
auxiliaryDocument.createElement = () => {
|
||||
throw new Error('Not allowed to create elements in child window JavaScript context.');
|
||||
};
|
||||
disposables.add(toDisposable(() => auxiliaryDocument.createElement = createElement));
|
||||
|
||||
sanitizeChatClipboardFragment(fragment);
|
||||
const replacement = fragment.firstElementChild;
|
||||
|
||||
assert.deepStrictEqual({
|
||||
html: replacement?.outerHTML,
|
||||
ownerDocument: replacement?.ownerDocument === auxiliaryDocument,
|
||||
mainRealmElement: replacement instanceof HTMLElement,
|
||||
}, {
|
||||
html: '<code>a.ts</code>',
|
||||
ownerDocument: true,
|
||||
mainRealmElement: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('produces markdown without internal targets when pasted back into chat', () => {
|
||||
const copied = sanitizeToHtml(
|
||||
'<p>This is <strong>inherited</strong> from the <a href="" data-href="file:///repo/src/FooBar.ts">FooBar</a> class. '
|
||||
|
||||
@@ -63,6 +63,40 @@ suite('ChatInputNoticeWidget', () => {
|
||||
{ parented: false, connected: false });
|
||||
});
|
||||
|
||||
test('creates the notice and its actions for an auxiliary window', () => {
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
disposables.add(toDisposable(() => iframe.remove()));
|
||||
|
||||
const auxiliaryDocument = iframe.contentDocument!;
|
||||
const container = document.createElement('div');
|
||||
auxiliaryDocument.body.appendChild(container);
|
||||
const createElement = auxiliaryDocument.createElement;
|
||||
auxiliaryDocument.createElement = () => {
|
||||
throw new Error('Not allowed to create elements in child window JavaScript context.');
|
||||
};
|
||||
disposables.add(toDisposable(() => auxiliaryDocument.createElement = createElement));
|
||||
|
||||
const notice = createNotice(container);
|
||||
const action = notice.addAction({
|
||||
ariaLabel: 'Continue',
|
||||
icon: Codicon.check,
|
||||
onActivate: () => { },
|
||||
});
|
||||
|
||||
assert.deepStrictEqual({
|
||||
noticeOwnerDocument: notice.domNode.ownerDocument === auxiliaryDocument,
|
||||
actionOwnerDocument: action.ownerDocument === auxiliaryDocument,
|
||||
mainRealmNotice: notice.domNode instanceof HTMLElement,
|
||||
mainRealmAction: action instanceof HTMLElement,
|
||||
}, {
|
||||
noticeOwnerDocument: true,
|
||||
actionOwnerDocument: true,
|
||||
mainRealmNotice: true,
|
||||
mainRealmAction: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('interrupts for an introduction, but waits its turn for a tip', () => {
|
||||
const container = createContainer(disposables);
|
||||
const ariaContainer = dom.append(container, dom.$('div'));
|
||||
|
||||
@@ -121,6 +121,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
private readonly _xtermColorProvider: IXtermColorProvider;
|
||||
private readonly _capabilities: ITerminalCapabilityStore;
|
||||
private readonly _disableOverviewRuler: boolean;
|
||||
private readonly _mainDocument: Document;
|
||||
|
||||
private static _suggestedRendererType: 'dom' | undefined = undefined;
|
||||
private _attached?: { container: HTMLElement; options: IXtermAttachToElementOptions };
|
||||
@@ -144,7 +145,10 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
private _searchAddon?: SearchAddonType;
|
||||
private _unicode11Addon?: Unicode11AddonType;
|
||||
private _webglAddon?: WebglAddonType;
|
||||
private _webglAddonCustomGlyphs?: boolean = false;
|
||||
private readonly _webglContextLossListener = this._register(new MutableDisposable());
|
||||
private _webglAddonCustomGlyphs?: boolean;
|
||||
private _webglAddonLoading = false;
|
||||
private _webglAddonLoadId = 0;
|
||||
private _serializeAddon?: SerializeAddonType;
|
||||
private _imageAddon?: ImageAddonType;
|
||||
private readonly _ligaturesAddon: MutableDisposable<LigaturesAddonType> = this._register(new MutableDisposable());
|
||||
@@ -228,6 +232,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
this._xtermColorProvider = options.xtermColorProvider;
|
||||
this._capabilities = options.capabilities;
|
||||
this._disableOverviewRuler = options.disableOverviewRuler ?? false;
|
||||
this._mainDocument = layoutService.mainContainer.ownerDocument;
|
||||
|
||||
const font = this._terminalConfigurationService.getFont(dom.getActiveWindow(), undefined, true);
|
||||
const config = this._terminalConfigurationService.config;
|
||||
@@ -237,7 +242,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
allowProposedApi: true,
|
||||
cols: options.cols,
|
||||
rows: options.rows,
|
||||
documentOverride: layoutService.mainContainer.ownerDocument,
|
||||
documentOverride: this._mainDocument,
|
||||
altClickMovesCursor: config.altClickMovesCursor && editorOptions.multiCursorModifier === 'alt',
|
||||
scrollback: config.scrollback,
|
||||
theme: this.getXtermTheme(),
|
||||
@@ -882,26 +887,58 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
|
||||
private async _enableWebglRenderer(): Promise<void> {
|
||||
// Currently webgl options can only be specified on addon creation
|
||||
if (!this.raw.element || this._webglAddon && this._webglAddonCustomGlyphs === this._terminalConfigurationService.config.customGlyphs) {
|
||||
if (!this.raw.element) {
|
||||
return;
|
||||
}
|
||||
const customGlyphs = this._getWebglCustomGlyphs();
|
||||
if ((this._webglAddon || this._webglAddonLoading) && this._webglAddonCustomGlyphs === customGlyphs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Dispose of existing addon before creating a new one to avoid leaking WebGL contexts
|
||||
this._disposeOfWebglRenderer();
|
||||
|
||||
this._webglAddonCustomGlyphs = this._terminalConfigurationService.config.customGlyphs;
|
||||
const loadId = this._webglAddonLoadId;
|
||||
this._webglAddonLoading = true;
|
||||
this._webglAddonCustomGlyphs = customGlyphs;
|
||||
|
||||
let Addon: typeof WebglAddonType;
|
||||
try {
|
||||
Addon = await this._xtermAddonLoader.importAddon('webgl');
|
||||
} catch (error) {
|
||||
if (loadId === this._webglAddonLoadId) {
|
||||
this._webglAddonLoading = false;
|
||||
this._webglAddonCustomGlyphs = undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (loadId !== this._webglAddonLoadId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._webglAddonLoading = false;
|
||||
if (!this.raw.element) {
|
||||
this._webglAddonCustomGlyphs = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCustomGlyphs = this._getWebglCustomGlyphs();
|
||||
if (customGlyphs !== currentCustomGlyphs) {
|
||||
this._webglAddonCustomGlyphs = undefined;
|
||||
await this._enableWebglRenderer();
|
||||
return;
|
||||
}
|
||||
|
||||
const Addon = await this._xtermAddonLoader.importAddon('webgl');
|
||||
this._webglAddon = new Addon({
|
||||
customGlyphs: this._terminalConfigurationService.config.customGlyphs
|
||||
customGlyphs
|
||||
});
|
||||
try {
|
||||
this.raw.loadAddon(this._webglAddon);
|
||||
this._logService.trace('Webgl was loaded');
|
||||
this._store.add(this._webglAddon.onContextLoss(() => {
|
||||
this._webglContextLossListener.value = this._webglAddon.onContextLoss(() => {
|
||||
this._logService.info(`Webgl lost context, disposing of webgl renderer`);
|
||||
this._disposeOfWebglRenderer();
|
||||
}));
|
||||
});
|
||||
this._refreshImageAddon();
|
||||
// WebGL renderer cell dimensions differ from the DOM renderer, make sure the terminal
|
||||
// gets resized after the webgl addon is loaded
|
||||
@@ -919,6 +956,11 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
}
|
||||
}
|
||||
|
||||
private _getWebglCustomGlyphs(): boolean {
|
||||
// The custom glyph rasterizer creates a canvas through the rendering document, which is blocked in auxiliary windows.
|
||||
return this._terminalConfigurationService.config.customGlyphs && this.raw.element?.ownerDocument === this._mainDocument;
|
||||
}
|
||||
|
||||
@debounce(100)
|
||||
private async _refreshLigaturesAddon(): Promise<void> {
|
||||
if (!this.raw.element) {
|
||||
@@ -994,6 +1036,10 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
}
|
||||
|
||||
private _disposeOfWebglRenderer(): void {
|
||||
this._webglAddonLoadId++;
|
||||
this._webglAddonLoading = false;
|
||||
this._webglAddonCustomGlyphs = undefined;
|
||||
this._webglContextLossListener.clear();
|
||||
if (!this._webglAddon) {
|
||||
return;
|
||||
}
|
||||
@@ -1003,7 +1049,6 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
// ignore
|
||||
}
|
||||
this._webglAddon = undefined;
|
||||
this._webglAddonCustomGlyphs = undefined;
|
||||
this._refreshImageAddon();
|
||||
// WebGL renderer cell dimensions differ from the DOM renderer, make sure the terminal
|
||||
// gets resized after the webgl addon is disposed
|
||||
@@ -1100,6 +1145,9 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
|
||||
refresh() {
|
||||
this._updateTheme();
|
||||
this._decorationAddon.refreshLayouts();
|
||||
if (this._webglAddon || this._webglAddonLoading) {
|
||||
this._enableWebglRenderer();
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateUnicodeVersion(): Promise<void> {
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
import { deepStrictEqual, ok, strictEqual } from 'assert';
|
||||
import { importAMDNodeModule } from '../../../../../../amdX.js';
|
||||
import { timeout } from '../../../../../../base/common/async.js';
|
||||
import { Color, RGBA } from '../../../../../../base/common/color.js';
|
||||
import { Emitter } from '../../../../../../base/common/event.js';
|
||||
import { toDisposable } from '../../../../../../base/common/lifecycle.js';
|
||||
import { mock } from '../../../../../../base/test/common/mock.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';
|
||||
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { IConfigurationChangeEvent } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js';
|
||||
import { IThemeService } from '../../../../../../platform/theme/common/themeService.js';
|
||||
@@ -102,6 +106,7 @@ suite('XtermTerminal', () => {
|
||||
|
||||
TestWebglAddon.shouldThrow = false;
|
||||
TestWebglAddon.isEnabled = false;
|
||||
TestWebglAddon.customGlyphOptions.length = 0;
|
||||
});
|
||||
|
||||
test('should use fallback dimensions of 80x30', () => {
|
||||
@@ -109,6 +114,86 @@ suite('XtermTerminal', () => {
|
||||
strictEqual(xterm.raw.rows, 30);
|
||||
});
|
||||
|
||||
test('disables custom glyphs when moved into an auxiliary window', async () => {
|
||||
await configurationService.setUserConfiguration('terminal.integrated', {
|
||||
...defaultTerminalConfig,
|
||||
gpuAcceleration: 'on',
|
||||
customGlyphs: true,
|
||||
});
|
||||
configurationService.onDidChangeConfigurationEmitter.fire(new class extends mock<IConfigurationChangeEvent>() {
|
||||
override affectsConfiguration(section: string): boolean {
|
||||
return section.startsWith('terminal.integrated');
|
||||
}
|
||||
});
|
||||
|
||||
const mainContainer = document.createElement('div');
|
||||
document.body.appendChild(mainContainer);
|
||||
store.add(toDisposable(() => mainContainer.remove()));
|
||||
xterm.attachToElement(mainContainer);
|
||||
await timeout(0);
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
store.add(toDisposable(() => iframe.remove()));
|
||||
const auxiliaryDocument = iframe.contentDocument!;
|
||||
const auxiliaryContainer = document.createElement('div');
|
||||
auxiliaryDocument.body.appendChild(auxiliaryContainer);
|
||||
const createElement = auxiliaryDocument.createElement;
|
||||
auxiliaryDocument.createElement = () => {
|
||||
throw new Error('Not allowed to create elements in child window JavaScript context.');
|
||||
};
|
||||
store.add(toDisposable(() => auxiliaryDocument.createElement = createElement));
|
||||
|
||||
auxiliaryContainer.appendChild(xterm.raw.element!);
|
||||
xterm.raw.open(xterm.raw.element!);
|
||||
xterm.refresh();
|
||||
await timeout(0);
|
||||
|
||||
mainContainer.appendChild(xterm.raw.element!);
|
||||
xterm.raw.open(xterm.raw.element!);
|
||||
xterm.refresh();
|
||||
await timeout(0);
|
||||
|
||||
deepStrictEqual(TestWebglAddon.customGlyphOptions, [true, false, true]);
|
||||
});
|
||||
|
||||
test('does not load stale custom glyph settings when moved during addon import', async () => {
|
||||
await configurationService.setUserConfiguration('terminal.integrated', {
|
||||
...defaultTerminalConfig,
|
||||
gpuAcceleration: 'on',
|
||||
customGlyphs: true,
|
||||
});
|
||||
configurationService.onDidChangeConfigurationEmitter.fire(new class extends mock<IConfigurationChangeEvent>() {
|
||||
override affectsConfiguration(section: string): boolean {
|
||||
return section.startsWith('terminal.integrated');
|
||||
}
|
||||
});
|
||||
|
||||
const mainContainer = document.createElement('div');
|
||||
document.body.appendChild(mainContainer);
|
||||
store.add(toDisposable(() => mainContainer.remove()));
|
||||
xterm.attachToElement(mainContainer);
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
store.add(toDisposable(() => iframe.remove()));
|
||||
const auxiliaryDocument = iframe.contentDocument!;
|
||||
const auxiliaryContainer = document.createElement('div');
|
||||
auxiliaryDocument.body.appendChild(auxiliaryContainer);
|
||||
const createElement = auxiliaryDocument.createElement;
|
||||
auxiliaryDocument.createElement = () => {
|
||||
throw new Error('Not allowed to create elements in child window JavaScript context.');
|
||||
};
|
||||
store.add(toDisposable(() => auxiliaryDocument.createElement = createElement));
|
||||
|
||||
auxiliaryContainer.appendChild(xterm.raw.element!);
|
||||
xterm.raw.open(xterm.raw.element!);
|
||||
xterm.refresh();
|
||||
await timeout(0);
|
||||
|
||||
deepStrictEqual(TestWebglAddon.customGlyphOptions, [false]);
|
||||
});
|
||||
|
||||
suite('getContentsAsText', () => {
|
||||
test('should return all buffer contents when no markers provided', async () => {
|
||||
await write('line 1\r\nline 2\r\nline 3\r\nline 4\r\nline 5');
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { WebglAddon } from '@xterm/addon-webgl';
|
||||
import type { IWebglAddonOptions, WebglAddon } from '@xterm/addon-webgl';
|
||||
import type { IEvent } from '@xterm/xterm';
|
||||
import { Emitter } from '../../../../../../base/common/event.js';
|
||||
import { XtermAddonImporter, type IXtermAddonNameToCtor } from '../../../browser/xterm/xtermAddonImporter.js';
|
||||
@@ -11,6 +11,7 @@ import { XtermAddonImporter, type IXtermAddonNameToCtor } from '../../../browser
|
||||
export class TestWebglAddon implements WebglAddon {
|
||||
static shouldThrow = false;
|
||||
static isEnabled = false;
|
||||
static readonly customGlyphOptions: (boolean | undefined)[] = [];
|
||||
private readonly _onChangeTextureAtlas = new Emitter<HTMLCanvasElement>();
|
||||
private readonly _onAddTextureAtlasCanvas = new Emitter<HTMLCanvasElement>();
|
||||
private readonly _onRemoveTextureAtlasCanvas = new Emitter<HTMLCanvasElement>();
|
||||
@@ -19,7 +20,8 @@ export class TestWebglAddon implements WebglAddon {
|
||||
readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event as IEvent<HTMLCanvasElement>;
|
||||
readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event as IEvent<HTMLCanvasElement, void>;
|
||||
readonly onContextLoss = this._onContextLoss.event as IEvent<void>;
|
||||
constructor(preserveDrawingBuffer?: boolean) {
|
||||
constructor(options?: IWebglAddonOptions) {
|
||||
TestWebglAddon.customGlyphOptions.push(options?.customGlyphs);
|
||||
}
|
||||
activate(): void {
|
||||
TestWebglAddon.isEnabled = !TestWebglAddon.shouldThrow;
|
||||
@@ -45,4 +47,3 @@ export class TestXtermAddonImporter extends XtermAddonImporter {
|
||||
return super.importAddon(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user