mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
@@ -56,35 +56,35 @@ describe('WebLinksAddon', () => {
|
||||
it('all half width', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, 'aaa http://example.com aaa http://example.com aaa');
|
||||
await resetAndHover(5, 1);
|
||||
await resetAndHover(5, 0);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } });
|
||||
await resetAndHover(1, 2);
|
||||
await resetAndHover(1, 1);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } });
|
||||
});
|
||||
it('url after full width', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥ http://example.com ¥¥¥ http://example.com aaa');
|
||||
await resetAndHover(8, 1);
|
||||
await resetAndHover(8, 0);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } });
|
||||
await resetAndHover(1, 2);
|
||||
await resetAndHover(1, 1);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } });
|
||||
});
|
||||
it('full width within url and before', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥');
|
||||
await resetAndHover(8, 1);
|
||||
await resetAndHover(8, 0);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
|
||||
await resetAndHover(1, 2);
|
||||
await resetAndHover(1, 1);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
|
||||
await resetAndHover(17, 2);
|
||||
await resetAndHover(17, 1);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } });
|
||||
});
|
||||
it('name + password url after full width and combining', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path');
|
||||
await resetAndHover(12, 1);
|
||||
await resetAndHover(12, 0);
|
||||
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
|
||||
await resetAndHover(13, 2);
|
||||
await resetAndHover(5, 1);
|
||||
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
|
||||
});
|
||||
});
|
||||
@@ -101,35 +101,36 @@ async function testHostName(hostname: string): Promise<void> {
|
||||
`\\'http://${hostname}/\\'\\r\\n` +
|
||||
`http://${hostname}/subpath/+/id`;
|
||||
await writeSync(page, data);
|
||||
await pollForLinkAtCell(3, 1, `http://${hostname}`);
|
||||
await pollForLinkAtCell(3, 2, `http://${hostname}/a~b#c~d?e~f`);
|
||||
await pollForLinkAtCell(3, 0, `http://${hostname}`);
|
||||
await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`);
|
||||
await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`);
|
||||
await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`);
|
||||
await pollForLinkAtCell(3, 4, `http://${hostname}/colon:test`);
|
||||
await pollForLinkAtCell(2, 4, `http://${hostname}/`);
|
||||
await pollForLinkAtCell(2, 5, `http://${hostname}/`);
|
||||
await pollForLinkAtCell(2, 6, `http://${hostname}/`);
|
||||
await pollForLinkAtCell(1, 7, `http://${hostname}/subpath/+/id`);
|
||||
await pollForLinkAtCell(1, 6, `http://${hostname}/subpath/+/id`);
|
||||
}
|
||||
|
||||
async function pollForLinkAtCell(col: number, row: number, value: string): Promise<void> {
|
||||
const rowSelector = `.xterm-rows > :nth-child(${row})`;
|
||||
// Ensure the hover element exists before trying to hover it
|
||||
await pollFor(page, `!!document.querySelector('${rowSelector} > :nth-child(${col})')`, true);
|
||||
await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
|
||||
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
|
||||
await page.mouse.move(...(await cellPos(col, row)));
|
||||
await pollFor(page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true);
|
||||
const text = await page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`);
|
||||
assert.deepEqual(text, value);
|
||||
}
|
||||
|
||||
async function setupCustom(): Promise<void> {
|
||||
await openTerminal(page, { cols: 40 });
|
||||
await page.evaluate(`window._linkStateData = {};
|
||||
await page.evaluate(`window._linkStateData = {uri:''};
|
||||
window._linkaddon = new window.WebLinksAddon();
|
||||
window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; };
|
||||
window.term.loadAddon(window._linkaddon);`);
|
||||
}
|
||||
|
||||
async function resetAndHover(col: number, row: number): Promise<void> {
|
||||
await page.evaluate(`window._linkStateData = {};`);
|
||||
const rowSelector = `.xterm-rows > :nth-child(${row})`;
|
||||
await page.hover(`${rowSelector} > :nth-child(${col})`);
|
||||
await page.mouse.move(0, 0);
|
||||
await page.evaluate(`window._linkStateData = {uri:''};`);
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
await page.mouse.move(...(await cellPos(col, row)));
|
||||
await pollFor(page, `!!window._linkStateData.uri.length`, true);
|
||||
}
|
||||
|
||||
async function evalLinkStateData(uri: string, range: any): Promise<void> {
|
||||
@@ -137,3 +138,14 @@ async function evalLinkStateData(uri: string, range: any): Promise<void> {
|
||||
assert.equal(data.uri, uri);
|
||||
assert.deepEqual(data.range, range);
|
||||
}
|
||||
|
||||
async function cellPos(col: number, row: number): Promise<[number, number]> {
|
||||
const coords: any = await page.evaluate(`
|
||||
(function() {
|
||||
const rect = window.term.element.getBoundingClientRect();
|
||||
const dim = term._core._renderService.dimensions;
|
||||
return {left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right, width: dim.css.cell.width, height: dim.css.cell.height};
|
||||
})();
|
||||
`);
|
||||
return [col * coords.width + coords.left + 2, row * coords.height + coords.top + 2];
|
||||
}
|
||||
|
||||
+17
-1
@@ -236,6 +236,7 @@ if (document.location.pathname === '/test') {
|
||||
document.getElementById('add-decoration').addEventListener('click', addDecoration);
|
||||
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
|
||||
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
|
||||
document.getElementById('bce').addEventListener('click', coloredErase);
|
||||
addVtButtons();
|
||||
initImageAddonExposed();
|
||||
}
|
||||
@@ -1212,11 +1213,26 @@ ipv6 https://[::1]/with/some?vars=and&a#hash aaa
|
||||
stop at final '.': This is a sentence with an url to http://example.com.
|
||||
stop at final '?': Is this the right url http://example.com/?
|
||||
stop at final '?': Maybe this one http://example.com/with?arguments=false?
|
||||
`;
|
||||
`;
|
||||
term.write(linkExamples.split('\n').join('\r\n'));
|
||||
}
|
||||
|
||||
|
||||
function coloredErase(): void {
|
||||
const data = `
|
||||
Test BG-colored Erase (BCE):
|
||||
The color block in the following lines should look identical.
|
||||
For newly created rows at the bottom the last color should be applied
|
||||
for all cells to the right.
|
||||
|
||||
def 41 42 43 44 45 46 47\x1b[47m
|
||||
\x1b[m \x1b[41m \x1b[42m \x1b[43m \x1b[44m \x1b[45m \x1b[46m \x1b[47m
|
||||
\x1b[m\x1b[5X\x1b[41m\x1b[5C\x1b[5X\x1b[42m\x1b[5C\x1b[5X\x1b[43m\x1b[5C\x1b[5X\x1b[44m\x1b[5C\x1b[5X\x1b[45m\x1b[5C\x1b[5X\x1b[46m\x1b[5C\x1b[5X\x1b[47m\x1b[5C\x1b[5X\x1b[m
|
||||
`;
|
||||
term.write(data.split('\n').join('\r\n'));
|
||||
}
|
||||
|
||||
|
||||
function initImageAddonExposed(): void {
|
||||
const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts;
|
||||
const limitStorageElement = document.querySelector<HTMLInputElement>('#image-storagelimit');
|
||||
|
||||
@@ -96,6 +96,7 @@
|
||||
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
|
||||
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
|
||||
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
|
||||
<dd><button id="bce" title="Test colored erase">Colored Erase (BCE)</button></dd>
|
||||
|
||||
<dt>Decorations</dt>
|
||||
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DIM_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { DomRendererRowFactory, RowCss } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { WidthCache } from 'browser/renderer/dom/WidthCache';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
@@ -15,6 +16,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IInstantiationService, IOptionsService } from 'common/services/Services';
|
||||
import { createStyle, IStyleSheet } from './StyleSheet';
|
||||
|
||||
|
||||
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
||||
const ROW_CONTAINER_CLASS = 'xterm-rows';
|
||||
const FG_CLASS_PREFIX = 'xterm-fg-';
|
||||
@@ -24,6 +26,7 @@ const SELECTION_CLASS = 'xterm-selection';
|
||||
|
||||
let nextTerminalId = 1;
|
||||
|
||||
|
||||
/**
|
||||
* A fallback renderer for when canvas is slow. This is not meant to be
|
||||
* particularly fast or feature complete, more just stable and usable for when
|
||||
@@ -38,7 +41,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[] = [];
|
||||
private _selectionContainer: HTMLElement;
|
||||
private _cellToRowElements: Int16Array[] = [];
|
||||
private _widthCache: WidthCache;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
@@ -91,7 +94,17 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._selectionContainer.remove();
|
||||
this._themeStyle.dispose();
|
||||
this._dimensionsStyle.dispose();
|
||||
this._widthCache.dispose();
|
||||
}));
|
||||
|
||||
this._widthCache = new WidthCache(document);
|
||||
this._widthCache.setFont(
|
||||
this._optionsService.rawOptions.fontFamily,
|
||||
this._optionsService.rawOptions.fontSize,
|
||||
this._optionsService.rawOptions.fontWeight,
|
||||
this._optionsService.rawOptions.fontWeightBold
|
||||
);
|
||||
this._setDefaultSpacing();
|
||||
}
|
||||
|
||||
private _updateDimensions(): void {
|
||||
@@ -123,10 +136,9 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
const styles =
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
|
||||
` display: inline-block;` +
|
||||
` display: inline-block;` + // TODO: find workaround for inline-block (creates ~20% render penalty)
|
||||
` height: 100%;` +
|
||||
` vertical-align: top;` +
|
||||
` width: ${this.dimensions.css.cell.width}px` +
|
||||
`}`;
|
||||
|
||||
this._dimensionsStyle.setCss(styles);
|
||||
@@ -147,6 +159,8 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
` color: ${colors.foreground.css};` +
|
||||
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
|
||||
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
|
||||
` font-kerning: none;` +
|
||||
` white-space: pre` +
|
||||
`}`;
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .xterm-dim {` +
|
||||
@@ -154,13 +168,13 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
`}`;
|
||||
// Text styles
|
||||
styles +=
|
||||
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
|
||||
`${this._terminalSelector} span:not(.${RowCss.BOLD_CLASS}) {` +
|
||||
` font-weight: ${this._optionsService.rawOptions.fontWeight};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} span.${BOLD_CLASS} {` +
|
||||
`${this._terminalSelector} span.${RowCss.BOLD_CLASS} {` +
|
||||
` font-weight: ${this._optionsService.rawOptions.fontWeightBold};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
|
||||
`${this._terminalSelector} span.${RowCss.ITALIC_CLASS} {` +
|
||||
` font-style: italic;` +
|
||||
`}`;
|
||||
// Blink animation
|
||||
@@ -177,33 +191,33 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
` color: ${colors.cursorAccent.css};` +
|
||||
` }` +
|
||||
` 50% {` +
|
||||
` background-color: ${colors.cursorAccent.css};` +
|
||||
` background-color: inherit;` +
|
||||
` color: ${colors.cursor.css};` +
|
||||
` }` +
|
||||
`}`;
|
||||
// Cursor
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} ,` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} ,` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} ` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} ,` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} ,` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} ` +
|
||||
`{` +
|
||||
` outline: 1px solid ${colors.cursor.css};` +
|
||||
` outline-offset: -1px;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` +
|
||||
` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` background-color: ${colors.cursor.css};` +
|
||||
` color: ${colors.cursorAccent.css};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
|
||||
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
` border-bottom: 1px ${colors.cursor.css};` +
|
||||
` border-bottom-style: solid;` +
|
||||
` height: calc(100% - 1px);` +
|
||||
@@ -229,19 +243,36 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
for (const [i, c] of colors.ansi.entries()) {
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i}.${DIM_CLASS} { color: ${color.multiplyOpacity(c, 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i}.${RowCss.DIM_CLASS} { color: ${color.multiplyOpacity(c, 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
|
||||
}
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` +
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR}.${DIM_CLASS} { color: ${color.multiplyOpacity(color.opaque(colors.background), 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR}.${RowCss.DIM_CLASS} { color: ${color.multiplyOpacity(color.opaque(colors.background), 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`;
|
||||
|
||||
this._themeStyle.setCss(styles);
|
||||
}
|
||||
|
||||
/**
|
||||
* default letter spacing
|
||||
* Due to rounding issues in dimensions dpr calc glyph might render
|
||||
* slightly too wide or too narrow. The method corrects the stacking offsets
|
||||
* by applying a default letter-spacing for all chars.
|
||||
* The value gets passed to the row factory to avoid setting this value again
|
||||
* (render speedup is roughly 10%).
|
||||
*/
|
||||
private _setDefaultSpacing(): void {
|
||||
// measure same char as in CharSizeService to get the base deviation
|
||||
const spacing = this.dimensions.css.cell.width - this._widthCache.get('W', false, false);
|
||||
this._rowContainer.style.letterSpacing = `${spacing}px`;
|
||||
this._rowFactory.defaultSpacing = spacing;
|
||||
}
|
||||
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
this._updateDimensions();
|
||||
this._widthCache.clear();
|
||||
this._setDefaultSpacing();
|
||||
}
|
||||
|
||||
private _refreshRowElements(cols: number, rows: number): void {
|
||||
@@ -264,6 +295,8 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
public handleCharSizeChanged(): void {
|
||||
this._updateDimensions();
|
||||
this._widthCache.clear();
|
||||
this._setDefaultSpacing();
|
||||
}
|
||||
|
||||
public handleBlur(): void {
|
||||
@@ -276,10 +309,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
// Remove all selections
|
||||
while (this._selectionContainer.children.length) {
|
||||
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
|
||||
}
|
||||
|
||||
this._selectionContainer.replaceChildren();
|
||||
this._rowFactory.handleSelectionChanged(start, end, columnSelectMode);
|
||||
this.renderRows(0, this._bufferService.rows - 1);
|
||||
|
||||
@@ -349,6 +379,14 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._updateDimensions();
|
||||
// Refresh CSS
|
||||
this._injectCss(this._themeService.colors);
|
||||
// update spacing cache
|
||||
this._widthCache.setFont(
|
||||
this._optionsService.rawOptions.fontFamily,
|
||||
this._optionsService.rawOptions.fontSize,
|
||||
this._optionsService.rawOptions.fontWeight,
|
||||
this._optionsService.rawOptions.fontWeightBold
|
||||
);
|
||||
this._setDefaultSpacing();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
@@ -365,19 +403,33 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public renderRows(start: number, end: number): void {
|
||||
const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
|
||||
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
|
||||
const buffer = this._bufferService.buffer;
|
||||
const cursorAbsoluteY = buffer.ybase + buffer.y;
|
||||
const cursorX = Math.min(buffer.x, this._bufferService.cols - 1);
|
||||
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const row = y + buffer.ydisp;
|
||||
const rowElement = this._rowElements[y];
|
||||
const row = y + this._bufferService.buffer.ydisp;
|
||||
const lineData = this._bufferService.buffer.lines.get(row);
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
if (!this._cellToRowElements[y] || this._cellToRowElements[y].length !== this._bufferService.cols) {
|
||||
this._cellToRowElements[y] = new Int16Array(this._bufferService.cols);
|
||||
const lineData = buffer.lines.get(row);
|
||||
if (!rowElement || !lineData) {
|
||||
break;
|
||||
}
|
||||
rowElement.replaceChildren(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.css.cell.width, this._bufferService.cols, this._cellToRowElements[y]));
|
||||
rowElement.replaceChildren(
|
||||
...this._rowFactory.createRow(
|
||||
lineData,
|
||||
row,
|
||||
row === cursorAbsoluteY,
|
||||
cursorStyle,
|
||||
cursorX,
|
||||
cursorBlink,
|
||||
this.dimensions.css.cell.width,
|
||||
this._widthCache,
|
||||
-1,
|
||||
-1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,40 +461,42 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
* - (async) link handler race condition: new buffer metrics, but still on old render state
|
||||
* - (async) render update: brings term metrics and render state back in sync
|
||||
*/
|
||||
// clip coords into viewport
|
||||
if (y < 0) x = 0;
|
||||
if (y2 < 0) x2 = 0;
|
||||
|
||||
// avoid out-of-sync y-values, simply clamp into valid area
|
||||
const maxY = this._cellToRowElements.length - 1;
|
||||
const maxY = this._bufferService.rows - 1;
|
||||
y = Math.max(Math.min(y, maxY), 0);
|
||||
y2 = Math.max(Math.min(y2, maxY), 0);
|
||||
const elemY = this._cellToRowElements[y];
|
||||
const elemY2 = this._cellToRowElements[y2];
|
||||
if (x >= elemY.length || x2 >= elemY2.length) {
|
||||
// avoid out-of-sync x-values
|
||||
// simply exit early, gets fixed by the next render update
|
||||
return;
|
||||
}
|
||||
x = elemY[x];
|
||||
x2 = elemY2[x2];
|
||||
|
||||
if (x === -1 || x2 === -1) {
|
||||
return;
|
||||
}
|
||||
cols = Math.min(cols, this._bufferService.cols);
|
||||
const buffer = this._bufferService.buffer;
|
||||
const cursorAbsoluteY = buffer.ybase + buffer.y;
|
||||
const cursorX = Math.min(buffer.x, cols - 1);
|
||||
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
|
||||
while (x !== x2 || y !== y2) {
|
||||
const row = this._rowElements[y];
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const span = row.children[x] as HTMLElement;
|
||||
if (span) {
|
||||
span.style.textDecoration = enabled ? 'underline' : 'none';
|
||||
}
|
||||
if (++x >= cols) {
|
||||
x = 0;
|
||||
y++;
|
||||
// refresh rows within link range
|
||||
for (let i = y; i <= y2; ++i) {
|
||||
const row = i + buffer.ydisp;
|
||||
const rowElement = this._rowElements[i];
|
||||
const bufferline = buffer.lines.get(row);
|
||||
if (!rowElement || !bufferline) {
|
||||
break;
|
||||
}
|
||||
rowElement.replaceChildren(
|
||||
...this._rowFactory.createRow(
|
||||
bufferline,
|
||||
row,
|
||||
row === cursorAbsoluteY,
|
||||
cursorStyle,
|
||||
cursorX,
|
||||
cursorBlink,
|
||||
this.dimensions.css.cell.width,
|
||||
this._widthCache,
|
||||
enabled ? (i === y ? x : 0) : -1,
|
||||
enabled ? ((i === y2 ? x2 : cols) - 1) : -1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { css } from 'common/Color';
|
||||
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
|
||||
import { TestWidthCache } from 'browser/renderer/dom/WidthCache.test';
|
||||
|
||||
|
||||
const EMPTY_WIDTH = new TestWidthCache(new jsdom.JSDOM('').window.document);
|
||||
|
||||
const EMPTY_ELEM_MAPPING = new Int16Array(1000);
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
@@ -37,54 +39,46 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
describe('createRow', () => {
|
||||
it('should not create anything for an empty row', () => {
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
it('should set correct attributes for double width characters', () => {
|
||||
EMPTY_WIDTH.widths['語'] = [10, 10, 10, 10];
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
|
||||
// There should be no element for the following "empty" cell
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="width: 10px;">語</span>'
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>語</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should add class for cursor and cursor style', () => {
|
||||
for (const style of ['block', 'bar', 'underline']) {
|
||||
const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should add class for cursor blink', () => {
|
||||
const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
`<span class="xterm-cursor xterm-cursor-blink xterm-cursor-block"> </span>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render cells that go beyond the terminal\'s columns', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span>a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
describe('attributes', () => {
|
||||
it('should add class for bold', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bold">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -93,8 +87,8 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-italic">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -103,8 +97,8 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-dim">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -116,8 +110,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-underline-1">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -127,8 +121,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-underline-2">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -138,8 +132,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.CURLY;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-underline-3">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -149,8 +143,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-underline-4">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -160,8 +154,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DASHED;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-underline-5">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -171,8 +165,8 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-overline">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -181,8 +175,8 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-strikethrough">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -194,8 +188,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.fg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
`<span class="xterm-fg-${i}">a</span>`
|
||||
);
|
||||
}
|
||||
@@ -208,8 +202,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.bg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
`<span class="xterm-bg-${i}">a</span>`
|
||||
);
|
||||
}
|
||||
@@ -220,8 +214,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -231,8 +225,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -241,8 +235,8 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -254,8 +248,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.fg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
|
||||
);
|
||||
}
|
||||
@@ -266,8 +260,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span style="background-color:#040506;color:#010203;">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -277,8 +271,8 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span style="background-color:#010203;color:#040506;">a</span>'
|
||||
);
|
||||
});
|
||||
@@ -289,25 +283,179 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>a</span><span class="xterm-decoration-top">b</span>'
|
||||
);
|
||||
});
|
||||
it('should force whitespace cells to be rendered above the background', () => {
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-decoration-top"> </span><span class="xterm-decoration-top">a</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getFragmentHtml(fragment: DocumentFragment): string {
|
||||
describe('createRow with merged spans', () => {
|
||||
// for test purpose assume all in codepoints 0..255 are merging
|
||||
// const ALL_MERGING = new Uint8Array(FontMetrics.MAX);
|
||||
|
||||
beforeEach(() => {
|
||||
lineData = createEmptyLineData(10);
|
||||
});
|
||||
|
||||
it('should not create anything for an empty row', () => {
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
''
|
||||
);
|
||||
});
|
||||
|
||||
it('can merge codepoints for equal spacing', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>abc</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not merge codepoints with different spacing', () => {
|
||||
EMPTY_WIDTH.widths['€'] = [2, 2, 2, 2];
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not merge on FG change', () => {
|
||||
const aColor1 = CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
aColor1.fg |= Attributes.CM_P16 | 1;
|
||||
const bColor2 = CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
bColor2.fg |= Attributes.CM_P16 | 2;
|
||||
lineData.setCell(0, aColor1);
|
||||
lineData.setCell(1, aColor1);
|
||||
lineData.setCell(2, bColor2);
|
||||
lineData.setCell(3, bColor2);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-fg-1">aa</span><span class="xterm-fg-2">bb</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not merge cursor cell', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'X', 1, 'X'.charCodeAt(0)]));
|
||||
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
const spans = rowFactory.createRow(lineData, 0, true, undefined, 2, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>aa</span><span class="xterm-cursor xterm-cursor-block">X</span><span>bb</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle BCE correctly', () => {
|
||||
const nullCell = lineData.loadCell(0, new CellData());
|
||||
nullCell.bg = Attributes.CM_P16 | 1;
|
||||
lineData.setCell(2, nullCell);
|
||||
nullCell.bg = Attributes.CM_P16 | 2;
|
||||
lineData.setCell(3, nullCell);
|
||||
lineData.setCell(4, nullCell);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span> </span><span class="xterm-bg-1"> </span><span class="xterm-bg-2"> </span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle BCE for multiple cells', () => {
|
||||
const nullCell = lineData.loadCell(0, new CellData());
|
||||
nullCell.bg = Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, nullCell);
|
||||
let spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-1"> </span>'
|
||||
);
|
||||
lineData.setCell(1, nullCell);
|
||||
spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-1"> </span>'
|
||||
);
|
||||
lineData.setCell(2, nullCell);
|
||||
lineData.setCell(3, nullCell);
|
||||
spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-1"> </span>'
|
||||
);
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-1"> </span><span>a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply correct positive or negative spacing', () => {
|
||||
EMPTY_WIDTH.widths['€'] = [2, 2, 2, 2]; // too small, should add 3px
|
||||
EMPTY_WIDTH.widths['語'] = [10, 10, 10, 10]; // exact match for its width, should merge
|
||||
EMPTY_WIDTH.widths['𝄞'] = [7, 7, 7, 7]; // too wide, should subtract -2px
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
|
||||
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)]));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c語</span><span style="letter-spacing: -2px;">𝄞</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not merge across link borders', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(6, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>aa</span><span style="text-decoration: underline;">xxx</span><span>bb</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('empty cells included in link underline', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>aa</span><span style="text-decoration: underline;">x x</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it('link range gets capped to actual line borders', () => {
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
}
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -100, 100);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span style="text-decoration: underline;">aaaaaaaaaa</span>'
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function extractHtml(spans: HTMLSpanElement[]): string {
|
||||
const element = dom.window.document.createElement('div');
|
||||
element.appendChild(fragment);
|
||||
element.replaceChildren(...spans);
|
||||
return element.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2018, 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferLine, ICellData, IColor } from 'common/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
|
||||
import { WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { color, rgba } from 'common/Color';
|
||||
@@ -13,18 +13,23 @@ import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'bro
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { WidthCache } from 'browser/renderer/dom/WidthCache';
|
||||
|
||||
|
||||
export const enum RowCss {
|
||||
BOLD_CLASS = 'xterm-bold',
|
||||
DIM_CLASS = 'xterm-dim',
|
||||
ITALIC_CLASS = 'xterm-italic',
|
||||
UNDERLINE_CLASS = 'xterm-underline',
|
||||
OVERLINE_CLASS = 'xterm-overline',
|
||||
STRIKETHROUGH_CLASS = 'xterm-strikethrough',
|
||||
CURSOR_CLASS = 'xterm-cursor',
|
||||
CURSOR_BLINK_CLASS = 'xterm-cursor-blink',
|
||||
CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block',
|
||||
CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar',
|
||||
CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'
|
||||
}
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
export const DIM_CLASS = 'xterm-dim';
|
||||
export const ITALIC_CLASS = 'xterm-italic';
|
||||
export const UNDERLINE_CLASS = 'xterm-underline';
|
||||
export const OVERLINE_CLASS = 'xterm-overline';
|
||||
export const STRIKETHROUGH_CLASS = 'xterm-strikethrough';
|
||||
export const CURSOR_CLASS = 'xterm-cursor';
|
||||
export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink';
|
||||
export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
|
||||
export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar';
|
||||
export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
|
||||
|
||||
export class DomRendererRowFactory {
|
||||
private _workCell: CellData = new CellData();
|
||||
@@ -33,6 +38,8 @@ export class DomRendererRowFactory {
|
||||
private _selectionEnd: [number, number] | undefined;
|
||||
private _columnSelectMode: boolean = false;
|
||||
|
||||
public defaultSpacing = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _document: Document,
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
@@ -49,39 +56,47 @@ export class DomRendererRowFactory {
|
||||
this._columnSelectMode = columnSelectMode;
|
||||
}
|
||||
|
||||
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number, cellMap: Int16Array): DocumentFragment {
|
||||
// NOTE: `cellMap` maps cell positions to a span element index in a row.
|
||||
// All positions should be updated, even skipped ones after wide chars or left overs at the end,
|
||||
// otherwise the mouse hover logic might mark the wrong elements as underlined.
|
||||
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
public createRow(
|
||||
lineData: IBufferLine,
|
||||
row: number,
|
||||
isCursorRow: boolean,
|
||||
cursorStyle: string | undefined,
|
||||
cursorX: number,
|
||||
cursorBlink: boolean,
|
||||
cellWidth: number,
|
||||
widthCache: WidthCache,
|
||||
linkStart: number,
|
||||
linkEnd: number
|
||||
): HTMLSpanElement[] {
|
||||
|
||||
const elements: HTMLSpanElement[] = [];
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
// Find the line length first, this prevents the need to output a bunch of
|
||||
// empty cells at the end. This cannot easily be integrated into the main
|
||||
// loop below because of the colCount feature (which can be removed after we
|
||||
// properly support reflow and disallow data to go beyond the right-side of
|
||||
// the viewport).
|
||||
let lineLength = 0;
|
||||
for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) {
|
||||
if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) {
|
||||
lineLength = x + 1;
|
||||
break;
|
||||
}
|
||||
const colors = this._themeService.colors;
|
||||
|
||||
let lineLength = lineData.getNoBgTrimmedLength();
|
||||
if (isCursorRow && lineLength < cursorX + 1) {
|
||||
lineLength = cursorX + 1;
|
||||
}
|
||||
|
||||
const colors = this._themeService.colors;
|
||||
let elemIndex = -1;
|
||||
let charElement: HTMLSpanElement | undefined;
|
||||
let cellAmount = 0;
|
||||
let text = '';
|
||||
let oldBg = 0;
|
||||
let oldFg = 0;
|
||||
let oldExt = 0;
|
||||
let oldLinkHover: number | boolean = false;
|
||||
let oldSpacing = 0;
|
||||
let spacing = 0;
|
||||
const classes: string[] = [];
|
||||
|
||||
let x = 0;
|
||||
for (; x < lineLength; x++) {
|
||||
const hasHover = linkStart !== -1 && linkEnd !== -1;
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
lineData.loadCell(x, this._workCell);
|
||||
let width = this._workCell.getWidth();
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by the char at x-1
|
||||
// still have to update cellMap with current element index
|
||||
if (width === 0) {
|
||||
cellMap[x] = elemIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,16 +127,66 @@ export class DomRendererRowFactory {
|
||||
width = cell.getWidth();
|
||||
}
|
||||
|
||||
const charElement = this._document.createElement('span');
|
||||
if (width > 1) {
|
||||
charElement.style.width = `${cellWidth * width}px`;
|
||||
const isInSelection = this._isCellInSelection(x, row);
|
||||
const isCursorCell = isCursorRow && x === cursorX;
|
||||
const isLinkHover = hasHover && x >= linkStart && x <= linkEnd;
|
||||
|
||||
// get chars to render for this cell
|
||||
let chars = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
if (chars === ' ' && (cell.isUnderline() || cell.isOverline())) {
|
||||
chars = '\xa0';
|
||||
}
|
||||
|
||||
if (isJoined) {
|
||||
// Ligatures in the DOM renderer must use display inline, as they may not show with
|
||||
// inline-block if they are outside the bounds of the element
|
||||
charElement.style.display = 'inline';
|
||||
// lookup char render width and calc spacing
|
||||
spacing = width * cellWidth - widthCache.get(chars, cell.isBold(), cell.isItalic());
|
||||
|
||||
if (!charElement) {
|
||||
charElement = this._document.createElement('span');
|
||||
} else {
|
||||
/**
|
||||
* chars can only be merged on existing span if:
|
||||
* - existing span only contains mergeable chars (cellAmount != 0)
|
||||
* - fg/bg/ul did not change
|
||||
* - char not part of a selection
|
||||
* - underline from hover state did not change
|
||||
* - cell content renders to same letter-spacing
|
||||
* - cell is not cursor
|
||||
*/
|
||||
if (
|
||||
cellAmount
|
||||
&& cell.bg === oldBg && cell.fg === oldFg && cell.extended.ext === oldExt
|
||||
&& !isInSelection
|
||||
&& isLinkHover === oldLinkHover
|
||||
&& spacing === oldSpacing
|
||||
&& !isCursorCell
|
||||
&& !isJoined
|
||||
) {
|
||||
// no span alterations, thus only account chars skipping all code below
|
||||
text += chars;
|
||||
cellAmount++;
|
||||
continue;
|
||||
} else {
|
||||
/**
|
||||
* cannot merge:
|
||||
* - apply left-over text to old span
|
||||
* - create new span, reset state holders cellAmount & text
|
||||
*/
|
||||
if (cellAmount) {
|
||||
charElement.textContent = text;
|
||||
}
|
||||
charElement = this._document.createElement('span');
|
||||
cellAmount = 0;
|
||||
text = '';
|
||||
}
|
||||
}
|
||||
// preserve conditions for next merger eval round
|
||||
oldBg = cell.bg;
|
||||
oldFg = cell.fg;
|
||||
oldExt = cell.extended.ext;
|
||||
oldLinkHover = isLinkHover;
|
||||
oldSpacing = spacing;
|
||||
|
||||
if (isJoined) {
|
||||
// The DOM renderer colors the background of the cursor but for ligatures all cells are
|
||||
// joined. The workaround here is to show a cursor around the whole ligature so it shows up,
|
||||
// the cursor looks the same when on any character of the ligature though
|
||||
@@ -130,48 +195,42 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._coreService.isCursorHidden && isCursorRow && x === cursorX) {
|
||||
charElement.classList.add(CURSOR_CLASS);
|
||||
|
||||
if (!this._coreService.isCursorHidden && isCursorCell) {
|
||||
classes.push(RowCss.CURSOR_CLASS);
|
||||
if (cursorBlink) {
|
||||
charElement.classList.add(CURSOR_BLINK_CLASS);
|
||||
}
|
||||
|
||||
switch (cursorStyle) {
|
||||
case 'bar':
|
||||
charElement.classList.add(CURSOR_STYLE_BAR_CLASS);
|
||||
break;
|
||||
case 'underline':
|
||||
charElement.classList.add(CURSOR_STYLE_UNDERLINE_CLASS);
|
||||
break;
|
||||
default:
|
||||
charElement.classList.add(CURSOR_STYLE_BLOCK_CLASS);
|
||||
break;
|
||||
classes.push(RowCss.CURSOR_BLINK_CLASS);
|
||||
}
|
||||
classes.push(
|
||||
cursorStyle === 'bar'
|
||||
? RowCss.CURSOR_STYLE_BAR_CLASS
|
||||
: cursorStyle === 'underline'
|
||||
? RowCss.CURSOR_STYLE_UNDERLINE_CLASS
|
||||
: RowCss.CURSOR_STYLE_BLOCK_CLASS
|
||||
);
|
||||
}
|
||||
|
||||
if (cell.isBold()) {
|
||||
charElement.classList.add(BOLD_CLASS);
|
||||
classes.push(RowCss.BOLD_CLASS);
|
||||
}
|
||||
|
||||
if (cell.isItalic()) {
|
||||
charElement.classList.add(ITALIC_CLASS);
|
||||
classes.push(RowCss.ITALIC_CLASS);
|
||||
}
|
||||
|
||||
if (cell.isDim()) {
|
||||
charElement.classList.add(DIM_CLASS);
|
||||
classes.push(RowCss.DIM_CLASS);
|
||||
}
|
||||
|
||||
if (cell.isInvisible()) {
|
||||
charElement.textContent = WHITESPACE_CELL_CHAR;
|
||||
text = WHITESPACE_CELL_CHAR;
|
||||
} else {
|
||||
charElement.textContent = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
text = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
}
|
||||
|
||||
if (cell.isUnderline()) {
|
||||
charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`);
|
||||
if (charElement.textContent === ' ') {
|
||||
charElement.textContent = '\xa0'; // =
|
||||
classes.push(`${RowCss.UNDERLINE_CLASS}-${cell.extended.underlineStyle}`);
|
||||
if (text === ' ') {
|
||||
text = '\xa0'; // =
|
||||
}
|
||||
if (!cell.isUnderlineColorDefault()) {
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
@@ -187,14 +246,19 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
|
||||
if (cell.isOverline()) {
|
||||
charElement.classList.add(OVERLINE_CLASS);
|
||||
if (charElement.textContent === ' ') {
|
||||
charElement.textContent = '\xa0'; // =
|
||||
classes.push(RowCss.OVERLINE_CLASS);
|
||||
if (text === ' ') {
|
||||
text = '\xa0'; // =
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.isStrikethrough()) {
|
||||
charElement.classList.add(STRIKETHROUGH_CLASS);
|
||||
classes.push(RowCss.STRIKETHROUGH_CLASS);
|
||||
}
|
||||
|
||||
// apply link hover underline late, effectively overrides any previous text-decoration settings
|
||||
if (isLinkHover) {
|
||||
charElement.style.textDecoration = 'underline';
|
||||
}
|
||||
|
||||
let fg = cell.getFgColor();
|
||||
@@ -234,7 +298,6 @@ export class DomRendererRowFactory {
|
||||
});
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
const isInSelection = this._isCellInSelection(x, row);
|
||||
if (!isTop) {
|
||||
if (colors.selectionForeground && isInSelection) {
|
||||
fgColorMode = Attributes.CM_RGB;
|
||||
@@ -252,7 +315,7 @@ export class DomRendererRowFactory {
|
||||
|
||||
// If it's a top decoration, render above the selection
|
||||
if (isTop) {
|
||||
charElement.classList.add(`xterm-decoration-top`);
|
||||
classes.push('xterm-decoration-top');
|
||||
}
|
||||
|
||||
// Background
|
||||
@@ -261,7 +324,7 @@ export class DomRendererRowFactory {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
resolvedBg = colors.ansi[bg];
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
classes.push(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
|
||||
@@ -271,7 +334,7 @@ export class DomRendererRowFactory {
|
||||
default:
|
||||
if (isInverse) {
|
||||
resolvedBg = colors.foreground;
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
classes.push(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
} else {
|
||||
resolvedBg = colors.background;
|
||||
}
|
||||
@@ -292,7 +355,7 @@ export class DomRendererRowFactory {
|
||||
fg += 8;
|
||||
}
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) {
|
||||
charElement.classList.add(`xterm-fg-${fg}`);
|
||||
classes.push(`xterm-fg-${fg}`);
|
||||
}
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
@@ -309,24 +372,40 @@ export class DomRendererRowFactory {
|
||||
default:
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, undefined)) {
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
|
||||
classes.push(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment.appendChild(charElement);
|
||||
cellMap[x] = ++elemIndex;
|
||||
// apply CSS classes
|
||||
// slightly faster than using classList by omitting
|
||||
// checks for doubled entries (code above should not have doublets)
|
||||
if (classes.length) {
|
||||
charElement.className = classes.join(' ');
|
||||
classes.length = 0;
|
||||
}
|
||||
|
||||
// exclude conditions for cell merging - never merge these
|
||||
if (!isCursorCell && !isInSelection && !isJoined) {
|
||||
cellAmount++;
|
||||
} else {
|
||||
charElement.textContent = text;
|
||||
}
|
||||
// apply letter-spacing rule
|
||||
if (spacing !== this.defaultSpacing) {
|
||||
charElement.style.letterSpacing = `${spacing}px`;
|
||||
}
|
||||
|
||||
elements.push(charElement);
|
||||
x = lastCharX;
|
||||
}
|
||||
|
||||
// since the loop above might exit early not handling all cells,
|
||||
// also set remaining cell positions to last element index
|
||||
if (x < cols - 1) {
|
||||
cellMap.subarray(x).fill(++elemIndex);
|
||||
// postfix text of last merged span
|
||||
if (charElement && cellAmount) {
|
||||
charElement.textContent = text;
|
||||
}
|
||||
|
||||
return fragment;
|
||||
return elements;
|
||||
}
|
||||
|
||||
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { WidthCache, WidthCacheSettings } from 'browser/renderer/dom/WidthCache';
|
||||
import jsdom = require('jsdom');
|
||||
|
||||
|
||||
export class TestWidthCache extends WidthCache {
|
||||
public get flat(): Float32Array {
|
||||
return (this as any)._flat;
|
||||
}
|
||||
public get holey(): Map<string, number> | undefined {
|
||||
return (this as any)._holey;
|
||||
}
|
||||
|
||||
public widths: {[key: string]: [number, number, number, number]} = {};
|
||||
protected _measure(c: string, variant: number): number {
|
||||
if (this.widths[c] !== undefined) {
|
||||
return this.widths[c][variant];
|
||||
}
|
||||
return 5; // 5 is default width in tests in DomRendererRowFactory.test.ts
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function castf32(v: number): number {
|
||||
const buffer = new Float32Array(1);
|
||||
buffer[0] = v;
|
||||
return buffer[0];
|
||||
}
|
||||
|
||||
|
||||
describe('WidthCache', () => {
|
||||
let wc: TestWidthCache;
|
||||
beforeEach(() => {
|
||||
wc = new TestWidthCache(new jsdom.JSDOM('').window.document);
|
||||
wc.setFont('monospace', 15, 'normal', 'bold');
|
||||
});
|
||||
describe('cache invalidation', () => {
|
||||
beforeEach(() => {
|
||||
wc.flat.fill(1.23);
|
||||
wc.holey?.set('a', 2.34);
|
||||
});
|
||||
it('can cache values', () => {
|
||||
assert.deepStrictEqual(wc.flat[0], castf32(1.23));
|
||||
assert.deepStrictEqual(wc.holey?.get('a'), 2.34);
|
||||
assert.deepStrictEqual(wc.holey?.size, 1);
|
||||
});
|
||||
it('clear resets cache entries', () => {
|
||||
wc.clear();
|
||||
assert.deepStrictEqual(wc.flat[0], castf32(WidthCacheSettings.FLAT_UNSET));
|
||||
assert.deepStrictEqual(wc.holey?.get('a'), undefined);
|
||||
assert.deepStrictEqual(wc.holey?.size, 0);
|
||||
});
|
||||
it('setFont with changed font name', () => {
|
||||
wc.setFont('Arial', 15, 'normal', 'bold');
|
||||
assert.deepStrictEqual(wc.flat[0], castf32(WidthCacheSettings.FLAT_UNSET));
|
||||
assert.deepStrictEqual(wc.holey?.get('a'), undefined);
|
||||
assert.deepStrictEqual(wc.holey?.size, 0);
|
||||
});
|
||||
it('setFont with changed font size', () => {
|
||||
wc.setFont('monospace', 14, 'normal', 'bold');
|
||||
assert.deepStrictEqual(wc.flat[0], castf32(WidthCacheSettings.FLAT_UNSET));
|
||||
assert.deepStrictEqual(wc.holey?.get('a'), undefined);
|
||||
assert.deepStrictEqual(wc.holey?.size, 0);
|
||||
});
|
||||
it('setFont with changed weight', () => {
|
||||
wc.setFont('monospace', 15, '100', 'bold');
|
||||
assert.deepStrictEqual(wc.flat[0], castf32(WidthCacheSettings.FLAT_UNSET));
|
||||
assert.deepStrictEqual(wc.holey?.get('a'), undefined);
|
||||
assert.deepStrictEqual(wc.holey?.size, 0);
|
||||
});
|
||||
it('setFont with changed weightBold', () => {
|
||||
wc.setFont('monospace', 15, 'normal', '900');
|
||||
assert.deepStrictEqual(wc.flat[0], castf32(WidthCacheSettings.FLAT_UNSET));
|
||||
assert.deepStrictEqual(wc.holey?.get('a'), undefined);
|
||||
assert.deepStrictEqual(wc.holey?.size, 0);
|
||||
});
|
||||
it('setFont with unchanged settings does not cache entries', () => {
|
||||
wc.setFont('monospace', 15, 'normal', 'bold');
|
||||
assert.deepStrictEqual(wc.flat[0], castf32(1.23));
|
||||
assert.deepStrictEqual(wc.holey?.get('a'), 2.34);
|
||||
assert.deepStrictEqual(wc.holey?.size, 1);
|
||||
});
|
||||
});
|
||||
describe('get', () => {
|
||||
it('store regular < WidthCacheSettings.FLAT_SIZE in flat', () => {
|
||||
for (let i = 0; i < WidthCacheSettings.FLAT_SIZE + 10; ++i) {
|
||||
const width = wc.get(String.fromCharCode(i), false, false);
|
||||
assert.deepStrictEqual(width, 5);
|
||||
if (i < WidthCacheSettings.FLAT_SIZE) {
|
||||
assert.deepStrictEqual(wc.flat[i], 5);
|
||||
assert.deepStrictEqual(wc.holey?.get(String.fromCharCode(i)), undefined);
|
||||
} else {
|
||||
assert.deepStrictEqual(wc.holey?.get(String.fromCharCode(i)), 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
it('stores bold & italic in holey', () => {
|
||||
// bold
|
||||
let width = wc.get('b', true, false);
|
||||
assert.deepStrictEqual(width, 5);
|
||||
assert.deepStrictEqual(wc.holey?.get('bB'), 5);
|
||||
// italic
|
||||
width = wc.get('i', false, true);
|
||||
assert.deepStrictEqual(width, 5);
|
||||
assert.deepStrictEqual(wc.holey?.get('iI'), 5);
|
||||
// bold&italic
|
||||
width = wc.get('x', true, true);
|
||||
assert.deepStrictEqual(width, 5);
|
||||
assert.deepStrictEqual(wc.holey?.get('xBI'), 5);
|
||||
});
|
||||
it('can store any string', () => {
|
||||
// regular
|
||||
let width = wc.get('foo', false, false);
|
||||
assert.deepStrictEqual(width, 5);
|
||||
assert.deepStrictEqual(wc.holey?.get('foo'), 5);
|
||||
// bold&italic
|
||||
width = wc.get('bar&baz', true, true);
|
||||
assert.deepStrictEqual(width, 5);
|
||||
assert.deepStrictEqual(wc.holey?.get('bar&bazBI'), 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { FontWeight } from 'common/services/Services';
|
||||
|
||||
|
||||
export const enum WidthCacheSettings {
|
||||
/** sentinel for unset values in flat cache */
|
||||
FLAT_UNSET = -9999,
|
||||
/** size of flat cache, size-1 equals highest codepoint handled by flat */
|
||||
FLAT_SIZE = 256,
|
||||
/** char repeat for measuring */
|
||||
REPEAT = 32
|
||||
}
|
||||
|
||||
|
||||
const enum FontVariant {
|
||||
REGULAR = 0,
|
||||
BOLD = 1,
|
||||
ITALIC = 2,
|
||||
BOLD_ITALIC = 3
|
||||
}
|
||||
|
||||
|
||||
export class WidthCache implements IDisposable {
|
||||
// flat cache for regular variant up to CacheSettings.FLAT_SIZE
|
||||
// NOTE: ~4x faster access than holey (serving >>80% of terminal content)
|
||||
// It has a small memory footprint (only 1MB for full BMP caching),
|
||||
// still the sweet spot is not reached before touching 32k different codepoints,
|
||||
// thus we store the remaining <<20% of terminal data in a holey structure.
|
||||
protected _flat = new Float32Array(WidthCacheSettings.FLAT_SIZE);
|
||||
|
||||
// holey cache for bold, italic and bold&italic for any string
|
||||
// FIXME: can grow really big over time (~8.5 MB for full BMP caching),
|
||||
// so a shared API across terminals is needed
|
||||
protected _holey: Map<string, number> | undefined;
|
||||
|
||||
private _font = '';
|
||||
private _fontSize = 0;
|
||||
private _weight: FontWeight = 'normal';
|
||||
private _weightBold: FontWeight = 'bold';
|
||||
private _container: HTMLDivElement;
|
||||
private _measureElements: HTMLSpanElement[] = [];
|
||||
|
||||
constructor(_document: Document) {
|
||||
this._container = _document.createElement('div');
|
||||
this._container.style.position = 'absolute';
|
||||
this._container.style.top = '-50000px';
|
||||
this._container.style.width = '50000px';
|
||||
// SP should stack in spans
|
||||
this._container.style.whiteSpace = 'pre';
|
||||
// avoid undercuts in non-monospace fonts from kerning
|
||||
this._container.style.fontKerning = 'none';
|
||||
|
||||
const regular = _document.createElement('span');
|
||||
|
||||
const bold = _document.createElement('span');
|
||||
bold.style.fontWeight = 'bold';
|
||||
|
||||
const italic = _document.createElement('span');
|
||||
italic.style.fontStyle = 'italic';
|
||||
|
||||
const boldItalic = _document.createElement('span');
|
||||
boldItalic.style.fontWeight = 'bold';
|
||||
boldItalic.style.fontStyle = 'italic';
|
||||
|
||||
// NOTE: must be in order of FontVariant
|
||||
this._measureElements = [regular, bold, italic, boldItalic];
|
||||
this._container.appendChild(regular);
|
||||
this._container.appendChild(bold);
|
||||
this._container.appendChild(italic);
|
||||
this._container.appendChild(boldItalic);
|
||||
|
||||
_document.body.appendChild(this._container);
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._container.remove(); // remove elements from DOM
|
||||
this._measureElements.length = 0; // release element refs
|
||||
this._holey = undefined; // free cache memory via GC
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the width cache.
|
||||
*/
|
||||
public clear(): void {
|
||||
this._flat.fill(WidthCacheSettings.FLAT_UNSET);
|
||||
// .clear() has some overhead, re-assign instead (>3 times faster)
|
||||
this._holey = new Map<string, number>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the font for measuring.
|
||||
* Must be called for any changes on font settings.
|
||||
* Also clears the cache.
|
||||
*/
|
||||
public setFont(font: string, fontSize: number, weight: FontWeight, weightBold: FontWeight): void {
|
||||
// skip if nothing changed
|
||||
if (font === this._font
|
||||
&& fontSize === this._fontSize
|
||||
&& weight === this._weight
|
||||
&& weightBold === this._weightBold
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._font = font;
|
||||
this._fontSize = fontSize;
|
||||
this._weight = weight;
|
||||
this._weightBold = weightBold;
|
||||
|
||||
this._container.style.fontFamily = this._font;
|
||||
this._container.style.fontSize = `${this._fontSize}px`;
|
||||
this._measureElements[FontVariant.REGULAR].style.fontWeight = `${weight}`;
|
||||
this._measureElements[FontVariant.BOLD].style.fontWeight = `${weightBold}`;
|
||||
this._measureElements[FontVariant.ITALIC].style.fontWeight = `${weight}`;
|
||||
this._measureElements[FontVariant.BOLD_ITALIC].style.fontWeight = `${weightBold}`;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the render width for cell content `c` with current font settings.
|
||||
* `variant` denotes the font variant to be used.
|
||||
*/
|
||||
public get(c: string, bold: boolean | number, italic: boolean | number): number {
|
||||
let cp = 0;
|
||||
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
|
||||
return this._flat[cp] !== WidthCacheSettings.FLAT_UNSET
|
||||
? this._flat[cp]
|
||||
: (this._flat[cp] = this._measure(c, 0));
|
||||
}
|
||||
let key = c;
|
||||
if (bold) key += 'B';
|
||||
if (italic) key += 'I';
|
||||
let width = this._holey!.get(key);
|
||||
if (width === undefined) {
|
||||
let variant = 0;
|
||||
if (bold) variant |= FontVariant.BOLD;
|
||||
if (italic) variant |= FontVariant.ITALIC;
|
||||
width = this._measure(c, variant);
|
||||
this._holey!.set(key, width);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
protected _measure(c: string, variant: FontVariant): number {
|
||||
const el = this._measureElements[variant];
|
||||
el.textContent = c.repeat(WidthCacheSettings.REPEAT);
|
||||
return el.offsetWidth / WidthCacheSettings.REPEAT;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,12 @@ import { IOptionsService } from 'common/services/Services';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ITerminalOptions } from 'common/Types';
|
||||
|
||||
|
||||
const enum MeasureSettings {
|
||||
REPEAT = 32
|
||||
}
|
||||
|
||||
|
||||
export class CharSizeService extends Disposable implements ICharSizeService {
|
||||
public serviceBrand: undefined;
|
||||
@@ -67,8 +72,10 @@ class DomMeasureStrategy implements IMeasureStrategy {
|
||||
) {
|
||||
this._measureElement = this._document.createElement('span');
|
||||
this._measureElement.classList.add('xterm-char-measure-element');
|
||||
this._measureElement.textContent = 'W';
|
||||
this._measureElement.textContent = 'W'.repeat(MeasureSettings.REPEAT);
|
||||
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||
this._measureElement.style.whiteSpace = 'pre';
|
||||
this._measureElement.style.fontKerning = 'none';
|
||||
this._parentElement.appendChild(this._measureElement);
|
||||
}
|
||||
|
||||
@@ -85,7 +92,7 @@ class DomMeasureStrategy implements IMeasureStrategy {
|
||||
// If values are 0 then the element is likely currently display:none, in which case we should
|
||||
// retain the previous value.
|
||||
if (geometry.width !== 0 && geometry.height !== 0) {
|
||||
this._result.width = geometry.width;
|
||||
this._result.width = geometry.width / MeasureSettings.REPEAT;
|
||||
this._result.height = Math.ceil(geometry.height);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -242,6 +242,7 @@ export interface IBufferLine {
|
||||
copyFrom(line: IBufferLine): void;
|
||||
clone(): IBufferLine;
|
||||
getTrimmedLength(): number;
|
||||
getNoBgTrimmedLength(): number;
|
||||
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
|
||||
|
||||
/* direct access to cell attrs */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { CharData, IBufferLine, ICellData, IAttributeData, IExtendedAttrs } from 'common/Types';
|
||||
import { stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content, BgFlags, FgFlags } from 'common/buffer/Constants';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content, BgFlags, FgFlags, Attributes } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData';
|
||||
|
||||
@@ -463,6 +463,15 @@ export class BufferLine implements IBufferLine {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public getNoBgTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) {
|
||||
return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
|
||||
const srcData = src._data;
|
||||
if (applyInReverse) {
|
||||
|
||||
Reference in New Issue
Block a user