Merge branch 'master' into clusters

This commit is contained in:
Per Bothner
2023-08-14 12:22:55 -07:00
committed by GitHub
47 changed files with 721 additions and 364 deletions
+2 -1
View File
@@ -109,7 +109,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _handleHover(position: IBufferCellPosition): void {
// TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
// TODO: This currently does not cache link provider results across wrapped lines, activeLine
// should be something like `activeRange: {startY, endY}`
// Check if we need to clear the link
if (this._activeLine !== position.y || this._wasResized) {
this._clearCurrentLink();
+2 -1
View File
@@ -104,7 +104,8 @@ export class OscLinkProvider implements ILinkProvider {
}
}
// TODO: Handle fetching and returning other link ranges to underline other links with the same id
// TODO: Handle fetching and returning other link ranges to underline other links with the same
// id
callback(result);
}
}
+3 -1
View File
@@ -529,6 +529,8 @@ export class MockThemeService implements IThemeService{
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
],
selectionBackgroundOpaque: css.toColor('#ff0000'),
selectionInactiveBackgroundOpaque: css.toColor('#00ff00')
} as any;
}
+3
View File
@@ -136,7 +136,10 @@ export interface IColorSet {
selectionInactiveBackgroundTransparent: IColor;
selectionInactiveBackgroundOpaque: IColor;
ansi: IColor[];
/** Maps original colors to colors that respect minimum contrast ratio. */
contrastCache: IColorContrastCache;
/** Maps original colors to colors that respect _half_ of the minimum contrast ratio. */
halfContrastCache: IColorContrastCache;
}
export type ReadonlyColorSet = Readonly<Omit<IColorSet, 'ansi'>> & { ansi: Readonly<Pick<IColorSet, 'ansi'>['ansi']> };
+2 -2
View File
@@ -65,8 +65,8 @@ export class Viewport extends Disposable implements IViewport {
super();
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that
// case, therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this)));
@@ -103,6 +103,10 @@ export class BufferDecorationRenderer extends Disposable {
decoration.element = element;
this._decorationElements.set(decoration, element);
this._container.appendChild(element);
decoration.onDispose(() => {
this._decorationElements.delete(decoration);
element!.remove();
});
}
element.style.top = `${line * this._renderService.dimensions.css.cell.height}px`;
element.style.display = this._altBufferIsActive ? 'none' : 'block';
+11 -9
View File
@@ -197,23 +197,20 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Cursor
styles +=
`${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} .${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} .${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} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_OUTLINE_CLASS} {` +
` outline: 1px solid ${colors.cursor.css};` +
` outline-offset: -1px;` +
`}` +
`${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;` +
`}` +
@@ -392,7 +389,8 @@ export class DomRenderer extends Disposable implements IRenderer {
public clear(): void {
for (const e of this._rowElements) {
/**
* NOTE: This used to be `e.innerText = '';` but that doesn't work when using `jsdom` and `@testing-library/react`
* NOTE: This used to be `e.innerText = '';` but that doesn't work when using `jsdom` and
* `@testing-library/react`
*
* references:
* - https://github.com/testing-library/react-testing-library/issues/1146
@@ -408,6 +406,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const cursorX = Math.min(buffer.x, this._bufferService.cols - 1);
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
for (let y = start; y <= end; y++) {
const row = y + buffer.ydisp;
@@ -422,6 +421,7 @@ export class DomRenderer extends Disposable implements IRenderer {
row,
row === cursorAbsoluteY,
cursorStyle,
cursorInactiveStyle,
cursorX,
cursorBlink,
this.dimensions.css.cell.width,
@@ -474,6 +474,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const cursorX = Math.min(buffer.x, cols - 1);
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
// refresh rows within link range
for (let i = y; i <= y2; ++i) {
@@ -489,6 +490,7 @@ export class DomRenderer extends Disposable implements IRenderer {
row,
row === cursorAbsoluteY,
cursorStyle,
cursorInactiveStyle,
cursorX,
cursorBlink,
this.dimensions.css.cell.width,
@@ -39,7 +39,7 @@ describe('DomRendererRowFactory', () => {
describe('createRow', () => {
it('should not create anything for an empty row', () => {
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
''
);
@@ -50,7 +50,7 @@ describe('DomRendererRowFactory', () => {
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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span>語</span>'
);
@@ -58,7 +58,7 @@ describe('DomRendererRowFactory', () => {
it('should add class for cursor and cursor style', () => {
for (const style of ['block', 'bar', 'underline']) {
const spans = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, true, style, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
);
@@ -66,18 +66,42 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for cursor blink', () => {
const spans = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, true, 'block', undefined, 0, true, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-cursor xterm-cursor-blink xterm-cursor-block"> </span>`
);
});
it('should add class for inactive cursor', () => {
const coreBrowserService = new MockCoreBrowserService();
coreBrowserService.isFocused = false;
const rowFactory = new DomRendererRowFactory(
dom.window.document,
new MockCharacterJoinerService(),
new MockOptionsService({ drawBoldTextInBrightColors: true }),
coreBrowserService,
new MockCoreService(),
new MockDecorationService(),
new MockThemeService()
);
for (const inactiveStyle of ['outline', 'block', 'bar', 'underline', 'none']){
const spans = rowFactory.createRow(lineData, 0, true, 'block', inactiveStyle, 0, false, 5, EMPTY_WIDTH, -1, -1);
if (inactiveStyle === 'none') {
assert.equal(extractHtml(spans),
`<span class="xterm-cursor"> </span>`);
} else {
assert.equal(extractHtml(spans),
`<span class="xterm-cursor xterm-cursor-${inactiveStyle}"> </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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bold">a</span>'
);
@@ -87,7 +111,7 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-italic">a</span>'
);
@@ -97,7 +121,7 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-dim">a</span>'
);
@@ -110,7 +134,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-1">a</span>'
);
@@ -121,7 +145,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-2">a</span>'
);
@@ -132,7 +156,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.CURLY;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-3">a</span>'
);
@@ -143,7 +167,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-4">a</span>'
);
@@ -154,7 +178,7 @@ describe('DomRendererRowFactory', () => {
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
cell.extended.underlineStyle = UnderlineStyle.DASHED;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-underline-5">a</span>'
);
@@ -165,7 +189,7 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-overline">a</span>'
);
@@ -175,7 +199,7 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-strikethrough">a</span>'
);
@@ -188,7 +212,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-fg-${i}">a</span>`
);
@@ -202,7 +226,7 @@ describe('DomRendererRowFactory', () => {
cell.bg &= ~Attributes.PCOLOR_MASK;
cell.bg |= i;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-bg-${i}">a</span>`
);
@@ -214,7 +238,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
);
@@ -225,7 +249,7 @@ describe('DomRendererRowFactory', () => {
cell.fg |= FgFlags.INVERSE;
cell.bg |= Attributes.CM_P16 | 1;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
);
@@ -235,7 +259,7 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
);
@@ -248,7 +272,7 @@ describe('DomRendererRowFactory', () => {
cell.fg &= ~Attributes.PCOLOR_MASK;
cell.fg |= i;
lineData.setCell(0, cell);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
);
@@ -260,7 +284,7 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span style="background-color:#040506;color:#010203;">a</span>'
);
@@ -271,7 +295,7 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span style="background-color:#010203;color:#040506;">a</span>'
);
@@ -283,17 +307,17 @@ 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span>a</span><span class="xterm-decoration-top">b</span>'
'<span>a</span><span style="background-color:#ff0000;" 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 spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 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>'
'<span style="background-color:#ff0000;" class="xterm-decoration-top"> a</span>'
);
});
});
@@ -308,7 +332,7 @@ describe('DomRendererRowFactory', () => {
});
it('should not create anything for an empty row', () => {
const spans = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
''
);
@@ -318,7 +342,7 @@ 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)]));
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span>abc</span>'
);
@@ -329,7 +353,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c</span>'
);
@@ -344,7 +368,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 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>'
);
@@ -356,7 +380,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, true, undefined, 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>'
);
@@ -369,7 +393,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 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>'
);
@@ -379,23 +403,23 @@ describe('DomRendererRowFactory', () => {
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);
let spans = rowFactory.createRow(lineData, 0, false, undefined, 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);
spans = rowFactory.createRow(lineData, 0, false, undefined, 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);
spans = rowFactory.createRow(lineData, 0, false, undefined, 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);
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span class="xterm-bg-1"> </span><span>a</span>'
);
@@ -410,7 +434,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 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>'
);
@@ -424,7 +448,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, 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>'
);
@@ -435,7 +459,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, 2, 4);
assert.equal(extractHtml(spans),
'<span>aa</span><span style="text-decoration: underline;">x x</span>'
);
@@ -445,7 +469,7 @@ describe('DomRendererRowFactory', () => {
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);
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -100, 100);
assert.equal(extractHtml(spans),
'<span style="text-decoration: underline;">aaaaaaaaaa</span>'
);
@@ -14,6 +14,7 @@ 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';
import { IColorContrastCache } from 'browser/Types';
export const enum RowCss {
@@ -26,6 +27,7 @@ export const enum RowCss {
CURSOR_CLASS = 'xterm-cursor',
CURSOR_BLINK_CLASS = 'xterm-cursor-blink',
CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block',
CURSOR_STYLE_OUTLINE_CLASS = 'xterm-cursor-outline',
CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar',
CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'
}
@@ -61,6 +63,7 @@ export class DomRendererRowFactory {
row: number,
isCursorRow: boolean,
cursorStyle: string | undefined,
cursorInactiveStyle: string | undefined,
cursorX: number,
cursorBlink: boolean,
cellWidth: number,
@@ -86,6 +89,7 @@ export class DomRendererRowFactory {
let oldExt = 0;
let oldLinkHover: number | boolean = false;
let oldSpacing = 0;
let oldIsInSelection: boolean = false;
let spacing = 0;
const classes: string[] = [];
@@ -151,16 +155,24 @@ export class DomRendererRowFactory {
/**
* 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
* - bg did not change (or both are in selection)
* - fg did not change (or both are in selection and selection fg is set)
* - ext did not change
* - 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
&& (
(isInSelection && oldIsInSelection)
|| (!isInSelection && cell.bg === oldBg)
)
&& (
(isInSelection && oldIsInSelection && colors.selectionForeground)
|| (!(isInSelection && oldIsInSelection && colors.selectionForeground) && cell.fg === oldFg)
)
&& cell.extended.ext === oldExt
&& isLinkHover === oldLinkHover
&& spacing === oldSpacing
&& !isCursorCell
@@ -191,6 +203,7 @@ export class DomRendererRowFactory {
oldExt = cell.extended.ext;
oldLinkHover = isLinkHover;
oldSpacing = spacing;
oldIsInSelection = isInSelection;
if (isJoined) {
// The DOM renderer colors the background of the cursor but for ligatures all cells are
@@ -203,16 +216,37 @@ export class DomRendererRowFactory {
if (!this._coreService.isCursorHidden && isCursorCell) {
classes.push(RowCss.CURSOR_CLASS);
if (cursorBlink) {
classes.push(RowCss.CURSOR_BLINK_CLASS);
if (this._coreBrowserService.isFocused) {
if (cursorBlink) {
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
);
} else {
if (cursorInactiveStyle) {
switch (cursorInactiveStyle) {
case 'outline':
classes.push(RowCss.CURSOR_STYLE_OUTLINE_CLASS);
break;
case 'block':
classes.push(RowCss.CURSOR_STYLE_BLOCK_CLASS);
break;
case 'bar':
classes.push(RowCss.CURSOR_STYLE_BAR_CLASS);
break;
case 'underline':
classes.push(RowCss.CURSOR_STYLE_UNDERLINE_CLASS);
break;
default:
break;
}
}
}
classes.push(
cursorStyle === 'bar'
? RowCss.CURSOR_STYLE_BAR_CLASS
: cursorStyle === 'underline'
? RowCss.CURSOR_STYLE_UNDERLINE_CLASS
: RowCss.CURSOR_STYLE_BLOCK_CLASS
);
}
if (cell.isBold()) {
@@ -262,7 +296,8 @@ export class DomRendererRowFactory {
classes.push(RowCss.STRIKETHROUGH_CLASS);
}
// apply link hover underline late, effectively overrides any previous text-decoration settings
// apply link hover underline late, effectively overrides any previous text-decoration
// settings
if (isLinkHover) {
charElement.style.textDecoration = 'underline';
}
@@ -303,22 +338,26 @@ export class DomRendererRowFactory {
isTop = d.options.layer === 'top';
});
// Apply selection foreground if applicable
if (!isTop) {
if (colors.selectionForeground && isInSelection) {
// Apply selection
if (!isTop && isInSelection) {
// If in the selection, force the element to be above the selection to improve contrast and
// support opaque selections. The applies background is not actually needed here as
// selection is drawn in a seperate container, the main purpose of this to ensuring minimum
// contrast ratio
bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque;
bg = bgOverride.rgba >> 8 & 0xFFFFFF;
bgColorMode = Attributes.CM_RGB;
// Since an opaque selection is being rendered, the selection pretends to be a decoration to
// ensure text is drawn above the selection.
isTop = true;
// Apply selection foreground if applicable
if (colors.selectionForeground) {
fgColorMode = Attributes.CM_RGB;
fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
fgOverride = colors.selectionForeground;
}
}
// If in the selection, force the element to be above the selection to improve contrast and
// support opaque selections
if (isInSelection) {
bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque;
isTop = true;
}
// If it's a top decoration, render above the selection
if (isTop) {
classes.push('xterm-decoration-top');
@@ -392,7 +431,7 @@ export class DomRendererRowFactory {
}
// exclude conditions for cell merging - never merge these
if (!isCursorCell && !isInSelection && !isJoined && !isDecorated) {
if (!isCursorCell && !isJoined && !isDecorated) {
cellAmount++;
} else {
charElement.textContent = text;
@@ -420,15 +459,19 @@ export class DomRendererRowFactory {
}
// Try get from cache first, only use the cache when there are no decoration overrides
const cache = this._getContrastCache(cell);
let adjustedColor: IColor | undefined | null = undefined;
if (!bgOverride && !fgOverride) {
adjustedColor = this._themeService.colors.contrastCache.getColor(bg.rgba, fg.rgba);
adjustedColor = cache.getColor(bg.rgba, fg.rgba);
}
// Calculate and store in cache
if (adjustedColor === undefined) {
adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio);
this._themeService.colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
// Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from
// non-dim cells
const ratio = this._optionsService.rawOptions.minimumContrastRatio / (cell.isDim() ? 2 : 1);
adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, ratio);
cache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
}
if (adjustedColor) {
@@ -439,6 +482,13 @@ export class DomRendererRowFactory {
return false;
}
private _getContrastCache(cell: ICellData): IColorContrastCache {
if (cell.isDim()) {
return this._themeService.colors.halfContrastCache;
}
return this._themeService.colors.contrastCache;
}
private _addStyle(element: HTMLElement, style: string): void {
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
}
@@ -24,7 +24,8 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
ansi: colors.ansi.slice(),
contrastCache: colors.contrastCache
contrastCache: colors.contrastCache,
halfContrastCache: colors.halfContrastCache
};
return {
customGlyphs: options.customGlyphs,
+2 -2
View File
@@ -8,7 +8,7 @@ import { isFirefox, isLegacyEdge } from 'common/Platform';
export const INVERTED_DEFAULT_COLOR = 257;
export const DIM_OPACITY = 0.5;
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge would
// result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge
// would result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
// unaligned Powerline fonts (PR 3356#issuecomment-850928179).
export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic';
+2 -1
View File
@@ -349,7 +349,8 @@ const enum VectorType {
* not been patched with powerline characters and also to get pixel perfect rendering as rendering
* issues can occur around AA/SPAA.
*
* The line variants draw beyond the cell and get clipped to ensure the end of the line is not visible.
* The line variants draw beyond the cell and get clipped to ensure the end of the line is not
* visible.
*
* Original symbols defined in https://github.com/powerline/fontpatcher
*/
+29 -26
View File
@@ -15,6 +15,7 @@ import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
import { EventEmitter } from 'common/EventEmitter';
import { IColorContrastCache } from 'browser/Types';
/**
* A shared object which is used to draw nothing for a particular cell.
@@ -298,7 +299,7 @@ export class TextureAtlas implements ITextureAtlas {
case Attributes.CM_DEFAULT:
default:
if (inverse) {
result = this._config.colors.foreground;
result = color.opaque(this._config.colors.foreground);
} else {
result = this._config.colors.background;
}
@@ -309,8 +310,7 @@ export class TextureAtlas implements ITextureAtlas {
}
private _getForegroundColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, dim: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor {
// TODO: Pass dim along to get min contrast?
const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, false, bold, excludeFromContrastRatioDemands);
const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, false, bold, dim, excludeFromContrastRatioDemands);
if (minimumContrastColor) {
return minimumContrastColor;
}
@@ -385,23 +385,26 @@ export class TextureAtlas implements ITextureAtlas {
}
}
private _getMinimumContrastColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor | undefined {
private _getMinimumContrastColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, dim: boolean, excludeFromContrastRatioDemands: boolean): IColor | undefined {
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
return undefined;
}
// Try get from cache first
const adjustedColor = this._config.colors.contrastCache.getColor(bg, fg);
const cache = this._getContrastCache(dim);
const adjustedColor = cache.getColor(bg, fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);
// Dim cells only require half the contrast, otherwise they wouldn't be distinguishable from
// non-dim cells
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio / (dim ? 2 : 1));
if (!result) {
this._config.colors.contrastCache.setColor(bg, fg, null);
cache.setColor(bg, fg, null);
return undefined;
}
@@ -410,11 +413,18 @@ export class TextureAtlas implements ITextureAtlas {
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
);
this._config.colors.contrastCache.setColor(bg, fg, color);
cache.setColor(bg, fg, color);
return color;
}
private _getContrastCache(dim: boolean): IColorContrastCache {
if (dim) {
return this._config.colors.halfContrastCache;
}
return this._config.colors.contrastCache;
}
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
@@ -466,8 +476,8 @@ export class TextureAtlas implements ITextureAtlas {
// draw the background
const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse, dim);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha,
// regardless of transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
@@ -532,10 +542,9 @@ export class TextureAtlas implements ITextureAtlas {
// Underline style/stroke
this._tmpCtx.beginPath();
const xLeft = padding;
const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset;
const yMid = padding + this._config.deviceCharHeight + lineWidth - yOffset;
const yBot = Math.ceil(padding + this._config.deviceCharHeight + lineWidth * 2) - yOffset;
const ySpace = lineWidth * 2;
const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset - (restrictToCellHeight ? lineWidth * 2 : 0);
const yMid = yTop + lineWidth;
const yBot = yTop + lineWidth * 2;
for (let i = 0; i < chWidth; i++) {
this._tmpCtx.save();
@@ -544,17 +553,10 @@ export class TextureAtlas implements ITextureAtlas {
const xChMid = xChLeft + this._config.deviceCellWidth / 2;
switch (this._workAttributeData.extended.underlineStyle) {
case UnderlineStyle.DOUBLE:
if (restrictToCellHeight) {
this._tmpCtx.moveTo(xChLeft, yTop - ySpace);
this._tmpCtx.lineTo(xChRight, yTop - ySpace);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
} else {
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
this._tmpCtx.moveTo(xChLeft, yBot);
this._tmpCtx.lineTo(xChRight, yBot);
}
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
this._tmpCtx.moveTo(xChLeft, yBot);
this._tmpCtx.lineTo(xChRight, yBot);
break;
case UnderlineStyle.CURLY:
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
@@ -567,7 +569,8 @@ export class TextureAtlas implements ITextureAtlas {
const clipRegion = new Path2D();
clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, yBot - yTop);
this._tmpCtx.clip(clipRegion);
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other
// cells
this._tmpCtx.moveTo(xChLeft - this._config.deviceCellWidth / 2, yMid);
this._tmpCtx.bezierCurveTo(
xChLeft - this._config.deviceCellWidth / 2, yCurlyTop,
+2 -1
View File
@@ -60,7 +60,8 @@ interface IMeasureResult {
height: number;
}
// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses ctx.measureText
// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses
// ctx.measureText
class DomMeasureStrategy implements IMeasureStrategy {
private _result: IMeasureResult = { width: 0, height: 0 };
private _measureElement: HTMLElement;
+6 -1
View File
@@ -35,7 +35,12 @@ describe('ThemeService', () => {
describe('constructor', () => {
it('should fill all colors with values', () => {
for (const key of Object.keys(themeService.colors)) {
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
if (![
'ansi',
'contrastCache',
'halfContrastCache',
'selectionForeground'
].includes(key)) {
// A #rrggbb or rgba(...)
assert.ok((themeService.colors as any)[key].css.length >= 7);
}
+5 -3
View File
@@ -81,7 +81,8 @@ export class ThemeService extends Disposable implements IThemeService {
public serviceBrand: undefined;
private _colors: IColorSet;
private _contrastCache: IColorContrastCache;
private _contrastCache: IColorContrastCache = new ColorContrastCache();
private _halfContrastCache: IColorContrastCache = new ColorContrastCache();
private _restoreColors!: IRestoreColorSet;
public get colors(): ReadonlyColorSet { return this._colors; }
@@ -94,7 +95,6 @@ export class ThemeService extends Disposable implements IThemeService {
) {
super();
this._contrastCache = new ColorContrastCache();
this._colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
@@ -106,7 +106,8 @@ export class ThemeService extends Disposable implements IThemeService {
selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
contrastCache: this._contrastCache,
halfContrastCache: this._halfContrastCache
};
this._updateRestoreColors();
this._setTheme(this._optionsService.rawOptions.theme);
@@ -172,6 +173,7 @@ export class ThemeService extends Disposable implements IThemeService {
}
// Clear our the cache
this._contrastCache.clear();
this._halfContrastCache.clear();
this._updateRestoreColors();
this._onChangeColors.fire(this.colors);
}
+2 -1
View File
@@ -106,7 +106,8 @@ export namespace color {
}
/**
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa').
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb',
* '#rrggbbaa').
*/
export namespace css {
let $ctx: CanvasRenderingContext2D | undefined;
+40 -32
View File
@@ -421,11 +421,11 @@ export class InputHandler extends Disposable implements IInputHandler {
* - undefined (void):
* all handlers were sync, no stack save, continue normally with next chunk
* - Promise\<boolean\>:
* execution stopped at async handler, stack saved, continue with
* same chunk and the promise resolve value as `promiseResult` until the method returns `undefined`
* execution stopped at async handler, stack saved, continue with same chunk and the promise
* resolve value as `promiseResult` until the method returns `undefined`
*
* Note: This method should only be called by `Terminal.write` to ensure correct execution order and
* proper continuation of async parser handlers.
* Note: This method should only be called by `Terminal.write` to ensure correct execution order
* and proper continuation of async parser handlers.
*/
public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean> {
let result: void | Promise<boolean>;
@@ -804,12 +804,13 @@ export class InputHandler extends Disposable implements IInputHandler {
// find last taken cell - last cell can have 3 different states:
// - hasContent(true) + hasWidth(1): narrow char - we are done
// - hasWidth(0): second part of wide char - we are done
// - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one cell further back
// - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one
// cell further back
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) {
this._activeBuffer.x--;
// We do this only once, since width=1 + hasContent=false currently happens only once before
// early wrapping of a wide char.
// We do this only once, since width=1 + hasContent=false currently happens only once
// before early wrapping of a wide char.
// This needs to be fixed once we support graphemes taking more than 2 cells.
}
}
@@ -1164,8 +1165,8 @@ export class InputHandler extends Disposable implements IInputHandler {
}
/**
* Helper method to reset cells in a terminal row.
* The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false.
* Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of
* the terminal and the isWrapped property is set to false.
* @param y row index
*/
private _resetBufferLine(y: number, respectProtect: boolean = false): void {
@@ -1362,8 +1363,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
*
* @vt: #Y CSI ICH "Insert Characters" "CSI Ps @" "Insert `Ps` (blank) characters (default = 1)."
* The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the blank characters.
* Text between the cursor and right margin moves to the right. Characters moved past the right margin are lost.
* The ICH sequence inserts `Ps` blank characters. The cursor remains at the beginning of the
* blank characters. Text between the cursor and right margin moves to the right. Characters moved
* past the right margin are lost.
*
*
* FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)
@@ -1388,8 +1390,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* Delete Ps Character(s) (default = 1) (DCH).
*
* @vt: #Y CSI DCH "Delete Character" "CSI Ps P" "Delete `Ps` characters (default=1)."
* As characters are deleted, the remaining characters between the cursor and right margin move to the left.
* Character attributes move with the characters. The terminal adds blank characters at the right margin.
* As characters are deleted, the remaining characters between the cursor and right margin move to
* the left. Character attributes move with the characters. The terminal adds blank characters at
* the right margin.
*
*
* FIXME: check against xterm - should not work outside of scroll margins (see VT520 manual)
@@ -1514,9 +1517,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.
*
* @vt: #Y CSI DECIC "Insert Columns" "CSI Ps ' }" "Insert `Ps` columns at cursor position."
* DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll margins,
* moving content to the right. Content at the right margin is lost.
* DECIC has no effect outside the scrolling margins.
* DECIC inserts `Ps` times blank columns at the cursor position for all lines with the scroll
* margins, moving content to the right. Content at the right margin is lost. DECIC has no effect
* outside the scrolling margins.
*/
public insertColumns(params: IParams): boolean {
if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) {
@@ -1597,12 +1600,12 @@ export class InputHandler extends Disposable implements IInputHandler {
* - wrap around is respected
* - any valid sequence resets the carried forward char
*
* Note: To get reset on a valid sequence working correctly without much runtime penalty,
* the preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`.
* Note: To get reset on a valid sequence working correctly without much runtime penalty, the
* preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`.
*
* @vt: #Y CSI REP "Repeat Preceding Character" "CSI Ps b" "Repeat preceding character `Ps` times (default=1)."
* REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is set.
* REP has no effect if the sequence does not follow a printable ASCII character
* REP repeats the previous character `Ps` times advancing the cursor, also wrapping if DECAWM is
* set. REP has no effect if the sequence does not follow a printable ASCII character
* (NOOP for any other sequence in between or NON ASCII characters).
*/
public repeatPrecedingCharacter(params: IParams): boolean {
@@ -2463,7 +2466,8 @@ export class InputHandler extends Disposable implements IInputHandler {
* | 5 | Dashed underline. | #Y |
* | other | Single underline. Same as `SGR 4 m`. | #Y |
*
* Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58) as follows:
* Extended colors are supported for foreground (Ps=38), background (Ps=48) and underline (Ps=58)
* as follows:
*
* | Ps + 1 | Meaning | Support |
* | ------ | ------------------------------------------------------------- | ------- |
@@ -2673,8 +2677,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* http://vt100.net/docs/vt220-rm/table4-10.html
*
* @vt: #Y CSI DECSTR "Soft Terminal Reset" "CSI ! p" "Reset several terminal attributes to initial state."
* There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full terminal bootstrap,
* DECSTR only resets certain attributes. For most needs DECSTR should be sufficient.
* There are two terminal reset sequences - RIS and DECSTR. While RIS performs almost a full
* terminal bootstrap, DECSTR only resets certain attributes. For most needs DECSTR should be
* sufficient.
*
* The following terminal attributes are reset to default values:
* - IRM is reset (dafault = false)
@@ -2899,7 +2904,8 @@ export class InputHandler extends Disposable implements IInputHandler {
* Icon name is not supported. For Window Title see below.
*
* @vt: #Y OSC 2 "Set Windows Title" "OSC 2 ; Pt BEL" "Set window title."
* xterm.js does not manipulate the title directly, instead exposes changes via the event `Terminal.onTitleChange`.
* xterm.js does not manipulate the title directly, instead exposes changes via the event
* `Terminal.onTitleChange`.
*/
public setTitle(data: string): boolean {
this._windowTitle = data;
@@ -2920,9 +2926,10 @@ export class InputHandler extends Disposable implements IInputHandler {
* OSC 4; <num> ; <text> ST (set ANSI color <num> to <text>)
*
* @vt: #Y OSC 4 "Set ANSI color" "OSC 4 ; c ; spec BEL" "Change color number `c` to the color specified by `spec`."
* `c` is the color index between 0 and 255. The color format of `spec` is derived from `XParseColor` (see OSC 10 for supported formats).
* There may be multipe `c ; spec` pairs present in the same instruction.
* If `spec` contains `?` the terminal returns a sequence with the currently set color.
* `c` is the color index between 0 and 255. The color format of `spec` is derived from
* `XParseColor` (see OSC 10 for supported formats). There may be multipe `c ; spec` pairs present
* in the same instruction. If `spec` contains `?` the terminal returns a sequence with the
* currently set color.
*/
public setOrReportIndexedColor(data: string): boolean {
const event: IColorEvent = [];
@@ -2962,9 +2969,10 @@ export class InputHandler extends Disposable implements IInputHandler {
*
* @vt: #Y OSC 8 "Create hyperlink" "OSC 8 ; params ; uri BEL" "Create a hyperlink to `uri` using `params`."
* `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an
* optional list of key=value assignments, separated by the : character. Example: `id=xyz123:foo=bar:baz=quux`.
* Currently only the id key is defined. Cells that share the same ID and URI share hover feedback.
* Use `OSC 8 ; ; BEL` to finish the current hyperlink.
* optional list of key=value assignments, separated by the : character.
* Example: `id=xyz123:foo=bar:baz=quux`.
* Currently only the id key is defined. Cells that share the same ID and URI share hover
* feedback. Use `OSC 8 ; ; BEL` to finish the current hyperlink.
*/
public setHyperlink(data: string): boolean {
const args = data.split(';');
@@ -3351,8 +3359,8 @@ export class InputHandler extends Disposable implements IInputHandler {
* Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)
*
* @vt: #P[Limited support, see below.] DCS DECRQSS "Request Selection or Setting" "DCS $ q Pt ST" "Request several terminal settings."
* Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the corresponding CSI string,
* `ESC P 0 ST` for invalid requests.
* Response is in the form `ESC P 1 $ r Pt ST` for valid requests, where `Pt` contains the
* corresponding CSI string, `ESC P 0 ST` for invalid requests.
*
* Supported requests and responses:
*
+14
View File
@@ -104,4 +104,18 @@ describe('SortedList', () => {
{ key: 10 }
]);
});
describe('values', () => {
it('should iterate correctly when list items change during iteration', () => {
list.insert(1);
list.insert(2);
list.insert(3);
list.insert(4);
const visited: number[] = [];
for (const item of list.values()) {
visited.push(item);
list.delete(item);
}
deepStrictEqual(visited, [1, 2, 3, 4]);
});
});
});
+2 -1
View File
@@ -89,7 +89,8 @@ export class SortedList<T> {
}
public values(): IterableIterator<T> {
return this._array.values();
// Duplicate the array to avoid issues when _array changes while iterating
return [...this._array].values();
}
private _search(key: number): number {
+2
View File
@@ -38,6 +38,8 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
export type CursorStyle = 'block' | 'underline' | 'bar';
export type CursorInactiveStyle = 'outline' | 'block' | 'bar' | 'underline' | 'none';
export type XtermListener = (...args: any[]) => void;
/**
+2 -2
View File
@@ -270,8 +270,8 @@ export class Buffer implements IBuffer {
private _batchedMemoryCleanup(): boolean {
let normalRun = true;
if (this._memoryCleanupPosition >= this.lines.length) {
// cleanup made it once through all lines, thus rescan in loop below to also catch shifted lines,
// which should finish rather quick if there are no more cleanups pending
// cleanup made it once through all lines, thus rescan in loop below to also catch shifted
// lines, which should finish rather quick if there are no more cleanups pending
this._memoryCleanupPosition = 0;
normalRun = false;
}
+2 -2
View File
@@ -11,9 +11,9 @@ export class Marker implements IMarker {
private static _nextId = 1;
public isDisposed: boolean = false;
private _disposables: IDisposable[] = [];
private readonly _disposables: IDisposable[] = [];
private _id: number = Marker._nextId++;
private readonly _id: number = Marker._nextId++;
public get id(): number { return this._id; }
private readonly _onDispose = this.register(new EventEmitter<void>());
+2 -1
View File
@@ -380,7 +380,8 @@ export function evaluateKeyboardEvent(
result.type = KeyboardResultType.SELECT_ALL;
}
} else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) {
// Include only keys that that result in a _single_ character; don't include num lock, volume up, etc.
// Include only keys that that result in a _single_ character; don't include num lock,
// volume up, etc.
result.key = ev.key;
} else if (ev.key && ev.ctrlKey) {
if (ev.key === '_') { // ^_
+2 -2
View File
@@ -28,8 +28,8 @@ export function utf32ToString(data: Uint32Array, start: number = 0, end: number
for (let i = start; i < end; ++i) {
let codepoint = data[i];
if (codepoint > 0xFFFF) {
// JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair
// conversion rules:
// JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate
// pair conversion rules:
// - subtract 0x10000 from code point, leaving a 20 bit number
// - add high 10 bits to 0xD800 --> first surrogate
// - add low 10 bits to 0xDC00 --> second surrogate
+4 -3
View File
@@ -181,9 +181,10 @@ export class WriteBuffer extends Disposable {
/**
* If a promise takes long to resolve, we should schedule continuation behind setTimeout.
* This might already be too late, if our .then enters really late (executor + prev thens took very long).
* This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work),
* but we can at least schedule a screen update as we gain control.
* This might already be too late, if our .then enters really late (executor + prev thens
* took very long). This cannot be solved here for the handler itself (it is the handlers
* responsibility to slice hard work), but we can at least schedule a screen update as we
* gain control.
*/
const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS
? setTimeout(() => this._innerWrite(0, r))
+6 -6
View File
@@ -532,9 +532,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
} else {
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
/**
* Reject further parsing on improper continuation after pausing.
* This is a really bad condition with screwed up execution order and prolly messed up
* terminal state, therefore we exit hard with an exception and reject any further parsing.
* Reject further parsing on improper continuation after pausing. This is a really bad
* condition with screwed up execution order and prolly messed up terminal state,
* therefore we exit hard with an exception and reject any further parsing.
*
* Note: With `Terminal.write` usage this exception should never occur, as the top level
* calls are guaranteed to handle async conditions properly. If you ever encounter this
@@ -542,9 +542,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
* continuation of a running async handler.
*
* It is possible to get rid of this error by calling `reset`. But dont rely on that,
* as the pending async handler still might mess up the terminal later. Instead fix the faulty
* async handling, so this error will not be thrown anymore.
* It is possible to get rid of this error by calling `reset`. But dont rely on that, as
* the pending async handler still might mess up the terminal later. Instead fix the
* faulty async handling, so this error will not be thrown anymore.
*/
this._parseStack.state = ParserStackType.FAIL;
throw new Error('improper continuation due to previous async handler, giving up parsing');
+2 -1
View File
@@ -43,7 +43,8 @@ export class BufferService extends Disposable implements IBufferService {
this.cols = cols;
this.rows = rows;
this.buffers.resize(cols, rows);
// TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward event
// TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward
// event
this._onResize.fire({ cols, rows });
}
+1 -13
View File
@@ -35,12 +35,7 @@ export class DecorationService extends Disposable implements IDecorationService
constructor() {
super();
this.register(toDisposable(() => {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
}
this.reset();
}));
this.register(toDisposable(() => this.reset()));
}
public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
@@ -92,13 +87,6 @@ export class DecorationService extends Disposable implements IDecorationService
}
});
}
public dispose(): void {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
}
this.reset();
}
}
class Decoration extends Disposable implements IInternalDecoration {
+1
View File
@@ -15,6 +15,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
cursorBlink: false,
cursorStyle: 'block',
cursorWidth: 1,
cursorInactiveStyle: 'outline',
customGlyphs: true,
drawBoldTextInBrightColors: true,
fastScrollModifier: 'alt',
+2 -1
View File
@@ -5,7 +5,7 @@
import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, IOscLinkData } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, CursorInactiveStyle, IOscLinkData } from 'common/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { IDecorationOptions, IDecoration, ILinkHandler, IWindowsPty, ILogger } from 'xterm';
@@ -212,6 +212,7 @@ export interface ITerminalOptions {
cursorBlink?: boolean;
cursorStyle?: CursorStyle;
cursorWidth?: number;
cursorInactiveStyle?: CursorInactiveStyle;
customGlyphs?: boolean;
disableStdin?: boolean;
drawBoldTextInBrightColors?: boolean;