Merge branch 'master' into remove-16-ci
This commit is contained in:
@@ -37,7 +37,7 @@ export class CanvasAddon extends Disposable implements ITerminalAddon , ICanvasA
|
||||
const coreService = core.coreService;
|
||||
const optionsService = core.optionsService;
|
||||
const screenElement = core.screenElement!;
|
||||
const linkifier = core.linkifier2;
|
||||
const linkifier = core.linkifier!;
|
||||
|
||||
const unsafeCore = core as any;
|
||||
const bufferService: IBufferService = unsafeCore._bufferService;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
@@ -22,6 +22,7 @@ import { IRenderLayer } from './Types';
|
||||
export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
private _observerDisposable = this.register(new MutableDisposable());
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
@@ -60,7 +61,11 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this._updateDimensions();
|
||||
|
||||
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
||||
this._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
|
||||
this.register(this._coreBrowserService.onWindowChange(w => {
|
||||
this._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
|
||||
}));
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
|
||||
@@ -28,5 +28,10 @@ test.describe('Canvas Renderer Integration Tests', () => {
|
||||
test.skip(({ browserName }) => browserName === 'webkit');
|
||||
|
||||
injectSharedRendererTests(ctxWrapper);
|
||||
injectSharedRendererTestsStandalone(ctxWrapper);
|
||||
injectSharedRendererTestsStandalone(ctxWrapper, async () => {
|
||||
await ctx.page.evaluate(`
|
||||
window.addon = new window.CanvasAddon(true);
|
||||
window.term.loadAddon(window.addon);
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { openTerminal, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
@@ -75,7 +75,15 @@ describe('FitAddon', () => {
|
||||
await page.evaluate(`window.term = new Terminal()`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
await loadFit();
|
||||
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
||||
const dimensions: { cols: number, rows: number } | undefined = await page.evaluate(`window.fit.proposeDimensions()`);
|
||||
// The value of dims will be undefined if the char measure strategy falls back to the DOM
|
||||
// method, so only assert if it's not undefined.
|
||||
if (dimensions) {
|
||||
assert.isAbove(dimensions.cols, 85);
|
||||
assert.isBelow(dimensions.cols, 88);
|
||||
assert.isAbove(dimensions.rows, 24);
|
||||
assert.isBelow(dimensions.rows, 29);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sinon": "^5.0.1",
|
||||
"axios": "^0.21.2",
|
||||
"axios": "^1.6.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"sinon": "6.3.5",
|
||||
"yauzl": "^2.10.0"
|
||||
|
||||
@@ -45,17 +45,36 @@ array-from@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
|
||||
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
|
||||
|
||||
axios@^0.21.2:
|
||||
version "0.21.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
|
||||
integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
axios@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102"
|
||||
integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.0"
|
||||
follow-redirects "^1.15.0"
|
||||
form-data "^4.0.0"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
diff@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
||||
@@ -66,10 +85,10 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
follow-redirects@^1.14.0:
|
||||
version "1.14.8"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
|
||||
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
|
||||
follow-redirects@^1.15.0:
|
||||
version "1.15.3"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a"
|
||||
integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==
|
||||
|
||||
font-finder@^1.0.3:
|
||||
version "1.0.4"
|
||||
@@ -95,6 +114,15 @@ font-ligatures@^1.4.1:
|
||||
lru-cache "^6.0.0"
|
||||
opentype.js "^0.8.0"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
get-system-fonts@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6"
|
||||
@@ -140,6 +168,18 @@ lru-cache@^6.0.0:
|
||||
dependencies:
|
||||
yallist "^4.0.0"
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
@@ -183,6 +223,11 @@ promise-stream-reader@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
|
||||
|
||||
sinon@6.3.5:
|
||||
version "6.3.5"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0"
|
||||
|
||||
@@ -9,7 +9,6 @@ import { SerializeAddon } from './SerializeAddon';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
@@ -83,6 +82,36 @@ describe('SerializeAddon', () => {
|
||||
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
|
||||
});
|
||||
|
||||
describe('ISerializeOptions.range', () => {
|
||||
it('should serialize the top line', async () => {
|
||||
await writeP(terminal, 'hello\r\nworld');
|
||||
assert.equal(serializeAddon.serialize({
|
||||
range: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
}), 'hello');
|
||||
});
|
||||
it('should serialize multiple lines from the top', async () => {
|
||||
await writeP(terminal, 'hello\r\nworld');
|
||||
assert.equal(serializeAddon.serialize({
|
||||
range: {
|
||||
start: 0,
|
||||
end: 1
|
||||
}
|
||||
}), 'hello\r\nworld');
|
||||
});
|
||||
it('should serialize lines in the middle', async () => {
|
||||
await writeP(terminal, 'hello\r\nworld');
|
||||
assert.equal(serializeAddon.serialize({
|
||||
range: {
|
||||
start: 1,
|
||||
end: 1
|
||||
}
|
||||
}), 'world');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('html', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||
import type { SerializeAddon as ISerializeApi } from '@xterm/addon-serialize';
|
||||
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
|
||||
import { IAttributeData, IColor } from 'common/Types';
|
||||
|
||||
@@ -21,24 +21,24 @@ abstract class BaseSerializeHandler {
|
||||
) {
|
||||
}
|
||||
|
||||
public serialize(range: IBufferRange): string {
|
||||
public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
|
||||
// we need two of them to flip between old and new cell
|
||||
const cell1 = this._buffer.getNullCell();
|
||||
const cell2 = this._buffer.getNullCell();
|
||||
let oldCell = cell1;
|
||||
|
||||
const startRow = range.start.x;
|
||||
const endRow = range.end.x;
|
||||
const startColumn = range.start.y;
|
||||
const endColumn = range.end.y;
|
||||
const startRow = range.start.y;
|
||||
const endRow = range.end.y;
|
||||
const startColumn = range.start.x;
|
||||
const endColumn = range.end.x;
|
||||
|
||||
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const line = this._buffer.getLine(row);
|
||||
if (line) {
|
||||
const startLineColumn = row !== range.start.x ? 0 : startColumn;
|
||||
const endLineColumn = row !== range.end.x ? line.length : endColumn;
|
||||
const startLineColumn = row === range.start.y ? startColumn : 0;
|
||||
const endLineColumn = row === range.end.y ? endColumn: line.length;
|
||||
for (let col = startLineColumn; col < endLineColumn; col++) {
|
||||
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
|
||||
if (!c) {
|
||||
@@ -54,14 +54,14 @@ abstract class BaseSerializeHandler {
|
||||
|
||||
this._afterSerialize();
|
||||
|
||||
return this._serializeString();
|
||||
return this._serializeString(excludeFinalCursorPosition);
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void { }
|
||||
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
|
||||
protected _afterSerialize(): void { }
|
||||
protected _serializeString(): string { return ''; }
|
||||
protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; }
|
||||
}
|
||||
|
||||
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
@@ -353,7 +353,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
protected _serializeString(): string {
|
||||
protected _serializeString(excludeFinalCursorPosition: boolean): string {
|
||||
let rowEnd = this._allRows.length;
|
||||
|
||||
// the fixup is only required for data without scrollback
|
||||
@@ -374,29 +374,31 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
|
||||
// restore the cursor
|
||||
const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
|
||||
const realCursorCol = this._buffer.cursorX;
|
||||
if (!excludeFinalCursorPosition) {
|
||||
const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
|
||||
const realCursorCol = this._buffer.cursorX;
|
||||
|
||||
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
|
||||
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
|
||||
|
||||
const moveRight = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}C`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}D`;
|
||||
const moveRight = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}C`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}D`;
|
||||
}
|
||||
};
|
||||
const moveDown = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}B`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}A`;
|
||||
}
|
||||
};
|
||||
|
||||
if (cursorMoved) {
|
||||
moveDown(realCursorRow - this._lastCursorRow);
|
||||
moveRight(realCursorCol - this._lastCursorCol);
|
||||
}
|
||||
};
|
||||
const moveDown = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}B`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}A`;
|
||||
}
|
||||
};
|
||||
|
||||
if (cursorMoved) {
|
||||
moveDown(realCursorRow - this._lastCursorRow);
|
||||
moveRight(realCursorCol - this._lastCursorCol);
|
||||
}
|
||||
|
||||
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
|
||||
@@ -419,14 +421,21 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
this._terminal = terminal;
|
||||
}
|
||||
|
||||
private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
|
||||
private _serializeBufferByScrollback(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
|
||||
const maxRows = buffer.length;
|
||||
const handler = new StringSerializeHandler(buffer, terminal);
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return this._serializeBufferByRange(terminal, buffer, {
|
||||
start: maxRows - correctRows,
|
||||
end: maxRows - 1
|
||||
}, false);
|
||||
}
|
||||
|
||||
private _serializeBufferByRange(terminal: Terminal, buffer: IBuffer, range: ISerializeRange, excludeFinalCursorPosition: boolean): string {
|
||||
const handler = new StringSerializeHandler(buffer, terminal);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
});
|
||||
start: { x: 0, y: typeof range.start === 'number' ? range.start : range.start.line },
|
||||
end: { x: terminal.cols, y: typeof range.end === 'number' ? range.end : range.end.line }
|
||||
}, excludeFinalCursorPosition);
|
||||
}
|
||||
|
||||
private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {
|
||||
@@ -438,16 +447,16 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
const scrollback = options.scrollback;
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
start: { x: 0, y: maxRows - correctRows },
|
||||
end: { x: terminal.cols, y: maxRows - 1 }
|
||||
});
|
||||
}
|
||||
|
||||
const selection = this._terminal?.getSelectionPosition();
|
||||
if (selection !== undefined) {
|
||||
return handler.serialize({
|
||||
start: { x: selection.start.y, y: selection.start.x },
|
||||
end: { x: selection.end.y, y: selection.end.x }
|
||||
start: { x: selection.start.x, y: selection.start.y },
|
||||
end: { x: selection.end.x, y: selection.end.y }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -490,12 +499,14 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
}
|
||||
|
||||
// Normal buffer
|
||||
let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback);
|
||||
let content = options?.range
|
||||
? this._serializeBufferByRange(this._terminal, this._terminal.buffer.normal, options.range, true)
|
||||
: this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.normal, options?.scrollback);
|
||||
|
||||
// Alternate buffer
|
||||
if (!options?.excludeAltBuffer) {
|
||||
if (this._terminal.buffer.active.type === 'alternate') {
|
||||
const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined);
|
||||
const alternativeScreenContent = this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.alternate, undefined);
|
||||
content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`;
|
||||
}
|
||||
}
|
||||
@@ -519,19 +530,6 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
|
||||
interface ISerializeOptions {
|
||||
scrollback?: number;
|
||||
excludeModes?: boolean;
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
interface IHTMLSerializeOptions {
|
||||
scrollback: number;
|
||||
onlySelection: boolean;
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
|
||||
export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
private _currentRow: string = '';
|
||||
|
||||
|
||||
+19
-2
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||
import { Terminal, ITerminalAddon, IMarker, IBufferRange } from '@xterm/xterm';
|
||||
|
||||
declare module '@xterm/addon-serialize' {
|
||||
/**
|
||||
@@ -48,10 +48,16 @@ declare module '@xterm/addon-serialize' {
|
||||
}
|
||||
|
||||
export interface ISerializeOptions {
|
||||
/**
|
||||
* The row range to serialize. The an explicit range is specified, the cursor will get its final
|
||||
* repositioning.
|
||||
*/
|
||||
range?: ISerializeRange;
|
||||
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
|
||||
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
|
||||
* serialized.
|
||||
* serialized. This will be ignored if {@link range} is specified.
|
||||
*/
|
||||
scrollback?: number;
|
||||
|
||||
@@ -85,4 +91,15 @@ declare module '@xterm/addon-serialize' {
|
||||
*/
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
|
||||
export interface ISerializeRange {
|
||||
/**
|
||||
* The line to start serializing (inclusive).
|
||||
*/
|
||||
start: IMarker | number;
|
||||
/**
|
||||
* The line to end serializing (inclusive).
|
||||
*/
|
||||
end: IMarker | number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _charAtlasDisposable = this.register(new MutableDisposable());
|
||||
private _charAtlas: ITextureAtlas | undefined;
|
||||
private _devicePixelRatio: number;
|
||||
private _observerDisposable = this.register(new MutableDisposable());
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
private _workCell: CellData = new CellData();
|
||||
@@ -80,7 +81,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._core = (this._terminal as any)._core;
|
||||
|
||||
this._renderLayers = [
|
||||
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService)
|
||||
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier!, this._coreBrowserService, _optionsService, this._themeService)
|
||||
];
|
||||
this.dimensions = createRenderDimensions();
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
@@ -123,7 +124,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._requestRedrawViewport();
|
||||
}));
|
||||
|
||||
this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
||||
this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
|
||||
this.register(this._coreBrowserService.onWindowChange(w => {
|
||||
this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
|
||||
}));
|
||||
|
||||
this._core.screenElement!.appendChild(this._canvas);
|
||||
|
||||
|
||||
@@ -29,5 +29,10 @@ test.describe('WebGL Renderer Integration Tests', async () => {
|
||||
}
|
||||
|
||||
injectSharedRendererTests(ctxWrapper);
|
||||
injectSharedRendererTestsStandalone(ctxWrapper);
|
||||
injectSharedRendererTestsStandalone(ctxWrapper, async () => {
|
||||
await ctx.page.evaluate(`
|
||||
window.addon = new window.WebglAddon(true);
|
||||
window.term.loadAddon(window.addon);
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { Linkifier2 } from 'browser/Linkifier2';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBufferService } from 'common/TestUtils.test';
|
||||
import { ILink } from 'browser/Types';
|
||||
import { LinkProviderService } from 'browser/services/LinkProviderService';
|
||||
import jsdom = require('jsdom');
|
||||
|
||||
class TestLinkifier2 extends Linkifier2 {
|
||||
class TestLinkifier2 extends Linkifier {
|
||||
public set currentLink(link: any) {
|
||||
this._currentLink = link;
|
||||
}
|
||||
@@ -43,8 +45,9 @@ describe('Linkifier2', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const dom = new jsdom.JSDOM();
|
||||
bufferService = new MockBufferService(100, 10);
|
||||
linkifier = new TestLinkifier2(bufferService);
|
||||
linkifier = new TestLinkifier2(dom.window.document.createElement('div'), null!, null!, bufferService, new LinkProviderService());
|
||||
linkifier.currentLink = {
|
||||
link,
|
||||
state: {
|
||||
@@ -4,18 +4,14 @@
|
||||
*/
|
||||
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkProvider, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable, disposeArray, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
|
||||
|
||||
export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _element: HTMLElement | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _linkProviders: ILinkProvider[] = [];
|
||||
export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
||||
protected _currentLink: ILinkWithState | undefined;
|
||||
private _mouseDownLink: ILinkWithState | undefined;
|
||||
@@ -33,39 +29,24 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
private readonly _element: HTMLElement,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ILinkProviderService private readonly _linkProviderService: ILinkProviderService
|
||||
) {
|
||||
super();
|
||||
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
|
||||
this.register(toDisposable(() => {
|
||||
this._lastMouseEvent = undefined;
|
||||
// Clear out link providers as they could easily cause an embedder memory leak
|
||||
this._activeProviderReplies?.clear();
|
||||
}));
|
||||
// Listen to resize to catch the case where it's resized and the cursor is out of the viewport.
|
||||
this.register(this._bufferService.onResize(() => {
|
||||
this._clearCurrentLink();
|
||||
this._wasResized = true;
|
||||
}));
|
||||
}
|
||||
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
this._linkProviders.push(linkProvider);
|
||||
return {
|
||||
dispose: () => {
|
||||
// Remove the link provider from the list
|
||||
const providerIndex = this._linkProviders.indexOf(linkProvider);
|
||||
|
||||
if (providerIndex !== -1) {
|
||||
this._linkProviders.splice(providerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void {
|
||||
this._element = element;
|
||||
this._mouseService = mouseService;
|
||||
this._renderService = renderService;
|
||||
|
||||
this.register(addDisposableDomListener(this._element, 'mouseleave', () => {
|
||||
this._isMouseOut = true;
|
||||
this._clearCurrentLink();
|
||||
@@ -78,10 +59,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _handleMouseMove(event: MouseEvent): void {
|
||||
this._lastMouseEvent = event;
|
||||
|
||||
if (!this._element || !this._mouseService) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
|
||||
if (!position) {
|
||||
return;
|
||||
@@ -142,7 +119,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
let linkProvided = false;
|
||||
|
||||
// There is no link cached, so ask for one
|
||||
for (const [i, linkProvider] of this._linkProviders.entries()) {
|
||||
for (const [i, linkProvider] of this._linkProviderService.linkProviders.entries()) {
|
||||
if (useLineCache) {
|
||||
const existingReply = this._activeProviderReplies?.get(i);
|
||||
// If there isn't a reply, the provider hasn't responded yet.
|
||||
@@ -164,7 +141,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
|
||||
// If all providers have responded, remove lower priority links that intersect ranges of
|
||||
// higher priority links
|
||||
if (this._activeProviderReplies?.size === this._linkProviders.length) {
|
||||
if (this._activeProviderReplies?.size === this._linkProviderService.linkProviders.length) {
|
||||
this._removeIntersectingLinks(position.y, this._activeProviderReplies);
|
||||
}
|
||||
});
|
||||
@@ -220,7 +197,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
|
||||
// Check if all the providers have responded
|
||||
if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
|
||||
if (this._activeProviderReplies.size === this._linkProviderService.linkProviders.length && !linkProvided) {
|
||||
// Respect the order of the link providers
|
||||
for (let j = 0; j < this._activeProviderReplies.size; j++) {
|
||||
const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
|
||||
@@ -240,7 +217,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
|
||||
private _handleMouseUp(event: MouseEvent): void {
|
||||
if (!this._element || !this._mouseService || !this._currentLink) {
|
||||
if (!this._currentLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -255,7 +232,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
|
||||
private _clearCurrentLink(startRow?: number, endRow?: number): void {
|
||||
if (!this._element || !this._currentLink || !this._lastMouseEvent) {
|
||||
if (!this._currentLink || !this._lastMouseEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -268,7 +245,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
|
||||
private _handleNewLink(linkWithState: ILinkWithState): void {
|
||||
if (!this._element || !this._lastMouseEvent || !this._mouseService) {
|
||||
if (!this._lastMouseEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -299,7 +276,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
|
||||
this._currentLink.state.decorations.pointerCursor = v;
|
||||
if (this._currentLink.state.isHovered) {
|
||||
this._element?.classList.toggle('xterm-cursor-pointer', v);
|
||||
this._element.classList.toggle('xterm-cursor-pointer', v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,29 +296,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
|
||||
// Listen to viewport changes to re-render the link under the cursor (only when the line the
|
||||
// link is on changes)
|
||||
if (this._renderService) {
|
||||
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
|
||||
// Sanity check, this shouldn't happen in practice as this listener would be disposed
|
||||
if (!this._currentLink) {
|
||||
return;
|
||||
}
|
||||
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
|
||||
// cleared.
|
||||
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
|
||||
const end = this._bufferService.buffer.ydisp + 1 + e.end;
|
||||
// Only clear the link if the viewport change happened on this line
|
||||
if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
|
||||
this._clearCurrentLink(start, end);
|
||||
if (this._lastMouseEvent && this._element) {
|
||||
// re-eval previously active link after changes
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
|
||||
if (position) {
|
||||
this._askForLink(position, false);
|
||||
}
|
||||
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
|
||||
// Sanity check, this shouldn't happen in practice as this listener would be disposed
|
||||
if (!this._currentLink) {
|
||||
return;
|
||||
}
|
||||
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
|
||||
// cleared.
|
||||
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
|
||||
const end = this._bufferService.buffer.ydisp + 1 + e.end;
|
||||
// Only clear the link if the viewport change happened on this line
|
||||
if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
|
||||
this._clearCurrentLink(start, end);
|
||||
if (this._lastMouseEvent) {
|
||||
// re-eval previously active link after changes
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
|
||||
if (position) {
|
||||
this._askForLink(position, false);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
|
||||
import { IBufferRange, ILink } from 'browser/Types';
|
||||
import { ILinkProvider } from 'browser/services/Services';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
|
||||
/**
|
||||
* Debounces calls to render terminal rows using animation frames.
|
||||
@@ -16,14 +17,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
private _refreshCallbacks: FrameRequestCallback[] = [];
|
||||
|
||||
constructor(
|
||||
private _parentWindow: Window,
|
||||
private _renderCallback: (start: number, end: number) => void
|
||||
private _renderCallback: (start: number, end: number) => void,
|
||||
private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._animationFrame) {
|
||||
this._parentWindow.cancelAnimationFrame(this._animationFrame);
|
||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +32,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
this._refreshCallbacks.push(callback);
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
return this._animationFrame;
|
||||
}
|
||||
@@ -49,7 +50,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
return;
|
||||
}
|
||||
|
||||
this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
|
||||
+41
-33
@@ -23,10 +23,10 @@
|
||||
|
||||
import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { Linkifier2 } from 'browser/Linkifier2';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
import { OscLinkProvider } from 'browser/OscLinkProvider';
|
||||
import { CharacterJoinerHandler, CustomKeyEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
|
||||
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
|
||||
import { Viewport } from 'browser/Viewport';
|
||||
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
|
||||
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
|
||||
@@ -39,9 +39,9 @@ import { CoreBrowserService } from 'browser/services/CoreBrowserService';
|
||||
import { MouseService } from 'browser/services/MouseService';
|
||||
import { RenderService } from 'browser/services/RenderService';
|
||||
import { SelectionService } from 'browser/services/SelectionService';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
import { color, rgba } from 'common/Color';
|
||||
import { channels, color } from 'common/Color';
|
||||
import { CoreTerminal } from 'common/CoreTerminal';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
@@ -57,6 +57,7 @@ import { IDecorationService } from 'common/services/Services';
|
||||
import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker } from '@xterm/xterm';
|
||||
import { WindowsOptionsReportType } from '../common/InputHandler';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { LinkProviderService } from 'browser/services/LinkProviderService';
|
||||
|
||||
export class Terminal extends CoreTerminal implements ITerminal {
|
||||
public textarea: HTMLTextAreaElement | undefined;
|
||||
@@ -69,14 +70,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _helperContainer: HTMLElement | undefined;
|
||||
private _compositionView: HTMLElement | undefined;
|
||||
|
||||
public linkifier: ILinkifier2 | undefined;
|
||||
private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
|
||||
|
||||
public browser: IBrowser = Browser as any;
|
||||
|
||||
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
|
||||
private _customWheelEventHandler: CustomWheelEventHandler | undefined;
|
||||
|
||||
// browser services
|
||||
// Browser services
|
||||
private _decorationService: DecorationService;
|
||||
private _linkProviderService: ILinkProviderService;
|
||||
|
||||
// Optional browser services
|
||||
private _charSizeService: ICharSizeService | undefined;
|
||||
private _coreBrowserService: ICoreBrowserService | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
@@ -112,7 +118,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
*/
|
||||
private _unprocessedDeadKey: boolean = false;
|
||||
|
||||
public linkifier2: ILinkifier2;
|
||||
public viewport: IViewport | undefined;
|
||||
private _compositionHelper: ICompositionHelper | undefined;
|
||||
private _accessibilityManager: MutableDisposable<AccessibilityManager> = this.register(new MutableDisposable());
|
||||
@@ -148,10 +153,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this._setup();
|
||||
|
||||
this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
|
||||
this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
|
||||
this._decorationService = this._instantiationService.createInstance(DecorationService);
|
||||
this._instantiationService.setService(IDecorationService, this._decorationService);
|
||||
this._linkProviderService = this._instantiationService.createInstance(LinkProviderService);
|
||||
this._instantiationService.setService(ILinkProviderService, this._linkProviderService);
|
||||
this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
|
||||
|
||||
// Setup InputHandler listeners
|
||||
this.register(this._inputHandler.onRequestBell(() => this._onBell.fire()));
|
||||
@@ -205,17 +211,17 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
switch (req.type) {
|
||||
case ColorRequestType.REPORT:
|
||||
const channels = color.toColorRGB(acc === 'ansi'
|
||||
const colorRgb = color.toColorRGB(acc === 'ansi'
|
||||
? this._themeService.colors.ansi[req.index]
|
||||
: this._themeService.colors[acc]);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`);
|
||||
break;
|
||||
case ColorRequestType.SET:
|
||||
if (acc === 'ansi') {
|
||||
this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color));
|
||||
this._themeService.modifyColors(colors => colors.ansi[req.index] = channels.toColor(...req.color));
|
||||
} else {
|
||||
const narrowedAcc = acc;
|
||||
this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color));
|
||||
this._themeService.modifyColors(colors => colors[narrowedAcc] = channels.toColor(...req.color));
|
||||
}
|
||||
break;
|
||||
case ColorRequestType.RESTORE:
|
||||
@@ -260,11 +266,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
/**
|
||||
* Binds the desired focus behavior on a given terminal object.
|
||||
*/
|
||||
private _handleTextAreaFocus(ev: KeyboardEvent): void {
|
||||
private _handleTextAreaFocus(ev: FocusEvent): void {
|
||||
if (this.coreService.decPrivateModes.sendFocus) {
|
||||
this.coreService.triggerDataEvent(C0.ESC + '[I');
|
||||
}
|
||||
this.updateCursorStyle(ev);
|
||||
this.element!.classList.add('focus');
|
||||
this._showCursor();
|
||||
this._onFocus.fire();
|
||||
@@ -428,6 +433,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this.screenElement = this._document.createElement('div');
|
||||
this.screenElement.classList.add('xterm-screen');
|
||||
this.register(addDisposableDomListener(this.screenElement, 'mousemove', (ev: MouseEvent) => this.updateCursorStyle(ev)));
|
||||
// Create the container that will hold helpers like the textarea for
|
||||
// capturing DOM Events. Then produce the helpers.
|
||||
this._helperContainer = this._document.createElement('div');
|
||||
@@ -458,11 +464,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
));
|
||||
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
|
||||
|
||||
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
|
||||
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: FocusEvent) => this._handleTextAreaFocus(ev)));
|
||||
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
|
||||
this._helperContainer.appendChild(this.textarea);
|
||||
|
||||
|
||||
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
|
||||
this._instantiationService.setService(ICharSizeService, this._charSizeService);
|
||||
|
||||
@@ -482,6 +487,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
|
||||
this._helperContainer.appendChild(this._compositionView);
|
||||
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
|
||||
this.linkifier = this.register(this._instantiationService.createInstance(Linkifier, this.screenElement));
|
||||
|
||||
// Performance: Add viewport and helper elements from the fragment
|
||||
this.element.appendChild(fragment);
|
||||
|
||||
@@ -493,9 +503,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._renderService.setRenderer(this._createRenderer());
|
||||
}
|
||||
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
|
||||
this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea);
|
||||
this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)),
|
||||
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
|
||||
@@ -513,7 +520,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
|
||||
this.element,
|
||||
this.screenElement,
|
||||
this.linkifier2
|
||||
this.linkifier
|
||||
));
|
||||
this._instantiationService.setService(ISelectionService, this._selectionService);
|
||||
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
|
||||
@@ -533,7 +540,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));
|
||||
|
||||
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
|
||||
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
|
||||
|
||||
@@ -575,7 +581,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
private _createRenderer(): IRenderer {
|
||||
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier2);
|
||||
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -633,6 +639,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
|
||||
return false;
|
||||
}
|
||||
const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
|
||||
|
||||
if (amount === 0) {
|
||||
@@ -792,6 +801,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// do nothing, if app side handles wheel itself
|
||||
if (requestedEvents.wheel) return;
|
||||
|
||||
if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.buffer.hasScrollback) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
|
||||
@@ -847,7 +860,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
/**
|
||||
* Change the cursor style for different selection modes
|
||||
*/
|
||||
public updateCursorStyle(ev: KeyboardEvent): void {
|
||||
public updateCursorStyle(ev: KeyboardEvent | MouseEvent): void {
|
||||
if (this._selectionService?.shouldColumnSelect(ev)) {
|
||||
this.element!.classList.add('column-select');
|
||||
} else {
|
||||
@@ -878,21 +891,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
paste(data, this.textarea!, this.coreService, this.optionsService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a custom key event handler which is run before keys are processed,
|
||||
* giving consumers of xterm.js ultimate control as to what keys should be
|
||||
* processed by the terminal and what keys should not.
|
||||
* @param customKeyEventHandler The custom KeyboardEvent handler to attach.
|
||||
* This is a function that takes a KeyboardEvent, allowing consumers to stop
|
||||
* propagation and/or prevent the default action. The function returns whether
|
||||
* the event should be processed by xterm.js.
|
||||
*/
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
|
||||
this._customKeyEventHandler = customKeyEventHandler;
|
||||
}
|
||||
|
||||
public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
|
||||
this._customWheelEventHandler = customWheelEventHandler;
|
||||
}
|
||||
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
return this.linkifier2.registerLinkProvider(linkProvider);
|
||||
return this._linkProviderService.registerLinkProvider(linkProvider);
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
|
||||
@@ -48,6 +48,7 @@ export class MockTerminal implements ITerminal {
|
||||
public onRender!: IEvent<{ start: number, end: number }>;
|
||||
public onResize!: IEvent<{ cols: number, rows: number }>;
|
||||
public markers!: IMarker[];
|
||||
public linkifier: ILinkifier2 | undefined;
|
||||
public coreMouseService!: ICoreMouseService;
|
||||
public coreService!: ICoreService;
|
||||
public optionsService!: IOptionsService;
|
||||
@@ -86,6 +87,9 @@ export class MockTerminal implements ITerminal {
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -148,7 +152,6 @@ export class MockTerminal implements ITerminal {
|
||||
}
|
||||
public bracketedPasteMode!: boolean;
|
||||
public renderer!: IRenderer;
|
||||
public linkifier2!: ILinkifier2;
|
||||
public isFocused!: boolean;
|
||||
public options!: Required<ITerminalOptions>;
|
||||
public element!: HTMLElement;
|
||||
|
||||
Vendored
+2
-9
@@ -7,7 +7,6 @@ import { IEvent } from 'common/EventEmitter';
|
||||
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
|
||||
/**
|
||||
* A portion of the public API that are implemented identially internally and simply passed through.
|
||||
@@ -18,9 +17,9 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
|
||||
screenElement: HTMLElement | undefined;
|
||||
browser: IBrowser;
|
||||
buffer: IBuffer;
|
||||
linkifier: ILinkifier2 | undefined;
|
||||
viewport: IViewport | undefined;
|
||||
options: Required<ITerminalOptions>;
|
||||
linkifier2: ILinkifier2;
|
||||
|
||||
onBlur: IEvent<void>;
|
||||
onFocus: IEvent<void>;
|
||||
@@ -32,6 +31,7 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
|
||||
}
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
export type CustomWheelEventHandler = (event: WheelEvent) => boolean;
|
||||
|
||||
export type LineData = CharData[];
|
||||
|
||||
@@ -127,13 +127,6 @@ export interface ILinkifier2 extends IDisposable {
|
||||
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
readonly currentLink: ILinkWithState | undefined;
|
||||
|
||||
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
|
||||
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
||||
}
|
||||
|
||||
interface ILinkProvider {
|
||||
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
|
||||
}
|
||||
|
||||
interface ILink {
|
||||
|
||||
@@ -148,6 +148,9 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
|
||||
}
|
||||
public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
|
||||
this._core.attachCustomWheelEventHandler(customWheelEventHandler);
|
||||
}
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
return this._core.registerLinkProvider(linkProvider);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/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';
|
||||
import { channels, color } from 'common/Color';
|
||||
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
|
||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { WidthCache } from 'browser/renderer/dom/WidthCache';
|
||||
import { IColorContrastCache } from 'browser/Types';
|
||||
@@ -376,7 +376,7 @@ export class DomRendererRowFactory {
|
||||
classes.push(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
|
||||
resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
|
||||
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
@@ -408,7 +408,7 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
const color = rgba.toColor(
|
||||
const color = channels.toColor(
|
||||
(fg >> 16) & 0xFF,
|
||||
(fg >> 8) & 0xFF,
|
||||
(fg ) & 0xFF
|
||||
@@ -458,7 +458,7 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
|
||||
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
|
||||
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) {
|
||||
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || treatGlyphAsBackgroundColor(cell.getCode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle
|
||||
import { IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { rgba } from 'common/Color';
|
||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $fg = 0;
|
||||
@@ -65,11 +67,11 @@ export class CellColorResolver {
|
||||
// Apply decorations on the bottom layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
|
||||
$hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
|
||||
$hasFg = true;
|
||||
}
|
||||
});
|
||||
@@ -77,10 +79,94 @@ export class CellColorResolver {
|
||||
// Apply the selection color if needed
|
||||
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
|
||||
if ($isSelected) {
|
||||
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
|
||||
// If the cell has a bg color, retain the color by blending it with the selection color
|
||||
if (
|
||||
(this.result.fg & FgFlags.INVERSE) ||
|
||||
(this.result.bg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT
|
||||
) {
|
||||
// Resolve the standard bg color
|
||||
if (this.result.fg & FgFlags.INVERSE) {
|
||||
switch (this.result.fg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
$bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
$bg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
$bg = this._themeService.colors.foreground.rgba;
|
||||
}
|
||||
} else {
|
||||
switch (this.result.bg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
$bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
$bg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF;
|
||||
break;
|
||||
// No need to consider default bg color here as it's not possible
|
||||
}
|
||||
}
|
||||
// Blend with selection bg color
|
||||
$bg = rgba.blend(
|
||||
$bg,
|
||||
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
|
||||
) >> 8 & Attributes.RGB_MASK;
|
||||
} else {
|
||||
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
|
||||
}
|
||||
$hasBg = true;
|
||||
|
||||
// Apply explicit selection foreground if present
|
||||
if ($colors.selectionForeground) {
|
||||
$fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
$fg = $colors.selectionForeground.rgba >> 8 & Attributes.RGB_MASK;
|
||||
$hasFg = true;
|
||||
}
|
||||
|
||||
// Overwrite fg as bg if it's a special decorative glyph (eg. powerline)
|
||||
if (treatGlyphAsBackgroundColor(cell.getCode())) {
|
||||
// Inverse default background should be treated as transparent
|
||||
if (
|
||||
(this.result.fg & FgFlags.INVERSE) &&
|
||||
(this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT
|
||||
) {
|
||||
$fg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
|
||||
} else {
|
||||
|
||||
if (this.result.fg & FgFlags.INVERSE) {
|
||||
switch (this.result.bg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
$fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
$fg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF;
|
||||
break;
|
||||
// No need to consider default bg color here as it's not possible
|
||||
}
|
||||
} else {
|
||||
switch (this.result.fg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
$fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
$fg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
$fg = this._themeService.colors.foreground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
$fg = rgba.blend(
|
||||
$fg,
|
||||
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
|
||||
) >> 8 & Attributes.RGB_MASK;
|
||||
}
|
||||
$hasFg = true;
|
||||
}
|
||||
}
|
||||
@@ -88,11 +174,11 @@ export class CellColorResolver {
|
||||
// Apply decorations on the top layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
|
||||
$hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
|
||||
$hasFg = true;
|
||||
}
|
||||
});
|
||||
@@ -119,7 +205,7 @@ export class CellColorResolver {
|
||||
if ($hasBg && !$hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
@@ -128,7 +214,7 @@ export class CellColorResolver {
|
||||
if (!$hasBg && $hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ function isBoxOrBlockGlyph(codepoint: number): boolean {
|
||||
return 0x2500 <= codepoint && codepoint <= 0x259F;
|
||||
}
|
||||
|
||||
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
|
||||
export function treatGlyphAsBackgroundColor(codepoint: number): boolean {
|
||||
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,16 +6,15 @@
|
||||
import { IColorContrastCache } from 'browser/Types';
|
||||
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
|
||||
import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { NULL_COLOR, color, rgba } from 'common/Color';
|
||||
import { NULL_COLOR, channels, color, rgba } from 'common/Color';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { FourKeyMap } from 'common/MultiKeyMap';
|
||||
import { IdleTaskQueue } from 'common/TaskQueue';
|
||||
import { IColor } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { traceCall } from 'common/services/LogService';
|
||||
import { IUnicodeService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
@@ -292,8 +291,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(bgColor);
|
||||
// TODO: This object creation is slow
|
||||
result = rgba.toColor(arr[0], arr[1], arr[2]);
|
||||
result = channels.toColor(arr[0], arr[1], arr[2]);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
@@ -325,7 +323,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(fgColor);
|
||||
result = rgba.toColor(arr[0], arr[1], arr[2]);
|
||||
result = channels.toColor(arr[0], arr[1], arr[2]);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
@@ -407,7 +405,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const color = rgba.toColor(
|
||||
const color = channels.toColor(
|
||||
(result >> 24) & 0xFF,
|
||||
(result >> 16) & 0xFF,
|
||||
(result >> 8) & 0xFF
|
||||
@@ -424,7 +422,6 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
return this._config.colors.contrastCache;
|
||||
}
|
||||
|
||||
@traceCall
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph {
|
||||
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
|
||||
|
||||
@@ -492,7 +489,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
|
||||
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
|
||||
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
|
||||
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, treatGlyphAsBackgroundColor(chars.charCodeAt(0)));
|
||||
this._tmpCtx.fillStyle = foregroundColor.css;
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
@@ -613,7 +610,14 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
nextOffset = computeNextVariantOffset(xChRight - xChLeft, lineWidth, nextOffset);
|
||||
break;
|
||||
case UnderlineStyle.DASHED:
|
||||
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]);
|
||||
const lineRatio = 0.6;
|
||||
const gapRatio = 0.3;
|
||||
// End line ratio is approximately equal to 0.1
|
||||
const xChWidth = xChRight - xChLeft;
|
||||
const line = Math.floor(lineRatio * xChWidth);
|
||||
const gap = Math.floor(gapRatio * xChWidth);
|
||||
const end = xChWidth - line - gap;
|
||||
this._tmpCtx.setLineDash([line, gap, end]);
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
this._tmpCtx.lineTo(xChRight, yTop);
|
||||
break;
|
||||
|
||||
@@ -8,12 +8,6 @@ import { EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
|
||||
|
||||
const enum MeasureSettings {
|
||||
REPEAT = 32
|
||||
}
|
||||
|
||||
|
||||
export class CharSizeService extends Disposable implements ICharSizeService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
@@ -32,7 +26,11 @@ export class CharSizeService extends Disposable implements ICharSizeService {
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
|
||||
try {
|
||||
this._measureStrategy = this.register(new TextMetricsMeasureStrategy(this._optionsService));
|
||||
} catch {
|
||||
this._measureStrategy = this.register(new DomMeasureStrategy(document, parentElement, this._optionsService));
|
||||
}
|
||||
this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
|
||||
}
|
||||
|
||||
@@ -47,12 +45,7 @@ export class CharSizeService extends Disposable implements ICharSizeService {
|
||||
}
|
||||
|
||||
interface IMeasureStrategy {
|
||||
measure(): IReadonlyMeasureResult;
|
||||
}
|
||||
|
||||
interface IReadonlyMeasureResult {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
measure(): Readonly<IMeasureResult>;
|
||||
}
|
||||
|
||||
interface IMeasureResult {
|
||||
@@ -60,10 +53,26 @@ interface IMeasureResult {
|
||||
height: number;
|
||||
}
|
||||
|
||||
// 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 };
|
||||
const enum DomMeasureStrategyConstants {
|
||||
REPEAT = 32
|
||||
}
|
||||
|
||||
abstract class BaseMeasureStategy extends Disposable implements IMeasureStrategy {
|
||||
protected _result: IMeasureResult = { width: 0, height: 0 };
|
||||
|
||||
protected _validateAndSet(width: number | undefined, height: number | undefined): void {
|
||||
// If values are 0 then the element is likely currently display:none, in which case we should
|
||||
// retain the previous value.
|
||||
if (width !== undefined && width > 0 && height !== undefined && height > 0) {
|
||||
this._result.width = width;
|
||||
this._result.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract measure(): Readonly<IMeasureResult>;
|
||||
}
|
||||
|
||||
class DomMeasureStrategy extends BaseMeasureStategy {
|
||||
private _measureElement: HTMLElement;
|
||||
|
||||
constructor(
|
||||
@@ -71,32 +80,48 @@ class DomMeasureStrategy implements IMeasureStrategy {
|
||||
private _parentElement: HTMLElement,
|
||||
private _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
this._measureElement = this._document.createElement('span');
|
||||
this._measureElement.classList.add('xterm-char-measure-element');
|
||||
this._measureElement.textContent = 'W'.repeat(MeasureSettings.REPEAT);
|
||||
this._measureElement.textContent = 'W'.repeat(DomMeasureStrategyConstants.REPEAT);
|
||||
this._measureElement.setAttribute('aria-hidden', 'true');
|
||||
this._measureElement.style.whiteSpace = 'pre';
|
||||
this._measureElement.style.fontKerning = 'none';
|
||||
this._parentElement.appendChild(this._measureElement);
|
||||
}
|
||||
|
||||
public measure(): IReadonlyMeasureResult {
|
||||
public measure(): Readonly<IMeasureResult> {
|
||||
this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily;
|
||||
this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`;
|
||||
|
||||
// Note that this triggers a synchronous layout
|
||||
const geometry = {
|
||||
height: Number(this._measureElement.offsetHeight),
|
||||
width: Number(this._measureElement.offsetWidth)
|
||||
};
|
||||
|
||||
// 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 / MeasureSettings.REPEAT;
|
||||
this._result.height = Math.ceil(geometry.height);
|
||||
}
|
||||
this._validateAndSet(Number(this._measureElement.offsetWidth) / DomMeasureStrategyConstants.REPEAT, Number(this._measureElement.offsetHeight));
|
||||
|
||||
return this._result;
|
||||
}
|
||||
}
|
||||
|
||||
class TextMetricsMeasureStrategy extends BaseMeasureStategy {
|
||||
private _canvas: OffscreenCanvas;
|
||||
private _ctx: OffscreenCanvasRenderingContext2D;
|
||||
|
||||
constructor(
|
||||
private _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
// This will throw if any required API is not supported
|
||||
this._canvas = new OffscreenCanvas(100, 100);
|
||||
this._ctx = this._canvas.getContext('2d')!;
|
||||
const a = this._ctx.measureText('W');
|
||||
if (!('width' in a && 'fontBoundingBoxAscent' in a && 'fontBoundingBoxDescent' in a)) {
|
||||
throw new Error('Required font metrics not supported');
|
||||
}
|
||||
}
|
||||
|
||||
public measure(): Readonly<IMeasureResult> {
|
||||
this._ctx.font = `${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;
|
||||
const metrics = this._ctx.measureText('W');
|
||||
this._validateAndSet(metrics.width, metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent);
|
||||
return this._result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ILinkProvider, ILinkProviderService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export class LinkProviderService extends Disposable implements ILinkProviderService {
|
||||
declare public serviceBrand: undefined;
|
||||
|
||||
public readonly linkProviders: ILinkProvider[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.register(toDisposable(() => this.linkProviders.length = 0));
|
||||
}
|
||||
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
this.linkProviders.push(linkProvider);
|
||||
return {
|
||||
dispose: () => {
|
||||
// Remove the link provider from the list
|
||||
const providerIndex = this.linkProviders.indexOf(linkProvider);
|
||||
|
||||
if (providerIndex !== -1) {
|
||||
this.linkProviders.splice(providerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import { IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable, MutableDisposable } from 'common/Lifecycle';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { DebouncedIdleTask } from 'common/TaskQueue';
|
||||
import { IBufferService, IDecorationService, IInstantiationService, IOptionsService } from 'common/services/Services';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
interface ISelectionState {
|
||||
start: [number, number] | undefined;
|
||||
@@ -24,6 +24,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
private _renderer: MutableDisposable<IRenderer> = this.register(new MutableDisposable());
|
||||
private _renderDebouncer: IRenderDebouncerWithCallback;
|
||||
private _pausedResizeTask = new DebouncedIdleTask();
|
||||
private _observerDisposable = this.register(new MutableDisposable());
|
||||
|
||||
private _isPaused: boolean = false;
|
||||
private _needsFullRefresh: boolean = false;
|
||||
@@ -38,7 +39,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
};
|
||||
|
||||
private readonly _onDimensionsChange = this.register(new EventEmitter<IRenderDimensions>());
|
||||
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
||||
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
||||
private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>());
|
||||
public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
|
||||
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
|
||||
@@ -56,12 +57,11 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
@IDecorationService decorationService: IDecorationService,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
|
||||
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), coreBrowserService);
|
||||
this.register(this._renderDebouncer);
|
||||
|
||||
this.register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
|
||||
@@ -102,12 +102,17 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
|
||||
this.register(themeService.onChangeColors(() => this._fullRefresh()));
|
||||
|
||||
this._registerIntersectionObserver(coreBrowserService.window, screenElement);
|
||||
this.register(coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
|
||||
}
|
||||
|
||||
private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void {
|
||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in coreBrowserService.window) {
|
||||
const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
if ('IntersectionObserver' in w) {
|
||||
const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
observer.observe(screenElement);
|
||||
this.register({ dispose: () => observer.disconnect() });
|
||||
this._observerDisposable.value = toDisposable(() => observer.disconnect());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { AllColorIndex, IDisposable } from 'common/Types';
|
||||
@@ -145,3 +145,14 @@ export interface IThemeService {
|
||||
*/
|
||||
modifyColors(callback: (colors: IColorSet) => void): void;
|
||||
}
|
||||
|
||||
|
||||
export const ILinkProviderService = createDecorator<ILinkProviderService>('LinkProviderService');
|
||||
export interface ILinkProviderService extends IDisposable {
|
||||
serviceBrand: undefined;
|
||||
readonly linkProviders: ReadonlyArray<ILinkProvider>;
|
||||
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
||||
}
|
||||
export interface ILinkProvider {
|
||||
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,25 @@ describe('Color', () => {
|
||||
assert.equal(channels.toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
|
||||
assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff');
|
||||
});
|
||||
it('should convert an rgba array to css hex string', () => {
|
||||
assert.equal(channels.toCss(0x00, 0x00, 0x00, 0x00), '#00000000');
|
||||
assert.equal(channels.toCss(0x10, 0x10, 0x10, 0x10), '#10101010');
|
||||
assert.equal(channels.toCss(0x20, 0x20, 0x20, 0x20), '#20202020');
|
||||
assert.equal(channels.toCss(0x30, 0x30, 0x30, 0x30), '#30303030');
|
||||
assert.equal(channels.toCss(0x40, 0x40, 0x40, 0x40), '#40404040');
|
||||
assert.equal(channels.toCss(0x50, 0x50, 0x50, 0x50), '#50505050');
|
||||
assert.equal(channels.toCss(0x60, 0x60, 0x60, 0x60), '#60606060');
|
||||
assert.equal(channels.toCss(0x70, 0x70, 0x70, 0x70), '#70707070');
|
||||
assert.equal(channels.toCss(0x80, 0x80, 0x80, 0x80), '#80808080');
|
||||
assert.equal(channels.toCss(0x90, 0x90, 0x90, 0x90), '#90909090');
|
||||
assert.equal(channels.toCss(0xa0, 0xa0, 0xa0, 0xa0), '#a0a0a0a0');
|
||||
assert.equal(channels.toCss(0xb0, 0xb0, 0xb0, 0xb0), '#b0b0b0b0');
|
||||
assert.equal(channels.toCss(0xc0, 0xc0, 0xc0, 0xc0), '#c0c0c0c0');
|
||||
assert.equal(channels.toCss(0xd0, 0xd0, 0xd0, 0xd0), '#d0d0d0d0');
|
||||
assert.equal(channels.toCss(0xe0, 0xe0, 0xe0, 0xe0), '#e0e0e0e0');
|
||||
assert.equal(channels.toCss(0xf0, 0xf0, 0xf0, 0xf0), '#f0f0f0f0');
|
||||
assert.equal(channels.toCss(0xff, 0xff, 0xff, 0xff), '#ffffffff');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toRgba', () => {
|
||||
@@ -71,6 +90,47 @@ describe('Color', () => {
|
||||
assert.equal(channels.toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toColor', () => {
|
||||
it('should convert an rgb array to an IColor', () => {
|
||||
assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00), { css: '#000000', rgba: 0x000000FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10), { css: '#101010', rgba: 0x101010FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20), { css: '#202020', rgba: 0x202020FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30), { css: '#303030', rgba: 0x303030FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40), { css: '#404040', rgba: 0x404040FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50), { css: '#505050', rgba: 0x505050FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60), { css: '#606060', rgba: 0x606060FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70), { css: '#707070', rgba: 0x707070FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80), { css: '#808080', rgba: 0x808080FF });
|
||||
assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90), { css: '#909090', rgba: 0x909090FF });
|
||||
assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
|
||||
assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
|
||||
assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
|
||||
assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
|
||||
assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
|
||||
assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
|
||||
assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff), { css: '#ffffff', rgba: 0xffffffFF });
|
||||
});
|
||||
it('should convert an rgba array to an IColor', () => {
|
||||
assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00, 0x00), { css: '#00000000', rgba: 0x00000000 });
|
||||
assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10, 0x10), { css: '#10101010', rgba: 0x10101010 });
|
||||
assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20, 0x20), { css: '#20202020', rgba: 0x20202020 });
|
||||
assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30, 0x30), { css: '#30303030', rgba: 0x30303030 });
|
||||
assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40, 0x40), { css: '#40404040', rgba: 0x40404040 });
|
||||
assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50, 0x50), { css: '#50505050', rgba: 0x50505050 });
|
||||
assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60, 0x60), { css: '#60606060', rgba: 0x60606060 });
|
||||
assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70, 0x70), { css: '#70707070', rgba: 0x70707070 });
|
||||
assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80, 0x80), { css: '#80808080', rgba: 0x80808080 });
|
||||
assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90, 0x90), { css: '#90909090', rgba: 0x90909090 });
|
||||
assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0, 0xa0), { css: '#a0a0a0a0', rgba: 0xa0a0a0a0 });
|
||||
assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0, 0xb0), { css: '#b0b0b0b0', rgba: 0xb0b0b0b0 });
|
||||
assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0, 0xc0), { css: '#c0c0c0c0', rgba: 0xc0c0c0c0 });
|
||||
assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0, 0xd0), { css: '#d0d0d0d0', rgba: 0xd0d0d0d0 });
|
||||
assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0, 0xe0), { css: '#e0e0e0e0', rgba: 0xe0e0e0e0 });
|
||||
assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0, 0xf0), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 });
|
||||
assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff, 0xff), { css: '#ffffffff', rgba: 0xffffffff });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('color', () => {
|
||||
@@ -271,6 +331,27 @@ describe('Color', () => {
|
||||
});
|
||||
|
||||
describe('rgba', () => {
|
||||
describe('blend', () => {
|
||||
it('should blend colors based on the alpha channel', () => {
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF00), 0x000000FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF10), 0x101010FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF20), 0x202020FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF30), 0x303030FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF40), 0x404040FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF50), 0x505050FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF60), 0x606060FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF70), 0x707070FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF80), 0x808080FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF90), 0x909090FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFA0), 0xA0A0A0FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFB0), 0xB0B0B0FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFC0), 0xC0C0C0FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFD0), 0xD0D0D0FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFE0), 0xE0E0E0FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFF0), 0xF0F0F0FF);
|
||||
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFFF), 0xFFFFFFFF);
|
||||
});
|
||||
});
|
||||
describe('ensureContrastRatio', () => {
|
||||
it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
|
||||
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined);
|
||||
|
||||
+28
-12
@@ -33,6 +33,13 @@ export namespace channels {
|
||||
// >>> 0 forces an unsigned int
|
||||
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
|
||||
}
|
||||
|
||||
export function toColor(r: number, g: number, b: number, a?: number): IColor {
|
||||
return {
|
||||
css: channels.toCss(r, g, b, a),
|
||||
rgba: channels.toRgba(r, g, b, a)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +77,7 @@ export namespace color {
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
return rgba.toColor(
|
||||
return channels.toColor(
|
||||
(result >> 24 & 0xFF),
|
||||
(result >> 16 & 0xFF),
|
||||
(result >> 8 & 0xFF)
|
||||
@@ -142,14 +149,14 @@ export namespace css {
|
||||
$r = parseInt(css.slice(1, 2).repeat(2), 16);
|
||||
$g = parseInt(css.slice(2, 3).repeat(2), 16);
|
||||
$b = parseInt(css.slice(3, 4).repeat(2), 16);
|
||||
return rgba.toColor($r, $g, $b);
|
||||
return channels.toColor($r, $g, $b);
|
||||
}
|
||||
case 5: { // #rgba
|
||||
$r = parseInt(css.slice(1, 2).repeat(2), 16);
|
||||
$g = parseInt(css.slice(2, 3).repeat(2), 16);
|
||||
$b = parseInt(css.slice(3, 4).repeat(2), 16);
|
||||
$a = parseInt(css.slice(4, 5).repeat(2), 16);
|
||||
return rgba.toColor($r, $g, $b, $a);
|
||||
return channels.toColor($r, $g, $b, $a);
|
||||
}
|
||||
case 7: // #rrggbb
|
||||
return {
|
||||
@@ -171,7 +178,7 @@ export namespace css {
|
||||
$g = parseInt(rgbaMatch[2]);
|
||||
$b = parseInt(rgbaMatch[3]);
|
||||
$a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
|
||||
return rgba.toColor($r, $g, $b, $a);
|
||||
return channels.toColor($r, $g, $b, $a);
|
||||
}
|
||||
|
||||
// Validate the context is available for canvas-based color parsing
|
||||
@@ -245,6 +252,23 @@ export namespace rgb {
|
||||
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
|
||||
*/
|
||||
export namespace rgba {
|
||||
export function blend(bg: number, fg: number): number {
|
||||
$a = (fg & 0xFF) / 0xFF;
|
||||
if ($a === 1) {
|
||||
return fg;
|
||||
}
|
||||
const fgR = (fg >> 24) & 0xFF;
|
||||
const fgG = (fg >> 16) & 0xFF;
|
||||
const fgB = (fg >> 8) & 0xFF;
|
||||
const bgR = (bg >> 24) & 0xFF;
|
||||
const bgG = (bg >> 16) & 0xFF;
|
||||
const bgB = (bg >> 8) & 0xFF;
|
||||
$r = bgR + Math.round((fgR - bgR) * $a);
|
||||
$g = bgG + Math.round((fgG - bgG) * $a);
|
||||
$b = bgB + Math.round((fgB - bgB) * $a);
|
||||
return channels.toRgba($r, $g, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a foreground color and a background color, either increase or reduce the luminance of the
|
||||
* foreground color until the specified contrast ratio is met. If pure white or black is hit
|
||||
@@ -325,17 +349,9 @@ export namespace rgba {
|
||||
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
||||
}
|
||||
|
||||
// FIXME: Move this to channels NS?
|
||||
export function toChannels(value: number): [number, number, number, number] {
|
||||
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
|
||||
}
|
||||
|
||||
export function toColor(r: number, g: number, b: number, a?: number): IColor {
|
||||
return {
|
||||
css: channels.toCss(r, g, b, a),
|
||||
rgba: channels.toRgba(r, g, b, a)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function toPaddedHex(c: number): string {
|
||||
|
||||
@@ -120,6 +120,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this._oscLinkService = this._instantiationService.createInstance(OscLinkService);
|
||||
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
|
||||
|
||||
|
||||
// Register input handler and handle/forward events
|
||||
this._inputHandler = this.register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService));
|
||||
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
|
||||
|
||||
Vendored
+2
-2
@@ -110,8 +110,8 @@ export interface ICharset {
|
||||
export type CharData = [number, string, number, number];
|
||||
|
||||
export interface IColor {
|
||||
css: string;
|
||||
rgba: number; // 32-bit int with rgba in each byte
|
||||
readonly css: string;
|
||||
readonly rgba: number; // 32-bit int with rgba in each byte
|
||||
}
|
||||
export type IColorRGB = [number, number, number];
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { CursorStyle, IDisposable } from 'common/Types';
|
||||
import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services';
|
||||
@@ -86,6 +86,13 @@ export class OptionsService extends Disposable implements IOptionsService {
|
||||
this.rawOptions = defaultOptions;
|
||||
this.options = { ... defaultOptions };
|
||||
this._setupOptions();
|
||||
|
||||
// Clear out options that could link outside xterm.js as they could easily cause an embedder
|
||||
// memory leak
|
||||
this.register(toDisposable(() => {
|
||||
this.rawOptions.linkHandler = null;
|
||||
this.rawOptions.documentOverride = null;
|
||||
}));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
|
||||
@@ -8,7 +8,10 @@ import { ITestContext, createTestContext, openTerminal } from './TestUtils';
|
||||
import { ISharedRendererTestContext, injectSharedRendererTestsStandalone, injectSharedRendererTests } from './SharedRendererTests';
|
||||
|
||||
let ctx: ITestContext;
|
||||
const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any;
|
||||
const ctxWrapper: ISharedRendererTestContext = {
|
||||
value: undefined,
|
||||
skipDomExceptions: true
|
||||
} as any;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
ctxWrapper.value = ctx;
|
||||
@@ -18,5 +21,5 @@ test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.describe('DOM Renderer Integration Tests', () => {
|
||||
injectSharedRendererTests(ctxWrapper);
|
||||
injectSharedRendererTestsStandalone(ctxWrapper);
|
||||
injectSharedRendererTestsStandalone(ctxWrapper, () => {});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate } f
|
||||
export interface ISharedRendererTestContext {
|
||||
value: ITestContext;
|
||||
skipCanvasExceptions?: boolean;
|
||||
skipDomExceptions?: boolean;
|
||||
}
|
||||
|
||||
export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void {
|
||||
@@ -945,7 +946,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 255, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 0, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 255, 0, 255]);
|
||||
await ctx.value.page.evaluate(`window.term.selectAll()`);
|
||||
await ctx.value.proxy.selectAll();
|
||||
frameDetails = undefined;
|
||||
// Selection only cell needs to be first to ensure renderer has kicked in
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]);
|
||||
@@ -965,7 +966,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
// Check both the cursor line and another line
|
||||
await ctx.value.proxy.writeln('_ ');
|
||||
await ctx.value.proxy.write('_ ');
|
||||
await ctx.value.page.evaluate(`window.term.selectAll()`);
|
||||
await ctx.value.proxy.selectAll();
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [128, 0, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [128, 0, 0, 255]);
|
||||
@@ -980,6 +981,44 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
});
|
||||
});
|
||||
|
||||
(ctx.skipCanvasExceptions || ctx.skipDomExceptions ? test.describe.skip : test.describe)('selection blending', () => {
|
||||
test('background', async () => {
|
||||
const theme: ITheme = {
|
||||
red: '#CC0000',
|
||||
selectionBackground: '#FFFFFF'
|
||||
};
|
||||
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await ctx.value.proxy.focus();
|
||||
await ctx.value.proxy.writeln('\x1b[41m red bg');
|
||||
await ctx.value.proxy.writeln('\x1b[7m inverse');
|
||||
await ctx.value.proxy.writeln('\x1b[31;7m red fg inverse');
|
||||
await ctx.value.proxy.selectAll();
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [230,128,128,255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [255,255,255,255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [230,128,128,255]);
|
||||
});
|
||||
test('powerline decorative symbols', async () => {
|
||||
const theme: ITheme = {
|
||||
red: '#CC0000',
|
||||
green: '#00CC00',
|
||||
selectionBackground: '#FFFFFF'
|
||||
};
|
||||
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await ctx.value.proxy.focus();
|
||||
await ctx.value.proxy.writeln('\u{E0B4} plain\x1b[0m');
|
||||
await ctx.value.proxy.writeln('\x1b[31;42m\u{E0B4} red fg green bg\x1b[0m');
|
||||
await ctx.value.proxy.writeln('\x1b[32;41m\u{E0B4} green fg red bg\x1b[0m');
|
||||
await ctx.value.proxy.writeln('\x1b[31;42;7m\u{E0B4} red fg green bg inverse\x1b[0m');
|
||||
await ctx.value.proxy.writeln('\x1b[32;41;7m\u{E0B4} green fg red bg inverse\x1b[0m');
|
||||
await ctx.value.proxy.selectAll();
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255,255,255,255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [230, 128, 128, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [128, 230, 128, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 4), [128, 230, 128, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 5), [230, 128, 128, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('allowTransparency', async () => {
|
||||
test.beforeEach(() => ctx.value.page.evaluate(`term.options.allowTransparency = true`));
|
||||
|
||||
@@ -1003,7 +1042,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\x1b[7m■\x1b[0m`;
|
||||
await ctx.value.proxy.write( data);
|
||||
await ctx.value.page.evaluate(`window.term.selectAll()`);
|
||||
await ctx.value.proxy.selectAll();
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
@@ -1092,7 +1131,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
});
|
||||
|
||||
test.describe('regression tests', () => {
|
||||
test('#4736: inactive selection background should replace regular cell background color', async () => {
|
||||
(ctx.skipCanvasExceptions ? test.skip : test)('#4736: inactive selection background should replace regular cell background color', async () => {
|
||||
const theme: ITheme = {
|
||||
selectionBackground: '#FF0000',
|
||||
selectionInactiveBackground: '#0000FF'
|
||||
@@ -1126,7 +1165,8 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 0, 255]);
|
||||
});
|
||||
test('#4759: minimum contrast ratio should be respected on inverse text', async () => {
|
||||
// HACK: It's not clear why DOM is failing here
|
||||
(ctx.skipDomExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on inverse text', async () => {
|
||||
const theme: ITheme = {
|
||||
foreground: '#aaaaaa',
|
||||
background: '#333333'
|
||||
@@ -1205,31 +1245,34 @@ enum CellColorPosition {
|
||||
* This is much slower than just calling `Terminal.reset` but testing some features needs this
|
||||
* treatment.
|
||||
*/
|
||||
export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext): void {
|
||||
test.beforeEach(async () => {
|
||||
// Recreate terminal
|
||||
await openTerminal(ctx.value);
|
||||
ctx.value.page.evaluate(`
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
window.term.options.allowTransparency = false;
|
||||
window.term.options.theme = undefined;
|
||||
`);
|
||||
// Clear the cached screenshot before each test
|
||||
frameDetails = undefined;
|
||||
});
|
||||
test.describe('regression tests', () => {
|
||||
test('#4790: cursor should not be displayed before focusing', async () => {
|
||||
const theme: ITheme = {
|
||||
cursor: '#0000FF'
|
||||
};
|
||||
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
|
||||
await ctx.value.proxy.focus();
|
||||
export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext, setupCb: () => Promise<void> | void): void {
|
||||
test.describe('standalone tests', () => {
|
||||
test.beforeEach(async () => {
|
||||
// Recreate terminal
|
||||
await openTerminal(ctx.value);
|
||||
await ctx.value.page.evaluate(`
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
window.term.options.allowTransparency = false;
|
||||
window.term.options.theme = undefined;
|
||||
`);
|
||||
await setupCb();
|
||||
// Clear the cached screenshot before each test
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]);
|
||||
await ctx.value.proxy.blur();
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
|
||||
});
|
||||
test.describe('regression tests', () => {
|
||||
test('#4790: cursor should not be displayed before focusing', async () => {
|
||||
const theme: ITheme = {
|
||||
cursor: '#0000FF'
|
||||
};
|
||||
await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
|
||||
await ctx.value.proxy.focus();
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]);
|
||||
await ctx.value.proxy.blur();
|
||||
frameDetails = undefined;
|
||||
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ type TerminalProxyCustomOverrides = 'buffer' | (
|
||||
'options' |
|
||||
'open' |
|
||||
'attachCustomKeyEventHandler' |
|
||||
'attachCustomWheelEventHandler' |
|
||||
'registerLinkProvider' |
|
||||
'registerCharacterJoiner' |
|
||||
'deregisterCharacterJoiner' |
|
||||
|
||||
Vendored
+22
@@ -1010,6 +1010,28 @@ declare module '@xterm/xterm' {
|
||||
*/
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
|
||||
/**
|
||||
* Attaches a custom wheel event handler which is run before keys are
|
||||
* processed, giving consumers of xterm.js control over whether to proceed
|
||||
* or cancel terminal wheel events.
|
||||
* @param customWheelEventHandler The custom WheelEvent handler to attach.
|
||||
* This is a function that takes a WheelEvent, allowing consumers to stop
|
||||
* propagation and/or prevent the default action. The function returns
|
||||
* whether the event should be processed by xterm.js.
|
||||
*
|
||||
* @example A handler that prevents all wheel events while ctrl is held from
|
||||
* being processed.
|
||||
* ```ts
|
||||
* term.attachCustomWheelEventHandler(ev => {
|
||||
* if (ev.ctrlKey) {
|
||||
* return false;
|
||||
* }
|
||||
* return true;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a link provider, allowing a custom parser to be used to match
|
||||
* and handle links. Multiple link providers can be used, they will be asked
|
||||
|
||||
Reference in New Issue
Block a user