Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]andTyriar 3e60d791d5 Fix linting issues
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-06-21 18:23:00 +00:00
copilot-swe-agent[bot]andTyriar b3503f4916 Implement lineHeight px format support
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-06-21 18:19:30 +00:00
copilot-swe-agent[bot] 0dffbedf76 Initial plan for issue 2025-06-21 18:01:12 +00:00
15 changed files with 251 additions and 301 deletions
+7 -8
View File
@@ -72,7 +72,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
private _cachedSearchTerm: string | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: IHighlight[] = [];
private _searchResultsWithHighlight: ISearchResult[] = [];
private _selectedDecoration: MutableDisposable<IMultiHighlight> = this._register(new MutableDisposable());
private _highlightLimit: number;
private _lastSearchOptions: ISearchOptions | undefined;
@@ -119,7 +118,6 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
this._selectedDecoration.clear();
dispose(this._highlightDecorations);
this._highlightDecorations = [];
this._searchResultsWithHighlight = [];
this._highlightedLines.clear();
if (!retainCachedSearchTerm) {
this._cachedSearchTerm = undefined;
@@ -169,14 +167,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
// new search, clear out the old decorations
this.clearDecorations(true);
const searchResultsWithHighlight: ISearchResult[] = [];
let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (this._searchResultsWithHighlight.length >= this._highlightLimit) {
if (searchResultsWithHighlight.length >= this._highlightLimit) {
break;
}
prevResult = result;
this._searchResultsWithHighlight.push(prevResult);
searchResultsWithHighlight.push(prevResult);
result = this._find(
term,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
@@ -184,7 +183,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
searchOptions
);
}
for (const match of this._searchResultsWithHighlight) {
for (const match of searchResultsWithHighlight) {
const decorations = this._createResultDecorations(match, searchOptions.decorations!, false);
if (decorations) {
for (const decoration of decorations) {
@@ -351,15 +350,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
let resultIndex = -1;
if (this._selectedDecoration.value) {
const selectedMatch = this._selectedDecoration.value.match;
for (let i = 0; i < this._searchResultsWithHighlight.length; i++) {
const match = this._searchResultsWithHighlight[i];
for (let i = 0; i < this._highlightDecorations.length; i++) {
const match = this._highlightDecorations[i].match;
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
resultIndex = i;
break;
}
}
}
this._onDidChangeResults.fire({ resultIndex, resultCount: this._searchResultsWithHighlight.length });
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
}
}
@@ -480,57 +480,6 @@ test.describe('Search Tests', () => {
]);
});
});
test.describe('Wrapped line search functionality', () => {
test('should correctly count matches across multiple wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'a'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
});
test('should handle reverse search across wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'x'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
});
test('should update counts when content changes across wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'z'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await ctx.proxy.write('\\n\\r' + content);
await timeout(300);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
});
});
});
function makeData(length: number): string {
+10 -1
View File
@@ -10,6 +10,15 @@ import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { NULL_COLOR } from 'common/Color';
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig {
// Convert lineHeight to a numeric multiplier for the char atlas
// If it's a px value, calculate the equivalent multiplier
let lineHeightMultiplier: number = typeof options.lineHeight === 'number' ? options.lineHeight : 1;
if (typeof options.lineHeight === 'string' && options.lineHeight.endsWith('px')) {
const pxValue = parseFloat(options.lineHeight.slice(0, -2));
// Calculate the multiplier based on font size
lineHeightMultiplier = pxValue / options.fontSize;
}
// null out some fields that don't matter
const clonedColors: IColorSet = {
foreground: colors.foreground,
@@ -36,7 +45,7 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
devicePixelRatio,
deviceMaxTextureSize,
letterSpacing: options.letterSpacing,
lineHeight: options.lineHeight,
lineHeight: lineHeightMultiplier,
deviceCellWidth: deviceCellWidth,
deviceCellHeight: deviceCellHeight,
deviceCharWidth: deviceCharWidth,
+20 -7
View File
@@ -27,6 +27,19 @@ import { addDisposableListener } from 'vs/base/browser/dom';
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
/**
* Calculates the line height in pixels based on the lineHeight option and character height.
*/
function calculateLineHeightInPixels(lineHeight: number | string, charHeight: number): number {
if (typeof lineHeight === 'string' && lineHeight.endsWith('px')) {
const pxValue = parseFloat(lineHeight.slice(0, -2));
// Ensure the pixel value is at least as large as the character height
return Math.max(pxValue, charHeight);
}
// Numeric lineHeight acts as a multiplier
return Math.floor(charHeight * (lineHeight as number));
}
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
@@ -139,7 +152,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
[this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
this._isAttached = this._core.screenElement!.isConnected;
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
this._register(toDisposable(() => {
for (const l of this._renderLayers) {
@@ -322,7 +335,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (this._core.screenElement?.isConnected && this._charSizeService.width && this._charSizeService.height) {
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
this._updateDimensions();
this._refreshCharAtlas();
this._isAttached = true;
@@ -570,14 +583,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
// cell.
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
// will always be larger than device char height.
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
// Calculate the device cell height, supporting both numeric multipliers and 'px' values
this.dimensions.device.cell.height = calculateLineHeightInPixels(this._optionsService.rawOptions.lineHeight, this.dimensions.device.char.height);
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
// correctly within the cell.
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
const isDefaultLineHeight = (typeof this._optionsService.rawOptions.lineHeight === 'number' && this._optionsService.rawOptions.lineHeight === 1) ||
(typeof this._optionsService.rawOptions.lineHeight === 'string' && this.dimensions.device.cell.height === this.dimensions.device.char.height);
this.dimensions.device.char.top = isDefaultLineHeight ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
// Calculate the device cell width, taking the letterSpacing into account.
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
+1 -3
View File
@@ -199,9 +199,7 @@ function bufferLine(
let currentRow = startRow;
let bufferStr = '';
while ((currentCol !== endCol || currentRow !== endRow) &&
currentRow >= 0 &&
currentRow < bufferService.buffer.lines.length) {
while (currentCol !== endCol || currentRow !== endRow) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > bufferService.cols - 1) {
+78
View File
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
/**
* Calculates the line height in pixels based on the lineHeight option and character height.
* This is the same function used in both DomRenderer and WebglRenderer.
*/
function calculateLineHeightInPixels(lineHeight: number | string, charHeight: number): number {
if (typeof lineHeight === 'string' && lineHeight.endsWith('px')) {
const pxValue = parseFloat(lineHeight.slice(0, -2));
// Ensure the pixel value is at least as large as the character height
return Math.max(pxValue, charHeight);
}
// Numeric lineHeight acts as a multiplier
return Math.floor(charHeight * (lineHeight as number));
}
describe('calculateLineHeightInPixels', () => {
describe('numeric lineHeight', () => {
it('should calculate correctly with multiplier 1', () => {
assert.equal(calculateLineHeightInPixels(1, 20), 20);
});
it('should calculate correctly with multiplier 1.2', () => {
assert.equal(calculateLineHeightInPixels(1.2, 20), 24);
});
it('should calculate correctly with multiplier 1.5', () => {
assert.equal(calculateLineHeightInPixels(1.5, 20), 30);
});
it('should floor the result', () => {
assert.equal(calculateLineHeightInPixels(1.3, 20), 26); // Math.floor(26)
});
});
describe('string lineHeight with px format', () => {
it('should use px value directly when larger than char height', () => {
assert.equal(calculateLineHeightInPixels('25px', 20), 25);
});
it('should use char height when px value is smaller', () => {
assert.equal(calculateLineHeightInPixels('15px', 20), 20);
});
it('should handle decimal px values', () => {
assert.equal(calculateLineHeightInPixels('23.5px', 20), 23.5);
});
it('should handle edge case where px equals char height', () => {
assert.equal(calculateLineHeightInPixels('20px', 20), 20);
});
it('should handle very large px values', () => {
assert.equal(calculateLineHeightInPixels('100px', 20), 100);
});
});
describe('edge cases', () => {
it('should handle zero char height with numeric multiplier', () => {
assert.equal(calculateLineHeightInPixels(1.5, 0), 0);
});
it('should handle zero char height with px value', () => {
assert.equal(calculateLineHeightInPixels('25px', 0), 25);
});
it('should handle 1px with various char heights', () => {
assert.equal(calculateLineHeightInPixels('1px', 5), 5);
assert.equal(calculateLineHeightInPixels('1px', 20), 20);
assert.equal(calculateLineHeightInPixels('1px', 0), 1);
});
});
});
+14 -1
View File
@@ -24,6 +24,19 @@ const BG_CLASS_PREFIX = 'xterm-bg-';
const FOCUS_CLASS = 'xterm-focus';
const SELECTION_CLASS = 'xterm-selection';
/**
* Calculates the line height in pixels based on the lineHeight option and character height.
*/
function calculateLineHeightInPixels(lineHeight: number | string, charHeight: number): number {
if (typeof lineHeight === 'string' && lineHeight.endsWith('px')) {
const pxValue = parseFloat(lineHeight.slice(0, -2));
// Ensure the pixel value is at least as large as the character height
return Math.max(pxValue, charHeight);
}
// Numeric lineHeight acts as a multiplier
return Math.floor(charHeight * (lineHeight as number));
}
let nextTerminalId = 1;
/**
@@ -116,7 +129,7 @@ export class DomRenderer extends Disposable implements IRenderer {
this.dimensions.device.char.width = this._charSizeService.width * dpr;
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
this.dimensions.device.cell.height = calculateLineHeightInPixels(this._optionsService.rawOptions.lineHeight, this.dimensions.device.char.height);
this.dimensions.device.char.left = 0;
this.dimensions.device.char.top = 0;
this.dimensions.device.canvas.width = this.dimensions.device.cell.width * this._bufferService.cols;
-169
View File
@@ -1,169 +0,0 @@
/**
* Test for Alt+- keyboard handling specifically
* This tests both the evaluateKeyboardEvent function and potential browser integration issues
*/
import { assert } from 'chai';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { IKeyboardResult, IKeyboardEvent } from 'common/Types';
function createMockKeyboardEvent(partialEvent: Partial<IKeyboardEvent>): IKeyboardEvent {
return {
altKey: false,
ctrlKey: false,
shiftKey: false,
metaKey: false,
keyCode: 0,
code: '',
key: '',
type: 'keydown',
...partialEvent
};
}
describe('Alt+- keyboard handling', () => {
it('should return correct escape sequence for Alt+- on all platforms', () => {
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
// Test on non-Mac (Linux/Windows)
const resultLinux = evaluateKeyboardEvent(event, false, false, false);
assert.equal(resultLinux.key, '\x1b-', 'Alt+- should produce \\x1b- on Linux');
assert.equal(resultLinux.cancel, false, 'Alt+- should not be cancelled on Linux');
// Test on Windows
const resultWindows = evaluateKeyboardEvent(event, false, false, false);
assert.equal(resultWindows.key, '\x1b-', 'Alt+- should produce \\x1b- on Windows');
// Test on Mac with macOptionIsMeta: true
const resultMacMeta = evaluateKeyboardEvent(event, false, true, true);
assert.equal(resultMacMeta.key, '\x1b-', 'Alt+- should produce \\x1b- on Mac with macOptionIsMeta');
// Test on Mac with macOptionIsMeta: false (third level shift)
const resultMacNoMeta = evaluateKeyboardEvent(event, false, true, false);
assert.equal(resultMacNoMeta.key, undefined, 'Alt+- should not produce escape sequence on Mac without macOptionIsMeta');
});
it('should handle Alt+- with shift key (underscore)', () => {
const event = createMockKeyboardEvent({
altKey: true,
shiftKey: true,
keyCode: 189,
key: '_',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
assert.equal(result.key, '\x1b_', 'Alt+Shift+- should produce \\x1b_');
});
it('should verify keyCode 189 maps to minus character', () => {
// This test verifies that keyCode 189 is correctly mapped in KEYCODE_KEY_MAPPINGS
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
assert.isDefined(result.key, 'Result should have a key');
assert.equal(result.key, '\x1b-', 'Should produce the correct escape sequence');
});
it('should handle various browser scenarios for Alt+-', () => {
// Test with different keyCode values that might be used for minus
const keyCodes = [189, 109]; // 189 is standard minus, 109 is numpad minus
keyCodes.forEach(keyCode => {
const event = createMockKeyboardEvent({
altKey: true,
keyCode: keyCode,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
if (keyCode === 189) {
assert.equal(result.key, '\x1b-', `keyCode ${keyCode} should produce \\x1b-`);
} else {
// For numpad minus or other codes, the behavior might be different
// but we should still handle them gracefully
assert.isDefined(result, `keyCode ${keyCode} should return a result`);
}
});
});
it('should not be affected by composition state for regular Alt combinations', () => {
// This tests that Alt+- is not treated as a composition character
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
// Ensure the keyCode is not 229 (composition character)
assert.notEqual(event.keyCode, 229, 'Alt+- should not use composition keyCode');
const result = evaluateKeyboardEvent(event, false, false, false);
assert.equal(result.key, '\x1b-', 'Alt+- should work normally without composition interference');
});
describe('Browser integration considerations', () => {
it('should document known scenarios where Alt+- might not work', () => {
// This test documents potential causes for the reported issue
// NOTE: These are integration issues, not keyboard evaluation issues
const possibleCauses = [
'Terminal not properly focused when testing',
'Custom key event handlers preventing default behavior',
'IME or composition methods intercepting events',
'Browser-specific keyboard layout differences',
'Third-party browser extensions interfering'
];
// The keyboard evaluation should always work correctly
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
assert.equal(result.key, '\x1b-', 'Keyboard evaluation should always work');
// Document that integration issues are separate concerns
assert.isTrue(possibleCauses.length > 0, 'There are known integration scenarios to consider');
});
it('should handle different keyboard layouts gracefully', () => {
// Some keyboard layouts might use different keyCodes for minus
const potentialMinusKeyCodes = [189, 173, 109]; // Various minus key codes
potentialMinusKeyCodes.forEach(keyCode => {
const event = createMockKeyboardEvent({
altKey: true,
keyCode: keyCode,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
if (keyCode === 189) {
// Standard minus key should always work
assert.equal(result.key, '\x1b-', `Standard minus (keyCode ${keyCode}) should work`);
} else {
// Other keys might not be mapped, but should not throw errors
assert.isDefined(result, `KeyCode ${keyCode} should return a valid result`);
}
});
});
});
});
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { OptionsService } from 'common/services/OptionsService';
describe('OptionsService lineHeight', () => {
let optionsService: OptionsService;
beforeEach(() => {
optionsService = new OptionsService({});
});
describe('numeric lineHeight', () => {
it('should accept numeric values', () => {
optionsService.options.lineHeight = 1.5;
assert.equal(optionsService.rawOptions.lineHeight, 1.5);
});
it('should reject values less than 1', () => {
assert.throws(() => optionsService.options.lineHeight = 0.5);
});
it('should accept 1 as minimum value', () => {
optionsService.options.lineHeight = 1;
assert.equal(optionsService.rawOptions.lineHeight, 1);
});
});
describe('string lineHeight with px format', () => {
it('should accept px values', () => {
optionsService.options.lineHeight = '24px';
assert.equal(optionsService.rawOptions.lineHeight, '24px');
});
it('should accept decimal px values', () => {
optionsService.options.lineHeight = '23.5px';
assert.equal(optionsService.rawOptions.lineHeight, '23.5px');
});
it('should reject px values less than 1', () => {
assert.throws(() => optionsService.options.lineHeight = '0.5px');
});
it('should accept 1px as minimum value', () => {
optionsService.options.lineHeight = '1px';
assert.equal(optionsService.rawOptions.lineHeight, '1px');
});
it('should reject non-px string values', () => {
assert.throws(() => optionsService.options.lineHeight = '24' as any);
assert.throws(() => optionsService.options.lineHeight = '24em' as any);
assert.throws(() => optionsService.options.lineHeight = 'normal' as any);
});
it('should reject invalid px values', () => {
assert.throws(() => optionsService.options.lineHeight = 'invalidpx' as any);
assert.throws(() => optionsService.options.lineHeight = 'px' as any);
});
});
describe('invalid lineHeight types', () => {
it('should reject null', () => {
assert.throws(() => optionsService.options.lineHeight = null as any);
});
it('should reject undefined when explicitly set', () => {
assert.throws(() => optionsService.options.lineHeight = undefined as any);
});
it('should reject boolean values', () => {
assert.throws(() => optionsService.options.lineHeight = true as any);
assert.throws(() => optionsService.options.lineHeight = false as any);
});
it('should reject arrays', () => {
assert.throws(() => optionsService.options.lineHeight = [] as any);
});
it('should reject objects', () => {
assert.throws(() => optionsService.options.lineHeight = {} as any);
});
});
});
+20 -1
View File
@@ -172,12 +172,31 @@ export class OptionsService extends Disposable implements IOptionsService {
case 'cursorWidth':
value = Math.floor(value);
// Fall through for bounds check
case 'lineHeight':
case 'tabStopWidth':
if (value < 1) {
throw new Error(`${key} cannot be less than 1, value: ${value}`);
}
break;
case 'lineHeight':
if (typeof value === 'string') {
// Handle 'px' format
if (value.endsWith('px')) {
const pxValue = parseFloat(value.slice(0, -2));
if (isNaN(pxValue) || pxValue < 1) {
throw new Error(`${key} with 'px' format must be a valid number >= 1, value: ${value}`);
}
// Keep the string value as-is for downstream processing
} else {
throw new Error(`${key} string value must end with 'px', value: ${value}`);
}
} else if (typeof value === 'number') {
if (value < 1) {
throw new Error(`${key} cannot be less than 1, value: ${value}`);
}
} else {
throw new Error(`${key} must be a number or string with 'px' suffix, value: ${value}`);
}
break;
case 'minimumContrastRatio':
value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));
break;
+1 -1
View File
@@ -231,7 +231,7 @@ export interface ITerminalOptions {
fontWeightBold?: FontWeight;
ignoreBracketedPasteMode?: boolean;
letterSpacing?: number;
lineHeight?: number;
lineHeight?: number | string;
linkHandler?: ILinkHandler | null;
logLevel?: LogLevel;
logger?: ILogger | null;
+2 -9
View File
@@ -1280,10 +1280,10 @@ enum CellColorPosition {
* treatment.
*/
export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext, setupCb: () => Promise<void> | void): void {
const setupTests = ({ shadowDom }: { shadowDom: boolean }): void => {
test.describe('standalone tests', () => {
test.beforeEach(async () => {
// Recreate terminal
await openTerminal(ctx.value, {}, { useShadowDom: shadowDom });
await openTerminal(ctx.value);
await ctx.value.page.evaluate(`
window.term.options.minimumContrastRatio = 1;
window.term.options.allowTransparency = false;
@@ -1308,13 +1308,6 @@ export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestCont
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
});
});
};
test.describe('standalone tests', () => {
setupTests({ shadowDom: false });
});
test.describe('standalone tests (Shadow dom)', () => {
setupTests({ shadowDom: true });
});
}
+4 -46
View File
@@ -412,14 +412,7 @@ class TerminalCoreProxy {
}
}
export async function openTerminal(
ctx: ITestContext,
options: ITerminalOptions | ITerminalInitOnlyOptions = {},
testOptions: { useShadowDom?: boolean, loadUnicodeGraphemesAddon?: boolean } = {}
): Promise<void> {
testOptions.useShadowDom ??= false;
testOptions.loadUnicodeGraphemesAddon ??= true;
export async function openTerminal(ctx: ITestContext, options: ITerminalOptions | ITerminalInitOnlyOptions = {}, testOptions: { loadUnicodeGraphemesAddon: boolean } = { loadUnicodeGraphemesAddon: true }): Promise<void> {
await ctx.page.evaluate(`
if ('term' in window) {
try {
@@ -430,45 +423,10 @@ export async function openTerminal(
// HACK: Tests may have side effects that could cause the terminal not to be removed. This
// assertion catches this case early.
strictEqual(await ctx.page.evaluate(`document.querySelector('#terminal-container').children.length`), 0, 'there must be no terminals on the page');
let script = `
await ctx.page.evaluate(`
window.term = new window.Terminal(${JSON.stringify({ allowProposedApi: true, ...options })});
let element = document.querySelector('#terminal-container');
// Remove shadow root if it exists
const newElement = element.cloneNode(false);
element.replaceWith(newElement);
element = newElement
`;
if (testOptions.useShadowDom) {
script += `
const shadowRoot = element.attachShadow({ mode: "open" });
// Copy parent styles to shadow DOM
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
styles.forEach((styleEl) => {
const clone = document.createElement('link');
clone.rel = 'stylesheet';
clone.href = styleEl.href;
shadowRoot.appendChild(clone);
});
// Create new element inside the shadow DOM
element = document.createElement('div');
element.style.width = '100%';
element.style.height = '100%';
shadowRoot.appendChild(element);
`;
}
script += `
window.term.open(element);
`;
await ctx.page.evaluate(script);
window.term.open(document.querySelector('#terminal-container'));
`);
// HACK: This is a soft layer breaker that's temporarily included until unicode graphemes have
// more complete integration tests. See https://github.com/xtermjs/xterm.js/pull/4519#discussion_r1285234453
if (testOptions.loadUnicodeGraphemesAddon) {
+4 -2
View File
@@ -94,9 +94,11 @@ declare module '@xterm/headless' {
letterSpacing?: number;
/**
* The line height used to render text.
* The line height used to render text. This can be specified as:
* - A number (e.g., 1.2) which acts as a multiplier of the font size
* - A string with 'px' suffix (e.g., '24px') which specifies an absolute pixel height
*/
lineHeight?: number;
lineHeight?: number | string;
/**
* What log level to use, this will log for all levels below and including
+4 -2
View File
@@ -152,9 +152,11 @@ declare module '@xterm/xterm' {
letterSpacing?: number;
/**
* The line height used to render text.
* The line height used to render text. This can be specified as:
* - A number (e.g., 1.2) which acts as a multiplier of the font size
* - A string with 'px' suffix (e.g., '24px') which specifies an absolute pixel height
*/
lineHeight?: number;
lineHeight?: number | string;
/**
* The handler for OSC 8 hyperlinks. Links will use the `confirm` browser