diff --git a/addons/xterm-addon-ligatures/package.json b/addons/xterm-addon-ligatures/package.json index f3c3aeff..7e5e5e90 100644 --- a/addons/xterm-addon-ligatures/package.json +++ b/addons/xterm-addon-ligatures/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-ligatures", - "version": "0.5.2", + "version": "0.5.3", "description": "Add support for programming ligatures to xterm.js", "author": { "name": "The xterm.js authors", diff --git a/addons/xterm-addon-ligatures/src/index.test.ts b/addons/xterm-addon-ligatures/src/index.test.ts index 838955c0..f9f10d0b 100644 --- a/addons/xterm-addon-ligatures/src/index.test.ts +++ b/addons/xterm-addon-ligatures/src/index.test.ts @@ -78,7 +78,7 @@ describe('xterm-addon-ligatures', () => { }); it('handles quoted font names', done => { - term.setOption('fontFamily', '"Fira Code", monospace'); + term.options.fontFamily = '"Fira Code", monospace'; assert.deepEqual(term.joiner!(input), []); onRefresh.callsFake(() => { assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]); @@ -87,7 +87,7 @@ describe('xterm-addon-ligatures', () => { }); it('falls back to later fonts if earlier ones are not present', done => { - term.setOption('fontFamily', 'notinstalled, Fira Code, monospace'); + term.options.fontFamily = 'notinstalled, Fira Code, monospace'; assert.deepEqual(term.joiner!(input), []); onRefresh.callsFake(() => { assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]); @@ -98,17 +98,17 @@ describe('xterm-addon-ligatures', () => { it('uses the current font value', done => { // The first three calls are all synchronous so that we don't allow time for // any fonts to load while we're switching things around - term.setOption('fontFamily', 'Fira Code'); + term.options.fontFamily = 'Fira Code'; assert.deepEqual(term.joiner!(input), []); - term.setOption('fontFamily', 'notinstalled'); + term.options.fontFamily = 'notinstalled'; assert.deepEqual(term.joiner!(input), []); - term.setOption('fontFamily', 'Iosevka'); + term.options.fontFamily = 'Iosevka'; assert.deepEqual(term.joiner!(input), []); onRefresh.callsFake(() => { assert.deepEqual(term.joiner!(input), [[2, 4]]); // And switch it back to Fira Code for good measure - term.setOption('fontFamily', 'Fira Code'); + term.options.fontFamily = 'Fira Code'; // At this point, we haven't loaded the new font, so the result reverts // back to empty until that happens @@ -124,7 +124,7 @@ describe('xterm-addon-ligatures', () => { it('allows multiple terminal instances that use different fonts', done => { const onRefresh2 = sinon.stub(); const term2 = new MockTerminal(onRefresh2); - term2.setOption('fontFamily', 'Iosevka'); + term2.options.fontFamily = 'Iosevka'; ligatureSupport.enableLigatures(term2 as any); assert.deepEqual(term.joiner!(input), []); @@ -140,7 +140,7 @@ describe('xterm-addon-ligatures', () => { }); it('fails if it finds but cannot load the font', async () => { - term.setOption('fontFamily', 'Nonexistant Font, monospace'); + term.options.fontFamily = 'Nonexistant Font, monospace'; assert.deepEqual(term.joiner!(input), []); await delay(500); assert.isTrue(onRefresh.notCalled); @@ -148,7 +148,7 @@ describe('xterm-addon-ligatures', () => { }); it('returns nothing if the font is not present on the system', async () => { - term.setOption('fontFamily', 'notinstalled'); + term.options.fontFamily = 'notinstalled'; assert.deepEqual(term.joiner!(input), []); await delay(500); assert.isTrue(onRefresh.notCalled); @@ -156,7 +156,7 @@ describe('xterm-addon-ligatures', () => { }); it('returns nothing if no specific font is specified', async () => { - term.setOption('fontFamily', 'monospace'); + term.options.fontFamily = 'monospace'; assert.deepEqual(term.joiner!(input), []); await delay(500); assert.isTrue(onRefresh.notCalled); @@ -164,7 +164,7 @@ describe('xterm-addon-ligatures', () => { }); it('returns nothing if no fonts are provided', async () => { - term.setOption('fontFamily', ''); + term.options.fontFamily = ''; assert.deepEqual(term.joiner!(input), []); await delay(500); assert.isTrue(onRefresh.notCalled); @@ -172,7 +172,7 @@ describe('xterm-addon-ligatures', () => { }); it('fails when given malformed inputs', async () => { - term.setOption('fontFamily', {} as any); + term.options.fontFamily = {} as any; assert.deepEqual(term.joiner!(input), []); await delay(500); assert.isTrue(onRefresh.notCalled); @@ -181,7 +181,7 @@ describe('xterm-addon-ligatures', () => { it('ensures no empty errors are thrown', async () => { sinon.stub(fontLigatures, 'loadFile').callsFake(async () => { throw undefined; }); - term.setOption('fontFamily', 'Iosevka'); + term.options.fontFamily = 'Iosevka'; assert.deepEqual(term.joiner!(input), []); await delay(500); assert.isTrue(onRefresh.notCalled); @@ -209,11 +209,11 @@ class MockTerminal { public deregisterCharacterJoiner(id: number): void { this.joiner = undefined; } - public setOption(name: string, value: string | number): void { - this._options[name] = value; - } - public getOption(name: string): string | number { - return this._options[name]; + public get options(): { [name: string]: string | number } { return this._options; } + public set options(options: { [name: string]: string | number }) { + for (const key in this._options) { + this._options[key] = options[key]; + } } } diff --git a/addons/xterm-addon-ligatures/src/index.ts b/addons/xterm-addon-ligatures/src/index.ts index c867369d..c54f3b50 100644 --- a/addons/xterm-addon-ligatures/src/index.ts +++ b/addons/xterm-addon-ligatures/src/index.ts @@ -34,7 +34,7 @@ export function enableLigatures(term: Terminal): void { term.registerCharacterJoiner((text: string): [number, number][] => { // If the font hasn't been loaded yet, load it and return an empty result - const termFont = term.getOption('fontFamily'); + const termFont = term.options.fontFamily; if ( termFont && (loadingState === LoadingState.UNLOADED || currentFontName !== termFont) @@ -48,20 +48,20 @@ export function enableLigatures(term: Terminal): void { .then(f => { // Another request may have come in while we were waiting, so make // sure our font is still vaild. - if (currentCallFontName === term.getOption('fontFamily')) { + if (currentCallFontName === term.options.fontFamily) { loadingState = LoadingState.LOADED; font = f; // Only refresh things if we actually found a font if (f) { - term.refresh(0, term.getOption('rows') - 1); + term.refresh(0, term.options.rows! - 1); } } }) .catch(e => { // Another request may have come in while we were waiting, so make // sure our font is still vaild. - if (currentCallFontName === term.getOption('fontFamily')) { + if (currentCallFontName === term.options.fontFamily) { loadingState = LoadingState.FAILED; font = undefined; loadError = e; diff --git a/addons/xterm-addon-ligatures/yarn.lock b/addons/xterm-addon-ligatures/yarn.lock index 64117742..6ba3eccb 100644 --- a/addons/xterm-addon-ligatures/yarn.lock +++ b/addons/xterm-addon-ligatures/yarn.lock @@ -141,9 +141,9 @@ lru-cache@^6.0.0: yallist "^4.0.0" minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mkdirp@0.5.5: version "0.5.5" diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 4651f6c7..e7ece483 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -3,13 +3,25 @@ * @license MIT */ -import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm'; +import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; +import { EventEmitter } from 'common/EventEmitter'; export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; incremental?: boolean; + decorations?: ISearchDecorationOptions; + noScroll?: boolean; +} + +interface ISearchDecorationOptions { + matchBackground?: string; + matchBorder?: string; + matchOverviewRuler: string; + activeMatchBackground?: string; + activeMatchBorder?: string; + activeMatchColorOverviewRuler: string; } export interface ISearchPosition { @@ -40,7 +52,14 @@ const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs export class SearchAddon implements ITerminalAddon { private _terminal: Terminal | undefined; - + private _cachedSearchTerm: string | undefined; + private _selectedDecoration: IDecoration | undefined; + private _resultDecorations: Map | undefined; + private _searchResults: Map | undefined; + private _onDataDisposable: IDisposable | undefined; + private _onResizeDisposable: IDisposable | undefined; + private _lastSearchOptions: ISearchOptions | undefined; + private _highlightTimeout: number | undefined; /** * translateBufferLineToStringWithWrap is a fairly expensive call. * We memoize the calls into an array that has a time based ttl. @@ -51,11 +70,55 @@ export class SearchAddon implements ITerminalAddon { private _cursorMoveListener: IDisposable | undefined; private _resizeListener: IDisposable | undefined; + private _resultIndex: number | undefined; + + private readonly _onDidChangeResults = new EventEmitter<{resultIndex: number, resultCount: number} | undefined>(); + public readonly onDidChangeResults = this._onDidChangeResults.event; + public activate(terminal: Terminal): void { this._terminal = terminal; + this._onDataDisposable = this._terminal.onData(() => this._updateMatches()); + this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches()); } - public dispose(): void { } + private _updateMatches(): void { + if (this._highlightTimeout) { + window.clearTimeout(this._highlightTimeout); + } + if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) { + this._highlightTimeout = setTimeout(() => { + this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true }); + this._onDidChangeResults.fire({ resultIndex: this._searchResults ? this._searchResults.size - 1 : -1, resultCount: this._searchResults ? this._searchResults.size : -1 }); + }, 200); + } + } + + public dispose(): void { + this.clearDecorations(); + this._onDataDisposable?.dispose(); + this._onResizeDisposable?.dispose(); + } + + public clearDecorations(retainCachedSearchTerm?: boolean): void { + this._selectedDecoration?.dispose(); + this._searchResults?.clear(); + this._resultDecorations?.forEach(decorations => { + for (const d of decorations) { + d.dispose(); + } + }); + this._resultDecorations?.clear(); + this._searchResults = undefined; + this._resultDecorations = undefined; + if (!retainCachedSearchTerm) { + this._cachedSearchTerm = undefined; + } + } + + public clearActiveDecoration(): void { + this._selectedDecoration?.dispose(); + this._selectedDecoration = undefined; + } /** * Find the next instance of the term, then scroll to and select it. If it @@ -68,12 +131,107 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } + this._lastSearchOptions = searchOptions; + if (searchOptions?.decorations) { + if (this._resultIndex !== undefined || this._cachedSearchTerm && term !== this._cachedSearchTerm) { + this._highlightAllMatches(term, searchOptions); + } + } + return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions); + } + private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void { + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } if (!term || term.length === 0) { - this._terminal.clearSelection(); + this.clearDecorations(); + return; + } + searchOptions = searchOptions || {}; + + // new search, clear out the old decorations + this.clearDecorations(true); + this._searchResults = new Map(); + this._resultDecorations = new Map(); + const resultDecorations = this._resultDecorations; + let result = this._find(term, 0, 0, searchOptions); + while (result && !this._searchResults.get(`${result.row}-${result.col}`)) { + this._searchResults.set(`${result.row}-${result.col}`, result); + result = this._find( + term, + result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row, + result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1, + searchOptions + ); + if (this._searchResults.size > 1000) { + this.clearDecorations(); + this._resultIndex = undefined; + return; + } + } + this._searchResults.forEach(result => { + const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!); + if (resultDecoration) { + const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || []; + decorationsForLine.push(resultDecoration); + resultDecorations.set(resultDecoration.marker.line, decorationsForLine); + } + }); + } + + private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined { + if (!this._terminal || !term || term.length === 0) { + this._terminal?.clearSelection(); + this.clearDecorations(); + return undefined; + } + if (startCol > this._terminal.cols) { + throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`); + } + + let result: ISearchResult | undefined = undefined; + + this._initLinesCache(); + + const searchPosition: ISearchPosition = { + startRow, + startCol + }; + + // Search startRow + result = this._findInLine(term, searchPosition, searchOptions); + // Search from startRow + 1 to end + if (!result) { + + for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) { + searchPosition.startRow = y; + searchPosition.startCol = 0; + // If the current line is wrapped line, increase index of column to ignore the previous scan + // Otherwise, reset beginning column index to zero with set new unwrapped line index + result = this._findInLine(term, searchPosition, searchOptions); + if (result) { + break; + } + } + } + return result; + } + + private _findNextAndSelect(term: string, searchOptions?: ISearchOptions): boolean { + if (!this._terminal || !term || term.length === 0) { + this._terminal?.clearSelection(); + this.clearDecorations(); + this._cachedSearchTerm = undefined; + this._resultIndex = -1; return false; } + if (this._cachedSearchTerm !== term) { + this._resultIndex = undefined; + this._terminal.clearSelection(); + } + let startCol = 0; let startRow = 0; let currentSelection: ISelectionPosition | undefined; @@ -95,7 +253,6 @@ export class SearchAddon implements ITerminalAddon { // Search startRow let result = this._findInLine(term, searchPosition, searchOptions); - // Search from startRow + 1 to end if (!result) { @@ -129,10 +286,19 @@ export class SearchAddon implements ITerminalAddon { result = this._findInLine(term, searchPosition, searchOptions); } + if (this._searchResults) { + if (this._resultIndex === undefined) { + this._resultIndex = 0; + } else { + this._resultIndex++; + if (this._resultIndex >= this._searchResults.size) { + this._resultIndex = 0; + } + } + } // Set selection and scroll if a result was found - return this._selectResult(result); + return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll); } - /** * Find the previous instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. @@ -144,16 +310,49 @@ export class SearchAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } + this._lastSearchOptions = searchOptions; + if (searchOptions?.decorations && (this._resultIndex !== undefined || term !== this._cachedSearchTerm)) { + this._highlightAllMatches(term, searchOptions); + } + return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions); + } - if (!term || term.length === 0) { - this._terminal.clearSelection(); + private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean { + if (searchOptions?.decorations) { + if (found && this._resultIndex !== undefined && this._searchResults?.size) { + this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size }); + } else if (this._resultIndex === -1) { + this._onDidChangeResults.fire({ resultIndex: -1, resultCount: -1 }); + } else { + this._onDidChangeResults.fire(undefined); + } + } + this._cachedSearchTerm = term; + return found; + } + + private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean { + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + let result: ISearchResult | undefined; + if (!this._terminal || !term || term.length === 0) { + result = undefined; + this._terminal?.clearSelection(); + this.clearDecorations(); + this._resultIndex = -1; return false; } - const isReverseSearch = true; + if (this._cachedSearchTerm !== term) { + this._resultIndex = undefined; + this._terminal.clearSelection(); + } + let startRow = this._terminal.buffer.active.baseY + this._terminal.rows; let startCol = this._terminal.cols; - let result: ISearchResult | undefined; + const isReverseSearch = true; + const incremental = searchOptions ? searchOptions.incremental : false; let currentSelection: ISelectionPosition | undefined; if (this._terminal.hasSelection()) { @@ -207,11 +406,22 @@ export class SearchAddon implements ITerminalAddon { } } + if (this._searchResults) { + if (this._resultIndex === undefined || this._resultIndex < 0) { + this._resultIndex = this._searchResults?.size - 1; + } else { + this._resultIndex--; + if (this._resultIndex === -1) { + this._resultIndex = this._searchResults?.size - 1; + } + } + } + // If there is only one result, return true. if (!result && currentSelection) return true; // Set selection and scroll if a result was found - return this._selectResult(result); + return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll); } /** @@ -446,21 +656,88 @@ export class SearchAddon implements ITerminalAddon { /** * Selects and scrolls to a result. * @param result The result to select. - * @return Whethera result was selected. + * @return Whether a result was selected. */ - private _selectResult(result: ISearchResult | undefined): boolean { + private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean { const terminal = this._terminal!; + this.clearActiveDecoration(); if (!result) { terminal.clearSelection(); return false; } terminal.select(result.col, result.row, result.size); + if (options) { + const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); + if (marker) { + this._selectedDecoration = terminal.registerDecoration({ + marker, + x: result.col, + width: result.size, + backgroundColor: options.activeMatchBackground, + layer: 'top', + overviewRulerOptions: { + color: options.activeMatchColorOverviewRuler + } + }); + this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder)); + this._selectedDecoration?.onDispose(() => marker.dispose()); + } + } + + if (!noScroll) { // If it is not in the viewport then we scroll else it just gets selected - if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { - let scroll = result.row - terminal.buffer.active.viewportY; - scroll -= Math.floor(terminal.rows / 2); - terminal.scrollLines(scroll); + if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) { + let scroll = result.row - terminal.buffer.active.viewportY; + scroll -= Math.floor(terminal.rows / 2); + terminal.scrollLines(scroll); + } } return true; } + + /** + * Applies styles to the decoration when it is rendered + * @param element the decoration's element + * @param backgroundColor the background color to apply + * @param borderColor the border color to apply + * @returns + */ + private _applyStyles(element: HTMLElement, borderColor: string | undefined): void { + if (element.clientWidth <= 0) { + return; + } + if (!element.classList.contains('xterm-find-result-decoration')) { + element.classList.add('xterm-find-result-decoration'); + if (borderColor) { + element.style.outline = `1px solid ${borderColor}`; + } + } + } + + /** + * Creates a decoration for the result and applies styles + * @param result the search result for which to create the decoration + * @param options the options for the decoration + * @returns the {@link IDecoration} or undefined if the marker has already been disposed of + */ + private _createResultDecoration(result: ISearchResult, options: ISearchDecorationOptions): IDecoration | undefined { + const terminal = this._terminal!; + const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row); + if (!marker) { + return undefined; + } + const findResultDecoration = terminal.registerDecoration({ + marker, + x: result.col, + width: result.size, + backgroundColor: options.matchBackground, + overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : { + color: options.matchOverviewRuler, + position: 'center' + } + }); + findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder)); + findResultDecoration?.onDispose(() => marker.dispose()); + return findResultDecoration; + } } diff --git a/addons/xterm-addon-search/src/tsconfig.json b/addons/xterm-addon-search/src/tsconfig.json index d264abe0..5a5e671f 100644 --- a/addons/xterm-addon-search/src/tsconfig.json +++ b/addons/xterm-addon-search/src/tsconfig.json @@ -13,10 +13,20 @@ "strict": true, "types": [ "../../../node_modules/@types/mocha" - ] + ], + "paths": { + "common/*": [ + "../../../src/common/*" + ] + } }, "include": [ "./**/*", "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + } ] } diff --git a/addons/xterm-addon-search/test/SearchAddon.api.ts b/addons/xterm-addon-search/test/SearchAddon.api.ts index 6d75f18f..7cb15c5d 100644 --- a/addons/xterm-addon-search/test/SearchAddon.api.ts +++ b/addons/xterm-addon-search/test/SearchAddon.api.ts @@ -134,7 +134,7 @@ describe('Search Tests', function(): void { .replace(/\n/g, '\\n\\r'); } fixture = fixture - .replace(/'/g, '\\\''); + .replace(/'/g, `\\'`); }); it('should find all occurrences using findNext', async () => { await writeSync(page, fixture); diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts index f27aba78..300e5063 100644 --- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm'; +import { Terminal, ITerminalAddon, IEvent } from 'xterm'; declare module 'xterm-addon-search' { /** @@ -32,6 +32,47 @@ declare module 'xterm-addon-search' { * `findNext`, not `findPrevious`. */ incremental?: boolean; + + /** + * When set, will highlight all instances of the word on search and show + * them in the overview ruler if it's enabled. + */ + decorations?: ISearchDecorationOptions; + } + + /** + * Options for showing decorations when searching. + */ + interface ISearchDecorationOptions { + /** + * The background color of a match, this must use #RRGGBB format. + */ + matchBackground?: string; + + /** + * The border color of a match. + */ + matchBorder?: string; + + /** + * The overview ruler color of a match. + */ + matchOverviewRuler: string; + + /** + * The background color for the currently active match, this must use #RRGGBB format. + */ + activeMatchBackground?: string; + + /** + * The border color of the currently active match. + */ + activeMatchBorder?: string; + + /** + * The overview ruler color of the currently active match. + */ + activeMatchColorOverviewRuler: string; } /** @@ -64,5 +105,26 @@ declare module 'xterm-addon-search' { * @param searchOptions The options for the search. */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean; + + /** + * Clears the decorations and selection + */ + public clearDecorations(): void; + + /** + * Clears the active result decoration, this decoration is applied on top of the selection so + * removing it will reveal the selection underneath. This is intended to be called on the search + * textarea's `blur` event. + */ + public clearActiveDecoration(): void; + + /** + * When decorations are enabled, fires when + * the search results change. + * @returns -1 if there are no matches and + * @returns undefined when the threshold of 1k results + * is exceeded and decorations are disposed of. + */ + readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>; } } diff --git a/addons/xterm-addon-search/webpack.config.js b/addons/xterm-addon-search/webpack.config.js index 726dceb6..30526812 100644 --- a/addons/xterm-addon-search/webpack.config.js +++ b/addons/xterm-addon-search/webpack.config.js @@ -21,6 +21,13 @@ module.exports = { } ] }, + resolve: { + modules: ['./node_modules'], + extensions: [ '.js' ], + alias: { + common: path.resolve('../../out/common') + } + }, output: { filename: mainFile, path: path.resolve('./lib'), diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json index 1565a7e4..01a8b68f 100644 --- a/addons/xterm-addon-serialize/package.json +++ b/addons/xterm-addon-serialize/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-serialize", - "version": "0.6.1", + "version": "0.6.2", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts index caa29053..67b83884 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -43,7 +43,7 @@ class TestSelectionService { } } -describe('xterm-addon-serialize html', () => { +describe('xterm-addon-serialize', () => { let cm: ColorManager; let dom: jsdom.JSDOM; let document: Document; @@ -83,123 +83,132 @@ describe('xterm-addon-serialize html', () => { (terminal as any)._core._selectionService = selectionService; }); - it('empty terminal with selection turned off', () => { - const output = serializeAddon.serializeAsHTML(); - assert.notEqual(output, ''); - assert.equal((output.match(new RegExp('
{10}<\/div>', 'g')) || []).length, 2); - }); - - it('empty terminal with no selection', () => { - const output = serializeAddon.serializeAsHTML({ - onlySelection: true + describe('text', () => { + it('restoring cursor styles', async () => { + await writeP(terminal, sgr('32') + '> ' + sgr('0')); + assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m'); }); - assert.equal(output, ''); }); - it('basic terminal with selection', async () => { - await writeP(terminal, ' terminal '); - terminal.select(1, 0, 8); - - const output = serializeAddon.serializeAsHTML({ - onlySelection: true + describe('html', () => { + it('empty terminal with selection turned off', () => { + const output = serializeAddon.serializeAsHTML(); + assert.notEqual(output, ''); + assert.equal((output.match(/
{10}<\/span><\/div>/g) || []).length, 2); }); - assert.equal((output.match(new RegExp('
terminal<\/span><\/div>', 'g')) || []).length, 1, output); - }); - it('cells with bold styling', async () => { - await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with italic styling', async () => { - await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with inverse styling', async () => { - await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with underline styling', async () => { - await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with invisible styling', async () => { - await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with dim styling', async () => { - await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with strikethrough styling', async () => { - await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with combined styling', async () => { - await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp(' <\/span>', 'g')) || []).length, 1, output); - assert.equal((output.match(new RegExp('termi<\/span>', 'g')) || []).length, 1, output); - assert.equal((output.match(new RegExp('nal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with color styling', async () => { - await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('cells with background styling', async () => { - await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' '); - - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('terminal<\/span>', 'g')) || []).length, 1, output); - }); - - it('empty terminal with default options', async () => { - const output = serializeAddon.serializeAsHTML(); - assert.equal((output.match(new RegExp('color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output); - }); - - it('empty terminal with custom options', async () => { - terminal.options.fontFamily = 'verdana'; - terminal.options.fontSize = 20; - terminal.options.theme = { - foreground: '#ff00ff', - background: '#00ff00' - }; - const output = serializeAddon.serializeAsHTML({ - includeGlobalBackground: true + it('empty terminal with no selection', () => { + const output = serializeAddon.serializeAsHTML({ + onlySelection: true + }); + assert.equal(output, ''); }); - assert.equal((output.match(new RegExp('color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;', 'g')) || []).length, 1, output); - }); - it('empty terminal with background included', async () => { - const output = serializeAddon.serializeAsHTML({ - includeGlobalBackground: true + it('basic terminal with selection', async () => { + await writeP(terminal, ' terminal '); + terminal.select(1, 0, 8); + + const output = serializeAddon.serializeAsHTML({ + onlySelection: true + }); + assert.equal((output.match(/
terminal<\/span><\/div>/g) || []).length, 1, output); + }); + + it('cells with bold styling', async () => { + await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with italic styling', async () => { + await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with inverse styling', async () => { + await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with underline styling', async () => { + await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with invisible styling', async () => { + await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with dim styling', async () => { + await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with strikethrough styling', async () => { + await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with combined styling', async () => { + await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/ <\/span>/g) || []).length, 1, output); + assert.equal((output.match(/termi<\/span>/g) || []).length, 1, output); + assert.equal((output.match(/nal<\/span>/g) || []).length, 1, output); + }); + + it('cells with color styling', async () => { + await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('cells with background styling', async () => { + await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' '); + + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/terminal<\/span>/g) || []).length, 1, output); + }); + + it('empty terminal with default options', async () => { + const output = serializeAddon.serializeAsHTML(); + assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output); + }); + + it('empty terminal with custom options', async () => { + terminal.options.fontFamily = 'verdana'; + terminal.options.fontSize = 20; + terminal.options.theme = { + foreground: '#ff00ff', + background: '#00ff00' + }; + const output = serializeAddon.serializeAsHTML({ + includeGlobalBackground: true + }); + assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output); + }); + + it('empty terminal with background included', async () => { + const output = serializeAddon.serializeAsHTML({ + includeGlobalBackground: true + }); + assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output); }); - assert.equal((output.match(new RegExp('color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output); }); }); diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 25a42e65..ed71e4e1 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -7,6 +7,7 @@ import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm'; import { IColorSet } from 'browser/Types'; +import { IAttributeData } from 'common/Types'; function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); @@ -62,17 +63,17 @@ abstract class BaseSerializeHandler { protected _serializeString(): string { return ''; } } -function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean { +function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { return cell1.getFgColorMode() === cell2.getFgColorMode() && cell1.getFgColor() === cell2.getFgColor(); } -function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean { +function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { return cell1.getBgColorMode() === cell2.getBgColorMode() && cell1.getBgColor() === cell2.getBgColor(); } -function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { +function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { return cell1.isInverse() === cell2.isInverse() && cell1.isBold() === cell2.isBold() && cell1.isUnderline() === cell2.isUnderline() @@ -229,7 +230,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._nullCellCount = 0; } - private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] { + private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] { const sgrSeq: number[] = []; const fgChanged = !equalFg(cell, oldCell); const bgChanged = !equalBg(cell, oldCell); @@ -393,6 +394,15 @@ class StringSerializeHandler extends BaseSerializeHandler { moveRight(realCursorCol - this._lastCursorCol); } + // Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677 + // HACK: Internal API access since it's awkward to expose this in the API and serialize will + // likely be the only consumer + const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData; + const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle); + if (sgrSeq.length > 0) { + content += `\u001b[${sgrSeq.join(';')}m`; + } + return content; } } @@ -544,7 +554,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { return target; } - targetLength = targetLength - target.length; + targetLength -= target.length; if (targetLength > padString.length) { padString += padString.repeat(targetLength / padString.length); } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 334016e6..5c4ae437 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -14,7 +14,7 @@ let page: Page; const width = 800; const height = 600; -const writeRawSync = (page: any, str: string): Promise => writeSync(page, '\' +' + JSON.stringify(str) + '+ \''); +const writeRawSync = (page: any, str: string): Promise => writeSync(page, `' +` + JSON.stringify(str) + `+ '`); const testNormalScreenEqual = async (page: any, str: string): Promise => { await writeRawSync(page, str); diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index f0caf974..8e9a8408 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -5,9 +5,10 @@ import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm'; -interface ILinkProviderOptions { +export interface ILinkProviderOptions { hover?(event: MouseEvent, text: string, location: IViewportRange): void; leave?(event: MouseEvent, text: string): void; + urlRegex?: RegExp; } export class WebLinkProvider implements ILinkProvider { @@ -78,10 +79,17 @@ export class LinkComputer { endY++; } + let startX = stringIndex + 1; + let startY = startLineIndex + 1; + while (startX > terminal.cols) { + startX -= terminal.cols; + startY++; + } + const range = { start: { - x: stringIndex + 1, - y: startLineIndex + 1 + x: startX, + y: startY }, end: { x: endX, diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index aae921ec..285ef5dc 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm'; -import { WebLinkProvider } from './WebLinkProvider'; +import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm'; +import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider'; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -40,12 +40,6 @@ function handleLink(event: MouseEvent, uri: string): void { } } -interface ILinkProviderOptions { - hover?(event: MouseEvent, text: string, location: IViewportRange): void; - leave?(event: MouseEvent, text: string): void; - urlRegex?: RegExp; -} - export class WebLinksAddon implements ITerminalAddon { private _linkMatcherId: number | undefined; private _terminal: Terminal | undefined; diff --git a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts index 78a258e5..5e6c266d 100644 --- a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts +++ b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts @@ -49,5 +49,10 @@ declare module 'xterm-addon-web-links' { * happen even when tooltipCallback hasn't fired for the link yet. */ leave?(event: MouseEvent, text: string): void; + + /** + * A callback to use instead of the default one. + */ + urlRegex?: RegExp; } } diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index e2c37be2..e9055a17 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -6,14 +6,11 @@ import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; -import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel'; import { fill } from 'common/TypedArrayUtils'; -import { slice } from './TypedArray'; -import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants'; +import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IBufferLine } from 'xterm'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; -import { AttributeData } from 'common/buffer/AttributeData'; interface IVertices { attributes: Float32Array; @@ -24,7 +21,6 @@ interface IVertices { * working on the next frame. */ attributesBuffers: Float32Array[]; - selectionAttributes: Float32Array; count: number; } @@ -91,8 +87,7 @@ export class GlyphRenderer { attributesBuffers: [ new Float32Array(0), new Float32Array(0) - ], - selectionAttributes: new Float32Array(0) + ] }; constructor( @@ -187,6 +182,8 @@ export class GlyphRenderer { if (!this._atlas) { return; } + + // Get the glyph if (chars && chars.length > 1) { rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg); } else { @@ -214,91 +211,6 @@ export class GlyphRenderer { // a_cellpos only changes on resize } - public updateSelection(model: IRenderModel): void { - const terminal = this._terminal; - - this._vertices.selectionAttributes = slice(this._vertices.attributes, 0); - - const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB; - - if (model.selection.columnSelectMode) { - const startCol = model.selection.startCol; - const width = model.selection.endCol - startCol; - const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1; - for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) { - this._updateSelectionRange(startCol, startCol + width, y, model, bg); - } - } else { - // Draw first row - const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0; - const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg); - - // Draw middle rows - const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0); - for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) { - this._updateSelectionRange(0, startRowEndCol, y, model, bg); - } - - // Draw final row - if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) { - // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; - this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg); - } - } - } - - private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void { - const terminal = this._terminal; - const row = y + terminal.buffer.active.viewportY; - let line: IBufferLine | undefined; - for (let x = startCol; x < endCol; x++) { - const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL; - const code = model.cells[offset]; - let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET]; - if (fg & FgFlags.INVERSE) { - const workCell = new AttributeData(); - workCell.fg = fg; - workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET]; - // Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors - // from bg. This is needed since the inverse fg color should be based on the original bg - // color, not on the selection color - fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE); - switch (workCell.getBgColorMode()) { - case Attributes.CM_P16: - case Attributes.CM_P256: - const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba; - fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK; - case Attributes.CM_RGB: - const arr = AttributeData.toColorRGB(workCell.getBgColor()); - fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT; - case Attributes.CM_DEFAULT: - default: - const c2 = this._colors.background.rgba; - fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK; - } - fg |= Attributes.CM_RGB; - } - if (code & COMBINED_CHAR_BIT_MASK) { - if (!line) { - line = terminal.buffer.active.getLine(row); - } - const chars = line!.getCell(x)!.getChars(); - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars); - } else { - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg); - } - } - } - - private _getColorFromAnsiIndex(idx: number): IColor { - if (idx >= this._colors.ansi.length) { - throw new Error('No color found for idx ' + idx); - } - return this._colors.ansi[idx]; - } - public clear(force?: boolean): void { const terminal = this._terminal; const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL; @@ -333,7 +245,7 @@ export class GlyphRenderer { public setColors(): void { } - public render(renderModel: IRenderModel, isSelectionVisible: boolean): void { + public render(renderModel: IRenderModel): void { if (!this._atlas) { return; } @@ -357,7 +269,7 @@ export class GlyphRenderer { let bufferLength = 0; for (let y = 0; y < renderModel.lineLengths.length; y++) { const si = y * this._terminal.cols * INDICES_PER_CELL; - const sub = (isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes).subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL); + const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL); activeBuffer.set(sub, bufferLength); bufferLength += sub.length; } diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index c96cc6bc..420e58d4 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -4,11 +4,11 @@ */ import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; -import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; -import { fill } from 'common/TypedArrayUtils'; +import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types'; import { Attributes, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColor } from 'common/Types'; +import { IColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; @@ -49,7 +49,6 @@ void main() { interface IVertices { attributes: Float32Array; - selection: Float32Array; count: number; } @@ -66,12 +65,10 @@ export class RectangleRenderer { private _attributesBuffer: WebGLBuffer; private _projectionLocation: WebGLUniformLocation; private _bgFloat!: Float32Array; - private _selectionFloat!: Float32Array; private _vertices: IVertices = { count: 0, - attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY), - selection: new Float32Array(3 * INDICES_PER_RECTANGLE) + attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY) }; constructor( @@ -137,11 +134,6 @@ export class RectangleRenderer { gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW); gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count); - - // Bind selection buffer and draw - gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer); - gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW); - gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3); } public onResize(): void { @@ -155,7 +147,6 @@ export class RectangleRenderer { private _updateCachedColors(): void { this._bgFloat = this._colorToFloat32Array(this._colors.background); - this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque); } private _updateViewportRectangle(): void { @@ -171,73 +162,6 @@ export class RectangleRenderer { ); } - public updateSelection(model: ISelectionRenderModel): void { - const terminal = this._terminal; - - if (!model.hasSelection) { - fill(this._vertices.selection, 0, 0); - return; - } - - if (model.columnSelectMode) { - const startCol = model.startCol; - const width = model.endCol - startCol; - const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1; - this._addRectangleFloat( - this._vertices.selection, - 0, - startCol * this._dimensions.scaledCellWidth, - model.viewportCappedStartRow * this._dimensions.scaledCellHeight, - width * this._dimensions.scaledCellWidth, - height * this._dimensions.scaledCellHeight, - this._selectionFloat - ); - fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE); - } else { - // Draw first row - const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0; - const startRowEndCol = model.viewportCappedStartRow === model.viewportEndRow ? model.endCol : terminal.cols; - this._addRectangleFloat( - this._vertices.selection, - 0, - startCol * this._dimensions.scaledCellWidth, - model.viewportCappedStartRow * this._dimensions.scaledCellHeight, - (startRowEndCol - startCol) * this._dimensions.scaledCellWidth, - this._dimensions.scaledCellHeight, - this._selectionFloat - ); - - // Draw middle rows - const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0); - this._addRectangleFloat( - this._vertices.selection, - INDICES_PER_RECTANGLE, - 0, - (model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight, - terminal.cols * this._dimensions.scaledCellWidth, - middleRowsCount * this._dimensions.scaledCellHeight, - this._selectionFloat - ); - - // Draw final row - if (model.viewportCappedStartRow !== model.viewportCappedEndRow) { - // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols; - this._addRectangleFloat( - this._vertices.selection, - INDICES_PER_RECTANGLE * 2, - 0, - model.viewportCappedEndRow * this._dimensions.scaledCellHeight, - endCol * this._dimensions.scaledCellWidth, - this._dimensions.scaledCellHeight, - this._selectionFloat - ); - } else { - fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2); - } - } - } - public updateBackgrounds(model: IRenderModel): void { const terminal = this._terminal; const vertices = this._vertices; diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index b8bcf5b1..4db072e8 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -9,6 +9,7 @@ import { ICharacterJoinerService, IRenderService } from 'browser/services/Servic import { IColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { isSafari } from 'common/Platform'; +import { IDecorationService } from 'common/services/Services'; export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; @@ -30,8 +31,9 @@ export class WebglAddon implements ITerminalAddon { this._terminal = terminal; const renderService: IRenderService = (terminal as any)._core._renderService; const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; + const decorationService: IDecorationService = (terminal as any)._core._decorationService; const colors: IColorSet = (terminal as any)._core._colorManager.colors; - this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer); + this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer); this._renderer.onContextLoss(() => this._onContextLoss.fire()); renderService.setRenderer(this._renderer); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 24e55fed..d060c4d1 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -12,7 +12,7 @@ import { RectangleRenderer } from './RectangleRenderer'; import { IWebGL2RenderingContext } from './Types'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable } from 'common/Lifecycle'; -import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; +import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; @@ -23,6 +23,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { ICharacterJoinerService } from 'browser/services/Services'; import { CharData, ICellData } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; +import { IDecorationService } from 'common/services/Services'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -31,6 +32,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); + private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 }; private _canvas: HTMLCanvasElement; private _gl: IWebGL2RenderingContext; @@ -52,6 +54,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _terminal: Terminal, private _colors: IColorSet, private readonly _characterJoinerService: ICharacterJoinerService, + private readonly _decorationService: IDecorationService, preserveDrawingBuffer?: boolean ) { super(); @@ -164,10 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; this._rectangleRenderer.onResize(); - if (this._model.selection.hasSelection) { - // Update selection as dimensions have changed - this._rectangleRenderer.updateSelection(this._model.selection); - } this._glyphRenderer.setDimensions(this.dimensions); this._glyphRenderer.onResize(); @@ -198,10 +197,8 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.onSelectionChanged(this._terminal, start, end, columnSelectMode); } - this._updateSelectionModel(start, end, columnSelectMode); - - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + this._requestRedrawViewport(); } public onCursorMove(): void { @@ -243,7 +240,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._charAtlas?.clearTexture(); this._model.clear(); this._updateModel(0, this._terminal.rows - 1); - this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + this._requestRedrawViewport(); } public clear(): void { @@ -289,7 +286,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Render this._rectangleRenderer.render(); - this._glyphRenderer.render(this._model, this._model.selection.hasSelection); + this._glyphRenderer.render(this._model); } private _updateModel(start: number, end: number): void { @@ -331,14 +328,17 @@ export class WebglRenderer extends Disposable implements IRenderer { let code = cell.getCode(); const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; + // Load colors/resolve overrides into work colors + this._loadColorsForCell(x, row); + if (code !== NULL_CELL_CODE) { this._model.lineLengths[y] = x + 1; } // Nothing has changed, no updates needed if (this._model.cells[i] === code && - this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg && - this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) { + this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg && + this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) { continue; } @@ -349,10 +349,10 @@ export class WebglRenderer extends Disposable implements IRenderer { // Cache the results in the model this._model.cells[i] = code; - this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg; - this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg; + this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; + this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; - this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.fg, chars); + this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars); if (isJoined) { // Restore work cell @@ -363,17 +363,103 @@ export class WebglRenderer extends Disposable implements IRenderer { const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR); this._model.cells[j] = NULL_CELL_CODE; - this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workCell.bg; - this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg; + this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg; + this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg; } } } } this._rectangleRenderer.updateBackgrounds(this._model); - if (this._model.selection.hasSelection) { - // Model could be updated but the selection is unchanged - this._glyphRenderer.updateSelection(this._model); + } + + /** + * Loads colors for the cell into the work colors object. This resolves overrides/inverse if + * necessary which is why the work cell object is not used. + */ + private _loadColorsForCell(x: number, y: number): void { + this._workColors.bg = this._workCell.bg; + this._workColors.fg = this._workCell.fg; + + // Get any foreground/background overrides, this happens on the model to avoid spreading + // override logic throughout the different sub-renderers + let bgOverride: number | undefined; + let fgOverride: number | undefined; + + // Apply decorations on the bottom layer + for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + } } + + // Apply the selection color if needed + if (this._isCellSelected(x, y)) { + bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF; + } + + // Apply decorations on the top layer + for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) { + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + } + } + + // Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag + // ahead of time in order to use the correct cache key + if (bgOverride !== undefined) { + // Non-RGB attributes from model + override + force RGB color mode + bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB; + } + if (fgOverride !== undefined) { + // Non-RGB attributes from model + force disable inverse + override + force RGB color mode + fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB; + } + + // Handle case where inverse was specified by only one of bgOverride or fgOverride was set, + // resolving the other inverse color and setting the inverse flag if needed. + if (this._workColors.fg & FgFlags.INVERSE) { + if (bgOverride !== undefined && fgOverride === undefined) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } + } + if (bgOverride === undefined && fgOverride !== undefined) { + // Resolve bg color type (default color has a different meaning in fg vs bg) + if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { + bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + } else { + bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); + } + } + } + + // Use the override if it exists + this._workColors.bg = bgOverride ?? this._workColors.bg; + this._workColors.fg = fgOverride ?? this._workColors.fg; + } + + private _isCellSelected(x: number, y: number): boolean { + if (!this._model.selection.hasSelection) { + return false; + } + y -= this._terminal.buffer.active.viewportY; + if (this._model.selection.columnSelectMode) { + return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow && + x < this._model.selection.endCol && y < this._model.selection.viewportCappedEndRow; + } + return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) || + (this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) || + (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) || + (this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol); } private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { @@ -382,7 +468,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // Selection does not exist if (!start || !end || (start[0] === end[0] && start[1] === end[1])) { this._model.clearSelection(); - this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -395,7 +480,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // No need to draw the selection if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) { this._model.clearSelection(); - this._rectangleRenderer.updateSelection(this._model.selection); return; } @@ -407,8 +491,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.selection.viewportCappedEndRow = viewportCappedEndRow; this._model.selection.startCol = start[0]; this._model.selection.endCol = end[0]; - - this._rectangleRenderer.updateSelection(this._model.selection); } /** @@ -440,18 +522,18 @@ export class WebglRenderer extends Disposable implements IRenderer { // will be floored because since lineHeight can never be lower then 1, there // is a guarentee that the scaled line height will always be larger than // scaled char height. - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.getOption('lineHeight')); + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!); // Calculate the y coordinate within a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledCharTop = this._terminal.getOption('lineHeight') === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); // Calculate the scaled cell width, taking the letterSpacing into account. - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.getOption('letterSpacing')); + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!); // Calculate the x coordinate with a cell that text should draw from in // order to draw in the center of a cell. - this.dimensions.scaledCharLeft = Math.floor(this._terminal.getOption('letterSpacing') / 2); + this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2); // Recalculate the canvas dimensions; scaled* define the actual number of // pixel in the canvas @@ -482,6 +564,10 @@ export class WebglRenderer extends Disposable implements IRenderer { this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio; this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio; } + + private _requestRedrawViewport(): void { + this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 }); + } } // TODO: Share impl with core diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 962eb7b3..0ce893df 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -6,7 +6,8 @@ import { ICharAtlasConfig } from './Types'; import { Attributes } from 'common/buffer/Constants'; import { Terminal, FontWeight } from 'xterm'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; +import { IColor } from 'common/Types'; const NULL_COLOR: IColor = { css: '', @@ -28,21 +29,21 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number contrastCache: colors.contrastCache }; return { - customGlyphs: terminal.getOption('customGlyphs'), + customGlyphs: terminal.options.customGlyphs!, devicePixelRatio: window.devicePixelRatio, - letterSpacing: terminal.getOption('letterSpacing'), - lineHeight: terminal.getOption('lineHeight'), + letterSpacing: terminal.options.letterSpacing!, + lineHeight: terminal.options.lineHeight!, scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, - fontFamily: terminal.getOption('fontFamily'), - fontSize: terminal.getOption('fontSize'), - fontWeight: terminal.getOption('fontWeight') as FontWeight, - fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight, - allowTransparency: terminal.getOption('allowTransparency'), - drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'), - minimumContrastRatio: terminal.getOption('minimumContrastRatio'), + fontFamily: terminal.options.fontFamily!, + fontSize: terminal.options.fontSize!, + fontWeight: terminal.options.fontWeight as FontWeight, + fontWeightBold: terminal.options.fontWeightBold as FontWeight, + allowTransparency: terminal.options.allowTransparency!, + drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors!, + minimumContrastRatio: terminal.options.minimumContrastRatio!, colors: clonedColors }; } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index dd95f177..34107fc5 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -8,11 +8,12 @@ import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; -import { IColor } from 'browser/Types'; +import { IColor } from 'common/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; -import { channels, rgba } from 'browser/Color'; +import { channels, rgba } from 'common/Color'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; +import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; // For debugging purposes, it can be useful to set this to a really tiny value, // to verify that LRU eviction works. @@ -216,8 +217,8 @@ export class WebglCharAtlas implements IDisposable { } } - private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string { - const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); + private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, isPowerLineGlyph: boolean): string { + const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, isPowerLineGlyph); if (minimumContrastCss) { return minimumContrastCss; } @@ -238,7 +239,7 @@ export class WebglCharAtlas implements IDisposable { const bg = this._config.colors.background.css; if (bg.length === 9) { // Remove bg alpha channel if present - return bg.substr(0, 7); + return bg.slice(0, 7); } return bg; } @@ -281,8 +282,8 @@ export class WebglCharAtlas implements IDisposable { } } - private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined { - if (this._config.minimumContrastRatio === 1) { + private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, isPowerLineGlyph: boolean): string | undefined { + if (this._config.minimumContrastRatio === 1 || isPowerLineGlyph) { return undefined; } @@ -370,26 +371,16 @@ export class WebglCharAtlas implements IDisposable { `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; this._tmpCtx.textBaseline = TEXT_BASELINE; - this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); + const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0)); + this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, powerLineGlyph); // Apply alpha to dim the character if (dim) { this._tmpCtx.globalAlpha = DIM_OPACITY; } - // Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no - // padding on either side that are allowed for other glyphs since they are designed to be pixel - // perfect but may render with "bad" anti-aliasing - let isPowerlineGlyph = false; - if (chars.length === 1) { - const code = chars.charCodeAt(0); - if (code >= 0xE0A0 && code <= 0xE0D6) { - isPowerlineGlyph = true; - } - } - // For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129) - const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING; + const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING; // Draw custom characters if applicable let drawSuccess = false; @@ -459,7 +450,7 @@ export class WebglCharAtlas implements IDisposable { return NULL_RASTERIZED_GLYPH; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph, drawSuccess); + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerLineGlyph, drawSuccess); const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); // Check if there is enough room in the current row and go to next if needed diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 4c17aad4..619ca1a8 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -254,10 +254,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param isBold If we should use the bold fontWeight. */ protected _getFont(terminal: Terminal, isBold: boolean, isItalic: boolean): string { - const fontWeight = isBold ? terminal.getOption('fontWeightBold') : terminal.getOption('fontWeight'); + const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight; const fontStyle = isItalic ? 'italic' : ''; - return `${fontStyle} ${fontWeight} ${terminal.getOption('fontSize') * window.devicePixelRatio}px ${terminal.getOption('fontFamily')}`; + return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * window.devicePixelRatio}px ${terminal.options.fontFamily}`; } } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 4199b7e1..c80b4c56 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -83,7 +83,7 @@ export class CursorRenderLayer extends BaseRenderLayer { } public onOptionsChanged(terminal: Terminal): void { - if (terminal.getOption('cursorBlink')) { + if (terminal.options.cursorBlink) { if (!this._cursorBlinkStateManager) { this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => { this._render(terminal, true); @@ -140,7 +140,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - const cursorStyle = terminal.getOption('cursorStyle'); + const cursorStyle = terminal.options.cursorStyle; if (cursorStyle && cursorStyle !== 'block') { this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell); } else { @@ -150,7 +150,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._state.x = cursorX; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; - this._state.style = cursorStyle; + this._state.style = cursorStyle!; this._state.width = this._cell.getWidth(); return; } @@ -166,7 +166,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._state.x === cursorX && this._state.y === viewportRelativeCursorY && this._state.isFocused === isTerminalFocused(terminal) && - this._state.style === terminal.getOption('cursorStyle') && + this._state.style === terminal.options.cursorStyle && this._state.width === this._cell.getWidth()) { return; } @@ -174,13 +174,13 @@ export class CursorRenderLayer extends BaseRenderLayer { } this._ctx.save(); - this._cursorRenderers[terminal.getOption('cursorStyle') || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell); + this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell); this._ctx.restore(); this._state.x = cursorX; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; - this._state.style = terminal.getOption('cursorStyle'); + this._state.style = terminal.options.cursorStyle!; this._state.width = this._cell.getWidth(); } @@ -205,7 +205,7 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth')); + this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth!); this._ctx.restore(); } diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index 0b95491f..b0c9f6be 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -20,6 +20,7 @@ ] }, "strict": true, + "downlevelIteration": true, "types": [ "../../../node_modules/@types/mocha" ] diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index e6942d1c..cec1e3b1 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { Browser, Page } from 'playwright'; import { ITheme } from 'xterm'; -import { getBrowserType, launchBrowser, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils'; +import { getBrowserType, launchBrowser, openTerminal, pollFor, timeout, writeSync } from '../../../out-test/api/TestUtils'; import { ITerminalOptions } from '../../../src/common/Types'; const APP = 'http://127.0.0.1:3001/test'; @@ -49,7 +49,7 @@ describe('WebGL Renderer Integration Tests', async () => { cyan: '#131415', white: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[30mâ–ˆ\\x1b[31mâ–ˆ\\x1b[32mâ–ˆ\\x1b[33mâ–ˆ\\x1b[34mâ–ˆ\\x1b[35mâ–ˆ\\x1b[36mâ–ˆ\\x1b[37mâ–ˆ`); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); @@ -73,8 +73,8 @@ describe('WebGL Renderer Integration Tests', async () => { brightWhite: '#161718' }; await page.evaluate(` - window.term.setOption('theme', ${JSON.stringify(theme)}); - window.term.setOption('drawBoldTextInBrightColors', true); + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.drawBoldTextInBrightColors = true; `); await writeSync(page, `\\x1b[1;30mâ–ˆ\\x1b[1;31mâ–ˆ\\x1b[1;32mâ–ˆ\\x1b[1;33mâ–ˆ\\x1b[1;34mâ–ˆ\\x1b[1;35mâ–ˆ\\x1b[1;36mâ–ˆ\\x1b[1;37mâ–ˆ`); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); @@ -98,7 +98,7 @@ describe('WebGL Renderer Integration Tests', async () => { cyan: '#131415', white: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); @@ -121,7 +121,7 @@ describe('WebGL Renderer Integration Tests', async () => { cyan: '#131415', white: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); @@ -144,7 +144,7 @@ describe('WebGL Renderer Integration Tests', async () => { cyan: '#131415', white: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[7;40mâ–ˆ\\x1b[7;41mâ–ˆ\\x1b[7;42mâ–ˆ\\x1b[7;43mâ–ˆ\\x1b[7;44mâ–ˆ\\x1b[7;45mâ–ˆ\\x1b[7;46mâ–ˆ\\x1b[7;47mâ–ˆ`); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); @@ -167,7 +167,7 @@ describe('WebGL Renderer Integration Tests', async () => { cyan: '#131415', white: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `); await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]); @@ -190,7 +190,7 @@ describe('WebGL Renderer Integration Tests', async () => { cyan: '#131415', white: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[8;40mâ–ˆ\\x1b[8;41mâ–ˆ\\x1b[8;42mâ–ˆ\\x1b[8;43mâ–ˆ\\x1b[8;44mâ–ˆ\\x1b[8;45mâ–ˆ\\x1b[8;46mâ–ˆ\\x1b[8;47mâ–ˆ`); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); @@ -213,7 +213,7 @@ describe('WebGL Renderer Integration Tests', async () => { brightCyan: '#131415', brightWhite: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[90mâ–ˆ\\x1b[91mâ–ˆ\\x1b[92mâ–ˆ\\x1b[93mâ–ˆ\\x1b[94mâ–ˆ\\x1b[95mâ–ˆ\\x1b[96mâ–ˆ\\x1b[97mâ–ˆ`); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); @@ -236,7 +236,7 @@ describe('WebGL Renderer Integration Tests', async () => { brightCyan: '#131415', brightWhite: '#161718' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, `\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); @@ -260,9 +260,9 @@ describe('WebGL Renderer Integration Tests', async () => { for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.substr(1, 2), 16); - const g = parseInt(cssColor.substr(3, 2), 16); - const b = parseInt(cssColor.substr(5, 2), 16); + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); } } @@ -280,9 +280,9 @@ describe('WebGL Renderer Integration Tests', async () => { for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.substr(1, 2), 16); - const g = parseInt(cssColor.substr(3, 2), 16); - const b = parseInt(cssColor.substr(5, 2), 16); + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); } } @@ -300,9 +300,9 @@ describe('WebGL Renderer Integration Tests', async () => { for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.substr(1, 2), 16); - const g = parseInt(cssColor.substr(3, 2), 16); - const b = parseInt(cssColor.substr(5, 2), 16); + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); } } @@ -320,9 +320,9 @@ describe('WebGL Renderer Integration Tests', async () => { for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.substr(1, 2), 16); - const g = parseInt(cssColor.substr(3, 2), 16); - const b = parseInt(cssColor.substr(5, 2), 16); + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); } } @@ -356,9 +356,9 @@ describe('WebGL Renderer Integration Tests', async () => { for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.substr(1, 2), 16); - const g = parseInt(cssColor.substr(3, 2), 16); - const b = parseInt(cssColor.substr(5, 2), 16); + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); } } @@ -715,8 +715,8 @@ describe('WebGL Renderer Integration Tests', async () => { brightWhite: '#eeeeec' }; await page.evaluate(` - window.term.setOption('theme', ${JSON.stringify(theme)}); - window.term.setOption('minimumContrastRatio', 1); + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; `); await writeSync(page, `\\x1b[30mâ–ˆ\\x1b[31mâ–ˆ\\x1b[32mâ–ˆ\\x1b[33mâ–ˆ\\x1b[34mâ–ˆ\\x1b[35mâ–ˆ\\x1b[36mâ–ˆ\\x1b[37mâ–ˆ\\r\\n` + @@ -742,21 +742,21 @@ describe('WebGL Renderer Integration Tests', async () => { // Setting and check for minimum contrast values, note that these are note // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail - await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`); + await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]); await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]); - await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]); - await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]); - await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]); - await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]); + await pollFor(page, () => getCellColor(3, 1), [152, 198, 110, 255]); + await pollFor(page, () => getCellColor(4, 1), [208, 179, 49, 255]); + await pollFor(page, () => getCellColor(5, 1), [161, 183, 215, 255]); + await pollFor(page, () => getCellColor(6, 1), [191, 174, 194, 255]); await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]); await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]); await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]); await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]); await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]); await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]); - await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]); - await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]); + await pollFor(page, () => getCellColor(5, 2), [154, 186, 221, 255]); + await pollFor(page, () => getCellColor(6, 2), [203, 173, 199, 255]); // Unchanged await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); @@ -783,8 +783,8 @@ describe('WebGL Renderer Integration Tests', async () => { brightWhite: '#eeeeec' }; await page.evaluate(` - window.term.setOption('theme', ${JSON.stringify(theme)}); - window.term.setOption('minimumContrastRatio', 1); + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; `); await writeSync(page, `\\x1b[30mâ–ˆ\\x1b[31mâ–ˆ\\x1b[32mâ–ˆ\\x1b[33mâ–ˆ\\x1b[34mâ–ˆ\\x1b[35mâ–ˆ\\x1b[36mâ–ˆ\\x1b[37mâ–ˆ\\r\\n` + @@ -810,21 +810,21 @@ describe('WebGL Renderer Integration Tests', async () => { // Setting and check for minimum contrast values, note that these are note // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail - await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`); + await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]); await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]); - await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]); - await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]); - await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]); - await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]); + await pollFor(page, () => getCellColor(3, 1), [36, 72, 0, 255]); + await pollFor(page, () => getCellColor(4, 1), [72, 59, 0, 255]); + await pollFor(page, () => getCellColor(5, 1), [32, 64, 106, 255]); + await pollFor(page, () => getCellColor(6, 1), [75, 51, 80, 255]); await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]); await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]); await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]); await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]); - await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]); - await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]); - await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]); - await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]); + await pollFor(page, () => getCellColor(3, 2), [40, 67, 13, 255]); + await pollFor(page, () => getCellColor(4, 2), [67, 63, 19, 255]); + await pollFor(page, () => getCellColor(5, 2), [45, 65, 87, 255]); + await pollFor(page, () => getCellColor(6, 2), [81, 57, 78, 255]); await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]); await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); }); @@ -843,7 +843,7 @@ describe('WebGL Renderer Integration Tests', async () => { background: '#00FF00', selection: '#0000FF' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await writeSync(page, ` â–ˆ\\x1b[7mâ–ˆ\\x1b[0m`); await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]); await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]); @@ -867,13 +867,102 @@ describe('WebGL Renderer Integration Tests', async () => { const theme: ITheme = { background: '#ff000080' }; - await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); const data = `\\x1b[7mâ–ˆ\x1b[0m`; await writeSync(page, data); // Inverse background should be opaque await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); }); }); + + describe('decoration color overrides', async () => { + if (areTestsEnabled) { + before(async () => setupBrowser({ rendererType: 'dom' })); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + } + + itWebgl('foregroundColor', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `â–ˆ`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + itWebgl('foregroundColor should ignore inverse', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\\x1b[7mâ–ˆ\\x1b[0m`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + itWebgl('foregroundColor should ignore inverse (only fg on decoration)', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + foregroundColor: '#ff0000' + }); + `); + const data = `\\x1b[7mâ–ˆ \\x1b[0m`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of 'â–ˆ' should be decoration fg override + await pollFor(page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground + }); + itWebgl('backgroundColor', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = ` `; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); + itWebgl('backgroundColor should ignore inverse', async () => { + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\\x1b[7m \\x1b[0m`; + await writeSync(page, data); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); + itWebgl('backgroundColor should ignore inverse (only bg on decoration)', async () => { + const data = `\\x1b[7mâ–ˆ \\x1b[0m`; + await writeSync(page, data); + await page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + backgroundColor: '#0000ff' + }); + `); + await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of 'â–ˆ' should be default + await pollFor(page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override + }); + }); }); async function getCellColor(col: number, row: number): Promise { diff --git a/bin/publish.js b/bin/publish.js index 75de4b68..a43b8cd4 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -104,11 +104,11 @@ function getNextBetaVersion(packageJson) { return `${nextStableVersion}-${tag}.1`; } const latestPublishedVersion = publishedVersions.sort((a, b) => { - const aVersion = parseInt(a.substr(a.search(/\d+$/))); - const bVersion = parseInt(b.substr(b.search(/\d+$/))); + const aVersion = parseInt(a.slice(a.search(/\d+$/))); + const bVersion = parseInt(b.slice(b.search(/\d+$/))); return aVersion > bVersion ? -1 : 1; })[0]; - const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/\d+$/)), 10); + const latestTagVersion = parseInt(latestPublishedVersion.slice(latestPublishedVersion.search(/\d+$/)), 10); return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`; } diff --git a/css/xterm.css b/css/xterm.css index ab3965b4..2f84c859 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -36,6 +36,7 @@ */ .xterm { + cursor: text; position: relative; user-select: none; -ms-user-select: none; @@ -124,10 +125,6 @@ line-height: normal; } -.xterm { - cursor: text; -} - .xterm.enable-mouse-events { /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ cursor: default; @@ -178,3 +175,11 @@ z-index: 6; position: absolute; } + +.xterm-decoration-overview-ruler { + z-index: 7; + position: absolute; + top: 0; + right: 0; + pointer-events: none; +} diff --git a/demo/client.ts b/demo/client.ts index aee23401..b9e52d7b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -92,6 +92,7 @@ const addons: { [T in AddonType]: IDemoAddon} = { const terminalContainer = document.getElementById('terminal-container'); const actionElements = { + find: document.querySelector('#find'), findNext: document.querySelector('#find-next'), findPrevious: document.querySelector('#find-previous') }; @@ -107,7 +108,15 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked, - incremental: e.key !== `Enter` + incremental: e.key !== `Enter`, + decorations: (document.getElementById('highlight-all-matches') as HTMLInputElement).checked ? { + matchBackground: '#232422', + matchBorder: '#555753', + matchOverviewRuler: '#555753', + activeMatchBackground: '#ef2929', + activeMatchBorder: '#ffffff', + activeMatchColorOverviewRuler: '#ef2929' + } : undefined }; } @@ -151,6 +160,7 @@ if (document.location.pathname === '/test') { document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); document.getElementById('load-test').addEventListener('click', loadTest); document.getElementById('add-decoration').addEventListener('click', addDecoration); + document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); } function createTerminal(): void { @@ -202,10 +212,15 @@ function createTerminal(): void { addDomListener(actionElements.findNext, 'keyup', (e) => { addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e)); }); - addDomListener(actionElements.findPrevious, 'keyup', (e) => { addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e)); }); + addDomListener(actionElements.findNext, 'blur', (e) => { + addons.search.instance.clearActiveDecoration(); + }); + addDomListener(actionElements.findPrevious, 'blur', (e) => { + addons.search.instance.clearActiveDecoration(); + }); // fit is called within a setTimeout, cols and rows need this. setTimeout(() => { @@ -544,9 +559,29 @@ function loadTest() { } function addDecoration() { + term.options['overviewRulerWidth'] = 15; const marker = term.addMarker(1); - const decoration = term.registerDecoration({ marker }); - decoration.onRender(() => { - decoration.element.style.backgroundColor = 'red'; + const decoration = term.registerDecoration({ + marker, + backgroundColor: '#00FF00', + foregroundColor: '#00FE00', + overviewRulerOptions: { color: '#ef292980', position: 'left' } + }); + decoration.onRender((e: HTMLElement) => { + e.style.right = '100%'; + e.style.backgroundColor = '#ef292980'; }); } + +function addOverviewRuler() { + term.options['overviewRulerWidth'] = 15; + term.registerDecoration({marker: term.addMarker(1), overviewRulerOptions: { color: '#ef2929' }}); + term.registerDecoration({marker: term.addMarker(3), overviewRulerOptions: { color: '#8ae234' }}); + term.registerDecoration({marker: term.addMarker(5), overviewRulerOptions: { color: '#729fcf' }}); + term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }}); + term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }}); + term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }}); + term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' }}); + term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' }}); +} + diff --git a/demo/index.html b/demo/index.html index e024222c..f36fc629 100644 --- a/demo/index.html +++ b/demo/index.html @@ -28,7 +28,7 @@

Options

-

These options can be set in the Terminal constructor or by using the Terminal.setOption function.

+

These options can be set in the Terminal constructor or by using the Terminal.options property.

@@ -43,6 +43,7 @@ +

SerializeAddon

@@ -69,6 +70,7 @@ +
diff --git a/demo/server.js b/demo/server.js index c0d5e1f6..71a9d36a 100644 --- a/demo/server.js +++ b/demo/server.js @@ -114,6 +114,9 @@ function startServer() { } const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5); + // WARNING: This is a naive implementation that will not throttle the flow of data. This means + // it could flood the communication channel and make the terminal unresponsive. Learn more about + // the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/ term.on('data', function(data) { try { send(data); diff --git a/package.json b/package.json index 03811997..c3e29be5 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,6 @@ "webpack": "^5.61.0", "webpack-cli": "^4.9.1", "ws": "^8.2.3", - "xterm-benchmark": "^0.3.0" + "xterm-benchmark": "^0.3.1" } } diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts index b96b66cc..73b7a0b7 100644 --- a/src/browser/ColorContrastCache.ts +++ b/src/browser/ColorContrastCache.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { IColor, IColorContrastCache } from 'browser/Types'; +import { IColorContrastCache } from 'browser/Types'; +import { IColor } from 'common/Types'; export class ColorContrastCache implements IColorContrastCache { private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {}; diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index b4b57c67..2d6e4ea5 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types'; +import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types'; import { ITheme } from 'common/services/Services'; -import { channels, color, css } from 'browser/Color'; +import { channels, color, css } from 'common/Color'; import { ColorContrastCache } from 'browser/ColorContrastCache'; -import { ColorIndex } from 'common/Types'; +import { ColorIndex, IColor } from 'common/Types'; interface IRestoreColorSet { @@ -185,7 +185,7 @@ export class ColorManager implements IColorManager { foreground: this.colors.foreground, background: this.colors.background, cursor: this.colors.cursor, - ansi: [...this.colors.ansi] + ansi: this.colors.ansi.slice() }; } diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 872bfdc7..3eafb261 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -231,7 +231,7 @@ describe('Terminal', () => { }); term.paste('foo'); }); - it('should sanitize \n chars', done => { + it('should sanitize \\n chars', done => { term.onData(e => { assert.equal(e, '\rfoo\rbar\r'); done(); @@ -1332,8 +1332,8 @@ describe('Terminal', () => { (!(i % 3)) ? input[i] : (i % 3 === 1) - ? input.substr(i, 2) - : input.substr(i - 1, 2), + ? input.slice(i, i + 2) + : input.slice(i - 1, i + 1), terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); } }); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 703c995b..de3fff90 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -26,7 +26,7 @@ import { IRenderer } from 'browser/renderer/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; -import { C0 } from 'common/data/EscapeSequences'; +import { C0, C1_ESCAPED } from 'common/data/EscapeSequences'; import { WindowsOptionsReportType } from '../common/InputHandler'; import { Renderer } from 'browser/renderer/Renderer'; import { Linkifier } from 'browser/Linkifier'; @@ -45,17 +45,20 @@ import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService, IDecorationService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { IBuffer } from 'common/buffer/Types'; import { MouseService } from 'browser/services/MouseService'; import { Linkifier2 } from 'browser/Linkifier2'; import { CoreBrowserService } from 'browser/services/CoreBrowserService'; import { CoreTerminal } from 'common/CoreTerminal'; -import { color, rgba } from 'browser/Color'; +import { color, rgba } from 'common/Color'; import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'; import { toRgbString } from 'common/input/XParseColor'; -import { DecorationService } from 'browser/services/DecorationService'; +import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer'; +import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer'; +import { DecorationService } from 'common/services/DecorationService'; +import { IDecorationService } from 'common/services/Services'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -71,6 +74,8 @@ export class Terminal extends CoreTerminal implements ITerminal { private _helperContainer: HTMLElement | undefined; private _compositionView: HTMLElement | undefined; + private _overviewRulerRenderer: OverviewRulerRenderer | undefined; + // private _visualBellTimer: number; public browser: IBrowser = Browser as any; @@ -78,6 +83,7 @@ export class Terminal extends CoreTerminal implements ITerminal { private _customKeyEventHandler: CustomKeyEventHandler | undefined; // browser services + private _decorationService: DecorationService; private _charSizeService: ICharSizeService | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; @@ -92,6 +98,12 @@ export class Terminal extends CoreTerminal implements ITerminal { */ private _keyDownHandled: boolean = false; + /** + * Records whether a keydown event has occured since the last keyup event, i.e. whether a key + * is currently "pressed". + */ + private _keyDownSeen: boolean = false; + /** * Records whether the keypress event has already been handled and triggered a data event, if so * the input event should not trigger a data event but should still print to the textarea so @@ -109,7 +121,6 @@ export class Terminal extends CoreTerminal implements ITerminal { public linkifier: ILinkifier; public linkifier2: ILinkifier2; public viewport: IViewport | undefined; - public decorationService: IDecorationService; private _compositionHelper: ICompositionHelper | undefined; private _mouseZoneManager: IMouseZoneManager | undefined; private _accessibilityManager: AccessibilityManager | undefined; @@ -159,7 +170,8 @@ export class Terminal extends CoreTerminal implements ITerminal { this.linkifier = this._instantiationService.createInstance(Linkifier); this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2)); - this.decorationService = this.register(this._instantiationService.createInstance(DecorationService)); + this._decorationService = this._instantiationService.createInstance(DecorationService); + this._instantiationService.setService(IDecorationService, this._decorationService); // Setup InputHandler listeners this.register(this._inputHandler.onRequestBell(() => this.bell())); @@ -212,7 +224,7 @@ export class Terminal extends CoreTerminal implements ITerminal { const channels = color.toColorRGB(acc === 'ansi' ? this._colorManager.colors.ansi[req.index] : this._colorManager.colors[acc]); - this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C0.BEL}`); + this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`); break; case ColorRequestType.SET: if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); @@ -471,6 +483,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._viewportElement = document.createElement('div'); this._viewportElement.classList.add('xterm-viewport'); fragment.appendChild(this._viewportElement); + this._viewportScrollArea = document.createElement('div'); this._viewportScrollArea.classList.add('xterm-scroll-area'); this._viewportElement.appendChild(this._viewportScrollArea); @@ -576,8 +589,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this.onScroll(() => this._mouseZoneManager!.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService); - - this.decorationService.attachToDom(this.screenElement, this._renderService, this._bufferService); + this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement)); // This event listener must be registered aftre MouseZoneManager is created this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e))); @@ -595,6 +607,14 @@ export class Terminal extends CoreTerminal implements ITerminal { this._accessibilityManager = new AccessibilityManager(this, this._renderService); } + if (this.options.overviewRulerWidth) { + this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement); + } + this.optionsService.onOptionChange(() => { + if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) { + this._overviewRulerRenderer = this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement); + } + }); // Measure the character size this._charSizeService.measure(); @@ -1003,7 +1023,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - return this.decorationService!.registerDecoration(decorationOptions); + return this._decorationService.registerDecoration(decorationOptions); } /** @@ -1070,6 +1090,7 @@ export class Terminal extends CoreTerminal implements ITerminal { */ protected _keyDown(event: KeyboardEvent): boolean | undefined { this._keyDownHandled = false; + this._keyDownSeen = true; if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) { return false; @@ -1155,6 +1176,8 @@ export class Terminal extends CoreTerminal implements ITerminal { } protected _keyUp(ev: KeyboardEvent): void { + this._keyDownSeen = false; + if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) { return; } @@ -1228,7 +1251,8 @@ export class Terminal extends CoreTerminal implements ITerminal { protected _inputEvent(ev: InputEvent): boolean { // Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to // support reading out character input which can doubling up input characters - if (ev.data && ev.inputType === 'insertText' && !ev.composed && !this.optionsService.rawOptions.screenReaderMode) { + // Based on these event traces: https://github.com/xtermjs/xterm.js/issues/3679 + if (ev.data && ev.inputType === 'insertText' && (!ev.composed || !this._keyDownSeen) && !this.optionsService.rawOptions.screenReaderMode) { if (this._keyPressHandled) { return false; } @@ -1301,7 +1325,7 @@ export class Terminal extends CoreTerminal implements ITerminal { // Don't clear if it's already clear return; } - this.buffer.clearMarkers(); + this.buffer.clearAllMarkers(0); this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!); this.buffer.lines.length = 1; this.buffer.ydisp = 0; @@ -1334,6 +1358,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._setup(); super.reset(); this._selectionService?.reset(); + this._decorationService.reset(); // reattach this._customKeyEventHandler = customKeyEventHandler; diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 10a1435b..85e2bb55 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -257,7 +257,10 @@ export class MockBuffer implements IBuffer { public getWhitespaceCell(attr?: IAttributeData): ICellData { throw new Error('Method not implemented.'); } - public clearMarkers(): void { + public clearMarkers(y: number): void { + throw new Error('Method not implemented.'); + } + public clearAllMarkers(excludeY: number): void { throw new Error('Method not implemented.'); } } @@ -368,6 +371,7 @@ export class MockRenderService implements IRenderService { public serviceBrand: undefined; public onDimensionsChange: IEvent = new EventEmitter().event; public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; + public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event; public dimensions: IRenderDimensions = { scaledCharWidth: 0, diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 35b52d62..0e83c213 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -5,7 +5,7 @@ import { IDecorationOptions, IDecoration, IDisposable, IMarker, ISelectionPosition } from 'xterm'; import { IEvent } from 'common/EventEmitter'; -import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types'; +import { ICoreTerminal, CharData, ITerminalOptions, IColor } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IFunctionIdentifier, IParams } from 'common/parser/Types'; @@ -112,11 +112,6 @@ export interface IColorManager { onOptionsChange(key: string): void; } -export interface IColor { - css: string; - rgba: number; // 32-bit int with rgba in each byte -} - export interface IColorSet { foreground: IColor; background: IColor; diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts new file mode 100644 index 00000000..ac3457f3 --- /dev/null +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IRenderService } from 'browser/services/Services'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services'; + +export class BufferDecorationRenderer extends Disposable { + private readonly _container: HTMLElement; + private readonly _decorationElements: Map = new Map(); + + private _animationFrame: number | undefined; + private _altBufferIsActive: boolean = false; + + constructor( + private readonly _screenElement: HTMLElement, + @IBufferService private readonly _bufferService: IBufferService, + @IDecorationService private readonly _decorationService: IDecorationService, + @IRenderService private readonly _renderService: IRenderService + ) { + super(); + + this._container = document.createElement('div'); + this._container.classList.add('xterm-decoration-container'); + this._screenElement.appendChild(this._container); + + this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); + this.register(this._renderService.onDimensionsChange(() => this._queueRefresh())); + this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh())); + this.register(this._bufferService.buffers.onBufferActivate(() => { + this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt; + })); + this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh())); + this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration))); + } + + public override dispose(): void { + this._container.remove(); + this._decorationElements.clear(); + super.dispose(); + } + + private _queueRefresh(): void { + if (this._animationFrame !== undefined) { + return; + } + this._animationFrame = window.requestAnimationFrame(() => { + this.refreshDecorations(); + this._animationFrame = undefined; + }); + } + + public refreshDecorations(): void { + for (const decoration of this._decorationService.decorations) { + this._renderDecoration(decoration); + } + } + + private _renderDecoration(decoration: IInternalDecoration): void { + this._refreshStyle(decoration); + } + + private _createElement(decoration: IInternalDecoration): HTMLElement { + const element = document.createElement('div'); + element.classList.add('xterm-decoration'); + element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth)}px`; + element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`; + element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`; + element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`; + + const x = decoration.options.x ?? 0; + if (x && x > this._bufferService.cols) { + // exceeded the container width, so hide + element.style.display = 'none'; + } + if ((decoration.options.anchor || 'left') === 'right') { + element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + } else { + element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + } + + return element; + } + + private _refreshStyle(decoration: IInternalDecoration): void { + const line = decoration.marker.line - this._bufferService.buffers.active.ydisp; + if (line < 0 || line >= this._bufferService.rows) { + // outside of viewport + if (decoration.element) { + decoration.element.style.display = 'none'; + decoration.onRenderEmitter.fire(decoration.element); + } + } else { + let element = this._decorationElements.get(decoration); + if (!element) { + decoration.onDispose(() => this._removeDecoration(decoration)); + element = this._createElement(decoration); + decoration.element = element; + this._decorationElements.set(decoration, element); + this._container.appendChild(element); + } + element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`; + element.style.display = this._altBufferIsActive ? 'none' : 'block'; + decoration.onRenderEmitter.fire(element); + } + } + + private _removeDecoration(decoration: IInternalDecoration): void { + this._decorationElements.get(decoration)?.remove(); + this._decorationElements.delete(decoration); + } +} diff --git a/src/browser/decorations/ColorZoneStore.test.ts b/src/browser/decorations/ColorZoneStore.test.ts new file mode 100644 index 00000000..719ef45b --- /dev/null +++ b/src/browser/decorations/ColorZoneStore.test.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { ColorZoneStore } from 'browser/decorations/ColorZoneStore'; + +const optionsRedFull = { + overviewRulerOptions: { + color: 'red', + position: 'full' as 'full' + } +}; + +describe('ColorZoneStore', () => { + let store: ColorZoneStore; + + beforeEach(() => { + store = new ColorZoneStore(); + store.setPadding({ + full: 1, + left: 1, + center: 1, + right: 1 + }); + }); + + it('should merge adjacent zones', () => { + store.addDecoration({ + marker: { line: 0 }, + options: optionsRedFull + }); + store.addDecoration({ + marker: { line: 1 }, + options: optionsRedFull + }); + assert.deepStrictEqual(store.zones, [ + { + color: 'red', + position: 'full', + startBufferLine: 0, + endBufferLine: 1 + } + ]); + }); + + it('should not merge non-adjacent zones', () => { + store.addDecoration({ + marker: { line: 0 }, + options: optionsRedFull + }); + store.addDecoration({ + marker: { line: 2 }, + options: optionsRedFull + }); + assert.deepStrictEqual(store.zones, [ + { + color: 'red', + position: 'full', + startBufferLine: 0, + endBufferLine: 0 + }, + { + color: 'red', + position: 'full', + startBufferLine: 2, + endBufferLine: 2 + } + ]); + }); + + it('should reuse zone objects', () => { + const obj = { + marker: { line: 0 }, + options: optionsRedFull + }; + store.addDecoration(obj); + const zone = store.zones[0]; + store.clear(); + store.addDecoration({ + marker: { line: 1 }, + options: optionsRedFull + }); + // The object reference should be the same + assert.equal(zone, store.zones[0]); + }); +}); diff --git a/src/browser/decorations/ColorZoneStore.ts b/src/browser/decorations/ColorZoneStore.ts new file mode 100644 index 00000000..d066bedb --- /dev/null +++ b/src/browser/decorations/ColorZoneStore.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInternalDecoration } from 'common/services/Services'; + +export interface IColorZoneStore { + readonly zones: IColorZone[]; + clear(): void; + addDecoration(decoration: IInternalDecoration): void; + /** + * Sets the amount of padding in lines that will be added between zones, if new lines intersect + * the padding they will be merged into the same zone. + */ + setPadding(padding: { [position: string]: number }): void; +} + +export interface IColorZone { + /** Color in a format supported by canvas' fillStyle. */ + color: string; + position: 'full' | 'left' | 'center' | 'right' | undefined; + startBufferLine: number; + endBufferLine: number; +} + +interface IMinimalDecorationForColorZone { + marker: Pick; + options: Pick; +} + +export class ColorZoneStore implements IColorZoneStore { + private _zones: IColorZone[] = []; + + // The zone pool is used to keep zone objects from being freed between clearing the color zone + // store and fetching the zones. This helps reduce GC pressure since the color zones are + // accumulated on potentially every scroll event. + private _zonePool: IColorZone[] = []; + private _zonePoolIndex = 0; + + private _linePadding: { [position: string]: number } = { + full: 0, + left: 0, + center: 0, + right: 0 + }; + + public get zones(): IColorZone[] { + // Trim the zone pool to free unused memory + this._zonePool.length = Math.min(this._zonePool.length, this._zones.length); + return this._zones; + } + + public clear(): void { + this._zones.length = 0; + this._zonePoolIndex = 0; + } + + public addDecoration(decoration: IMinimalDecorationForColorZone): void { + if (!decoration.options.overviewRulerOptions) { + return; + } + for (const z of this._zones) { + if (z.color === decoration.options.overviewRulerOptions.color && + z.position === decoration.options.overviewRulerOptions.position) { + if (this._lineIntersectsZone(z, decoration.marker.line)) { + return; + } + if (this._lineAdjacentToZone(z, decoration.marker.line, decoration.options.overviewRulerOptions.position)) { + this._addLineToZone(z, decoration.marker.line); + return; + } + } + } + // Create using zone pool if possible + if (this._zonePoolIndex < this._zonePool.length) { + this._zonePool[this._zonePoolIndex].color = decoration.options.overviewRulerOptions.color; + this._zonePool[this._zonePoolIndex].position = decoration.options.overviewRulerOptions.position; + this._zonePool[this._zonePoolIndex].startBufferLine = decoration.marker.line; + this._zonePool[this._zonePoolIndex].endBufferLine = decoration.marker.line; + this._zones.push(this._zonePool[this._zonePoolIndex++]); + return; + } + // Create + this._zones.push({ + color: decoration.options.overviewRulerOptions.color, + position: decoration.options.overviewRulerOptions.position, + startBufferLine: decoration.marker.line, + endBufferLine: decoration.marker.line + }); + this._zonePool.push(this._zones[this._zones.length - 1]); + this._zonePoolIndex++; + } + + public setPadding(padding: { [position: string]: number }): void { + this._linePadding = padding; + } + + private _lineIntersectsZone(zone: IColorZone, line: number): boolean { + return ( + line >= zone.startBufferLine && + line <= zone.endBufferLine + ); + } + + private _lineAdjacentToZone(zone: IColorZone, line: number, position: IColorZone['position']): boolean { + return ( + (line >= zone.startBufferLine - this._linePadding[position || 'full']) && + (line <= zone.endBufferLine + this._linePadding[position || 'full']) + ); + } + + private _addLineToZone(zone: IColorZone, line: number): void { + zone.startBufferLine = Math.min(zone.startBufferLine, line); + zone.endBufferLine = Math.max(zone.endBufferLine, line); + } +} diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts new file mode 100644 index 00000000..39480ca2 --- /dev/null +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IRenderService } from 'browser/services/Services'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services'; + +// Helper objects to avoid excessive calculation and garbage collection during rendering. These are +// static values for each render and can be accessed using the decoration position as the key. +const drawHeight = { + full: 0, + left: 0, + center: 0, + right: 0 +}; +const drawWidth = { + full: 0, + left: 0, + center: 0, + right: 0 +}; +const drawX = { + full: 0, + left: 0, + center: 0, + right: 0 +}; + +export class OverviewRulerRenderer extends Disposable { + private readonly _canvas: HTMLCanvasElement; + private readonly _ctx: CanvasRenderingContext2D; + private readonly _colorZoneStore: IColorZoneStore = new ColorZoneStore(); + private get _width(): number { + return this._optionsService.options.overviewRulerWidth || 0; + } + private _animationFrame: number | undefined; + + private _shouldUpdateDimensions: boolean | undefined = true; + private _shouldUpdateAnchor: boolean | undefined = true; + private _lastKnownBufferLength: number = 0; + + private _containerHeight: number | undefined; + + constructor( + private readonly _viewportElement: HTMLElement, + private readonly _screenElement: HTMLElement, + @IBufferService private readonly _bufferService: IBufferService, + @IDecorationService private readonly _decorationService: IDecorationService, + @IRenderService private readonly _renderService: IRenderService, + @IOptionsService private readonly _optionsService: IOptionsService + ) { + super(); + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('xterm-decoration-overview-ruler'); + this._refreshCanvasDimensions(); + this._viewportElement.parentElement?.insertBefore(this._canvas, this._viewportElement); + const ctx = this._canvas.getContext('2d'); + if (!ctx) { + throw new Error('Ctx cannot be null'); + } else { + this._ctx = ctx; + } + this._registerDecorationListeners(); + this._registerBufferChangeListeners(); + this._registerDimensionChangeListeners(); + } + + /** + * On decoration add or remove, redraw + */ + private _registerDecorationListeners(): void { + this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh(undefined, true))); + this.register(this._decorationService.onDecorationRemoved(() => this._queueRefresh(undefined, true))); + } + + /** + * On buffer change, redraw + * and hide the canvas if the alt buffer is active + */ + private _registerBufferChangeListeners(): void { + this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); + this.register(this._bufferService.buffers.onBufferActivate(() => { + this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; + })); + this.register(this._bufferService.onScroll(() => { + if (this._lastKnownBufferLength !== this._bufferService.buffers.normal.lines.length) { + this._refreshColorZonePadding(); + } + })); + } + /** + * On dimension change, update canvas dimensions + * and then redraw + */ + private _registerDimensionChangeListeners(): void { + // container height changed + this.register(this._renderService.onRender((): void => { + if (!this._containerHeight || this._containerHeight !== this._screenElement.clientHeight) { + this._queueRefresh(true); + this._containerHeight = this._screenElement.clientHeight; + } + })); + // overview ruler width changed + this.register(this._optionsService.onOptionChange(o => { + if (o === 'overviewRulerWidth') { + this._queueRefresh(true); + } + })); + // device pixel ratio changed + this.register(addDisposableDomListener(window, 'resize', () => { + this._queueRefresh(true); + })); + // set the canvas dimensions + this._queueRefresh(true); + } + + public override dispose(): void { + this._canvas?.remove(); + super.dispose(); + } + + private _refreshDrawConstants(): void { + // width + const outerWidth = Math.floor(this._canvas.width / 3); + const innerWidth = Math.ceil(this._canvas.width / 3); + drawWidth.full = this._canvas.width; + drawWidth.left = outerWidth; + drawWidth.center = innerWidth; + drawWidth.right = outerWidth; + // height + drawHeight.full = Math.round(2 * window.devicePixelRatio); + drawHeight.left = Math.round(6 * window.devicePixelRatio); + drawHeight.center = Math.round(6 * window.devicePixelRatio); + drawHeight.right = Math.round(6 * window.devicePixelRatio); + // x + drawX.full = 0; + drawX.left = 0; + drawX.center = drawWidth.left; + drawX.right = drawWidth.left + drawWidth.center; + } + + private _refreshColorZonePadding(): void { + this._colorZoneStore.setPadding({ + full: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.full), + left: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.left), + center: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.center), + right: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.right) + }); + this._lastKnownBufferLength = this._bufferService.buffers.normal.lines.length; + } + + private _refreshCanvasDimensions(): void { + this._canvas.style.width = `${this._width}px`; + this._canvas.width = Math.round(this._width * window.devicePixelRatio); + this._canvas.style.height = `${this._screenElement.clientHeight}px`; + this._canvas.height = Math.round(this._screenElement.clientHeight * window.devicePixelRatio); + this._refreshDrawConstants(); + this._refreshColorZonePadding(); + } + + private _refreshDecorations(): void { + if (this._shouldUpdateDimensions) { + this._refreshCanvasDimensions(); + } + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + this._colorZoneStore.clear(); + for (const decoration of this._decorationService.decorations) { + this._colorZoneStore.addDecoration(decoration); + } + this._ctx.lineWidth = 1; + const zones = this._colorZoneStore.zones; + for (const zone of zones) { + if (zone.position !== 'full') { + this._renderColorZone(zone); + } + } + for (const zone of zones) { + if (zone.position === 'full') { + this._renderColorZone(zone); + } + } + this._shouldUpdateDimensions = false; + this._shouldUpdateAnchor = false; + } + + private _renderColorZone(zone: IColorZone): void { + // TODO: Is _decorationElements needed? + + this._ctx.fillStyle = zone.color; + this._ctx.fillRect( + /* x */ drawX[zone.position || 'full'], + /* y */ Math.round( + (this._canvas.height - 1) * // -1 to ensure at least 2px are allowed for decoration on last line + (zone.startBufferLine / this._bufferService.buffers.active.lines.length) - drawHeight[zone.position || 'full'] / 2 + ), + /* w */ drawWidth[zone.position || 'full'], + /* h */ Math.round( + (this._canvas.height - 1) * // -1 to ensure at least 2px are allowed for decoration on last line + ((zone.endBufferLine - zone.startBufferLine) / this._bufferService.buffers.active.lines.length) + drawHeight[zone.position || 'full'] + ) + ); + } + + private _queueRefresh(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void { + this._shouldUpdateDimensions = updateCanvasDimensions || this._shouldUpdateDimensions; + this._shouldUpdateAnchor = updateAnchor || this._shouldUpdateAnchor; + if (this._animationFrame !== undefined) { + return; + } + this._animationFrame = window.requestAnimationFrame(() => { + this._refreshDecorations(); + this._animationFrame = undefined; + }); + } +} diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 629e9436..696b793f 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -4,18 +4,18 @@ */ import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; -import { ICellData } from 'common/Types'; +import { ICellData, IColor } from 'common/Types'; import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IColorSet, IColor } from 'browser/Types'; +import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; -import { IBufferService, IOptionsService } from 'common/services/Services'; -import { throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { channels, color, rgba } from 'browser/Color'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { isPowerlineGlyph, throwIfFalsy } from 'browser/renderer/RendererUtils'; +import { channels, color, rgba } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; @@ -52,7 +52,8 @@ export abstract class BaseRenderLayer implements IRenderLayer { protected _colors: IColorSet, private _rendererId: number, protected readonly _bufferService: IBufferService, - protected readonly _optionsService: IOptionsService + protected readonly _optionsService: IOptionsService, + protected readonly _decorationService: IDecorationService ) { this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); @@ -294,7 +295,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { * @param bold Whether the text is bold. */ protected _drawChars(cell: ICellData, x: number, y: number): void { - const contrastColor = this._getContrastColor(cell); + const contrastColor = this._getContrastColor(cell, x, y); // skip cache right away if we draw in RGB // Note: to avoid bad runtime JoinedCellData will be skipped @@ -325,7 +326,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._currentGlyphIdentifier.bold = !!cell.isBold(); this._currentGlyphIdentifier.dim = !!cell.isDim(); this._currentGlyphIdentifier.italic = !!cell.isItalic(); - const atlasDidDraw = this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop); + + // Don't try cache the glyph if it uses any decoration foreground/background override. + let hasOverrides = false; + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + if (d.backgroundColorRGB || d.foregroundColorRGB) { + hasOverrides = true; + break; + } + } + + const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop); if (!atlasDidDraw) { this._drawUncachedChars(cell, x, y); @@ -427,15 +438,35 @@ export abstract class BaseRenderLayer implements IRenderLayer { return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`; } - private _getContrastColor(cell: CellData): IColor | undefined { - if (this._optionsService.rawOptions.minimumContrastRatio === 1) { + private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined { + // Get any decoration foreground/background overrides, this must be fetched before the early + // exist but applied after inverse + let bgOverride: number | undefined; + let fgOverride: number | undefined; + let isTop = false; + for (const d of this._decorationService.getDecorationsAtCell(x, y)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } + if (d.backgroundColorRGB) { + bgOverride = d.backgroundColorRGB.rgba; + } + if (d.foregroundColorRGB) { + fgOverride = d.foregroundColorRGB.rgba; + } + isTop = d.options.layer === 'top'; + } + + if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) { return undefined; } - // Try get from cache first - const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); - if (adjustedColor !== undefined) { - return adjustedColor || undefined; + if (!bgOverride && !fgOverride) { + // Try get from cache + const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg); + if (adjustedColor !== undefined) { + return adjustedColor || undefined; + } } let fgColor = cell.getFgColor(); @@ -453,13 +484,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { bgColorMode = temp2; } - const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse); + const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse); const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold); - const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.rawOptions.minimumContrastRatio); + let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio); if (!result) { - this._colors.contrastCache.setColor(cell.bg, cell.fg, null); - return undefined; + if (!fgOverride) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, null); + return undefined; + } + // If it was an override and there was no contrast change, set as the result + result = fgOverride; } const color: IColor = { @@ -470,7 +505,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { ), rgba: result }; - this._colors.contrastCache.setColor(cell.bg, cell.fg, color); + if (!bgOverride && !fgOverride) { + this._colors.contrastCache.setColor(cell.bg, cell.fg, color); + } return color; } diff --git a/src/browser/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts index ea419cb2..3fa576a9 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/src/browser/renderer/CursorRenderLayer.ts @@ -8,7 +8,7 @@ import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; -import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; +import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; import { ICoreBrowserService } from 'browser/services/Services'; @@ -40,9 +40,10 @@ export class CursorRenderLayer extends BaseRenderLayer { @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, @ICoreService private readonly _coreService: ICoreService, - @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService); + super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); this._state = { x: 0, y: 0, diff --git a/src/browser/renderer/CustomGlyphs.ts b/src/browser/renderer/CustomGlyphs.ts index 77562790..c2bfc210 100644 --- a/src/browser/renderer/CustomGlyphs.ts +++ b/src/browser/renderer/CustomGlyphs.ts @@ -414,10 +414,10 @@ function drawPatternChar( let b: number; let a: number; if (fillStyle.startsWith('#')) { - r = parseInt(fillStyle.substr(1, 2), 16); - g = parseInt(fillStyle.substr(3, 2), 16); - b = parseInt(fillStyle.substr(5, 2), 16); - a = fillStyle.length > 7 && parseInt(fillStyle.substr(7, 2), 16) || 1; + r = parseInt(fillStyle.slice(1, 3), 16); + g = parseInt(fillStyle.slice(3, 5), 16); + b = parseInt(fillStyle.slice(5, 7), 16); + a = fillStyle.length > 7 && parseInt(fillStyle.slice(7, 9), 16) || 1; } else if (fillStyle.startsWith('rgba')) { ([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e))); } else { diff --git a/src/browser/renderer/LinkRenderLayer.ts b/src/browser/renderer/LinkRenderLayer.ts index 2492f921..15086d9a 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -8,7 +8,7 @@ import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils'; import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; @@ -21,9 +21,10 @@ export class LinkRenderLayer extends BaseRenderLayer { linkifier: ILinkifier, linkifier2: ILinkifier2, @IBufferService bufferService: IBufferService, - @IOptionsService optionsService: IOptionsService + @IOptionsService optionsService: IOptionsService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService); + super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index a58893b4..8dfe09c9 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -14,7 +14,6 @@ import { ICharSizeService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services'; import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IDecorationOptions, IDecoration } from 'xterm'; let nextRendererId = 1; diff --git a/src/browser/renderer/RendererUtils.ts b/src/browser/renderer/RendererUtils.ts index 48fd26a4..3fc2bb3b 100644 --- a/src/browser/renderer/RendererUtils.ts +++ b/src/browser/renderer/RendererUtils.ts @@ -9,3 +9,10 @@ export function throwIfFalsy(value: T | undefined | null): T { } return value; } + +export function isPowerlineGlyph(codepoint: number): boolean { + // Only return true for Powerline symbols which require + // different padding and should be excluded from minimum contrast + // ratio standards + return 0xE0A0 <= codepoint && codepoint <= 0xE0D6; +} diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index 9054e3ca..be911eb9 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -6,7 +6,7 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; import { IColorSet } from 'browser/Types'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; interface ISelectionState { start?: [number, number]; @@ -24,9 +24,10 @@ export class SelectionRenderLayer extends BaseRenderLayer { colors: IColorSet, rendererId: number, @IBufferService bufferService: IBufferService, - @IOptionsService optionsService: IOptionsService + @IOptionsService optionsService: IOptionsService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService); + super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); this._clearState(); } diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts index 33d942ff..ef5a9b62 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/src/browser/renderer/TextRenderLayer.ts @@ -11,7 +11,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; -import { IOptionsService, IBufferService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; @@ -37,9 +37,10 @@ export class TextRenderLayer extends BaseRenderLayer { rendererId: number, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, - @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService + @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, + @IDecorationService decorationService: IDecorationService ) { - super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService); + super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService); this._state = new GridCache(); } @@ -176,6 +177,19 @@ export class TextRenderLayer extends BaseRenderLayer { nextFillStyle = this._colors.ansi[cell.getBgColor()].css; } + // Get any decoration foreground/background overrides, this must be fetched before the early + // exist but applied after inverse + let isTop = false; + for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } + if (d.backgroundColorRGB) { + nextFillStyle = d.backgroundColorRGB.css; + } + isTop = d.options.layer === 'top'; + } + if (prevFillStyle === null) { // This is either the first iteration, or the default background was set. Either way, we // don't need to draw anything. diff --git a/src/browser/renderer/atlas/CharAtlasUtils.ts b/src/browser/renderer/atlas/CharAtlasUtils.ts index be92727a..696c6c12 100644 --- a/src/browser/renderer/atlas/CharAtlasUtils.ts +++ b/src/browser/renderer/atlas/CharAtlasUtils.ts @@ -16,7 +16,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number cursor: undefined, cursorAccent: undefined, selection: undefined, - ansi: [...colors.ansi] + ansi: colors.ansi.slice() }; return { devicePixelRatio: window.devicePixelRatio, diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 118dbcd2..88194615 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -9,9 +9,9 @@ import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; import { LRUMap } from 'browser/renderer/atlas/LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; -import { IColor } from 'browser/Types'; +import { IColor } from 'common/Types'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; -import { color } from 'browser/Color'; +import { color } from 'common/Color'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index ee283399..d15d7eac 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -9,9 +9,9 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { Disposable } from 'common/Lifecycle'; import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; -import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { color } from 'browser/Color'; +import { color } from 'common/Color'; import { removeElementFromParent } from 'browser/Dom'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; @@ -87,11 +87,11 @@ export class DomRenderer extends Disposable implements IRenderer { this._screenElement.appendChild(this._rowContainer); this._screenElement.appendChild(this._selectionContainer); - this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e)); - this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e)); + this.register(this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e))); + this.register(this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e))); - this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)); - this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)); + this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e))); + this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e))); } public dispose(): void { @@ -361,7 +361,6 @@ export class DomRenderer extends Disposable implements IRenderer { for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; rowElement.innerText = ''; - const row = y + this._bufferService.buffer.ydisp; const lineData = this._bufferService.buffer.lines.get(row); const cursorStyle = this._optionsService.rawOptions.cursorStyle; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index f41e5d44..bb511a47 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -10,8 +10,8 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { MockCoreService, MockOptionsService } from 'common/TestUtils.test'; -import { css } from 'browser/Color'; +import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; +import { css } from 'common/Color'; import { MockCharacterJoinerService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { @@ -49,7 +49,8 @@ describe('DomRendererRowFactory', () => { } as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }), - new MockCoreService() + new MockCoreService(), + new MockDecorationService() ); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index fda800ae..bf3939e8 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -3,15 +3,16 @@ * @license MIT */ -import { IBufferLine } from 'common/Types'; +import { IBufferLine, ICellData, IColor } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; -import { ICoreService, IOptionsService } from 'common/services/Services'; -import { color, rgba } from 'browser/Color'; -import { IColorSet, IColor } from 'browser/Types'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { color, rgba } from 'common/Color'; +import { IColorSet } from 'browser/Types'; import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; +import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; @@ -32,7 +33,8 @@ export class DomRendererRowFactory { private _colors: IColorSet, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, @IOptionsService private readonly _optionsService: IOptionsService, - @ICoreService private readonly _coreService: ICoreService + @ICoreService private readonly _coreService: ICoreService, + @IDecorationService private readonly _decorationService: IDecorationService ) { } @@ -171,6 +173,28 @@ export class DomRendererRowFactory { bgColorMode = temp2; } + // Apply any decoration foreground/background overrides, this must happen after inverse has + // been applied + let bgOverride: IColor | undefined; + let fgOverride: IColor | undefined; + let isTop = false; + for (const d of this._decorationService.getDecorationsAtCell(x, row)) { + if (d.options.layer !== 'top' && isTop) { + continue; + } + if (d.backgroundColorRGB) { + bgColorMode = Attributes.CM_RGB; + bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + bgOverride = d.backgroundColorRGB; + } + if (d.foregroundColorRGB) { + fgColorMode = Attributes.CM_RGB; + fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + fgOverride = d.foregroundColorRGB; + } + isTop = d.options.layer === 'top'; + } + // Foreground switch (fgColorMode) { case Attributes.CM_P16: @@ -178,7 +202,7 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) { + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell, undefined, undefined)) { charElement.classList.add(`xterm-fg-${fg}`); } break; @@ -188,13 +212,13 @@ export class DomRendererRowFactory { (fg >> 8) & 0xFF, (fg ) & 0xFF ); - if (!this._applyMinimumContrast(charElement, this._colors.background, color)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, color, cell, bgOverride, fgOverride)) { this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`); } break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) { + if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell, undefined, undefined)) { if (isInverse) { charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } @@ -208,7 +232,7 @@ export class DomRendererRowFactory { charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: - this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`); + this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`); break; case Attributes.CM_DEFAULT: default: @@ -224,18 +248,23 @@ export class DomRendererRowFactory { return fragment; } - private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean { - if (this._optionsService.rawOptions.minimumContrastRatio === 1) { + private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean { + if (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode())) { return false; } - // Try get from cache first - let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + // Try get from cache first, only use the cache when there are no decoration overrides + let adjustedColor: IColor | undefined | null = undefined; + if (!bgOverride || !fgOverride) { + adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg); + } // Calculate and store in cache if (adjustedColor === undefined) { - adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.rawOptions.minimumContrastRatio); - this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio); + if (!bgOverride || !fgOverride) { + this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null); + } } if (adjustedColor) { diff --git a/src/browser/selection/SelectionModel.test.ts b/src/browser/selection/SelectionModel.test.ts index 410902d7..5ce3e316 100644 --- a/src/browser/selection/SelectionModel.test.ts +++ b/src/browser/selection/SelectionModel.test.ts @@ -116,6 +116,12 @@ describe('SelectionModel', () => { model.selectionStartLength = 4; assert.deepEqual(model.finalSelectionEnd, [2, 3]); }); + it('should return the end on a different row when start + length overflows onto a following row with selectionEnd inbetween', () => { + model.selectionStart = [78, 2]; + model.selectionEnd = [79, 2]; + model.selectionStartLength = 4; + assert.deepEqual(model.finalSelectionEnd, [2, 3]); + }); it('should return selection end if selection end is after selection start + length', () => { model.selectionStart = [2, 2]; model.selectionStartLength = 2; diff --git a/src/browser/selection/SelectionModel.ts b/src/browser/selection/SelectionModel.ts index 1d84446a..6c8abbfd 100644 --- a/src/browser/selection/SelectionModel.ts +++ b/src/browser/selection/SelectionModel.ts @@ -92,7 +92,12 @@ export class SelectionModel { if (this.selectionStartLength) { // Select the larger of the two when start and end are on the same line if (this.selectionEnd[1] === this.selectionStart[1]) { - return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]]; + // Keep the whole wrapped word/line selected if the content wraps multiple lines + const startPlusLength = this.selectionStart[0] + this.selectionStartLength; + if (startPlusLength > this._bufferService.cols) { + return [startPlusLength % this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols)]; + } + return [Math.max(startPlusLength, this.selectionEnd[0]), this.selectionEnd[1]]; } } return this.selectionEnd; diff --git a/src/browser/services/DecorationService.ts b/src/browser/services/DecorationService.ts deleted file mode 100644 index ed3c7224..00000000 --- a/src/browser/services/DecorationService.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright (c) 2022 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IDecorationService, IRenderService } from 'browser/services/Services'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; -import { IBufferService, IInstantiationService } from 'common/services/Services'; -import { IDecorationOptions, IDecoration, IMarker } from 'xterm'; - -export class DecorationService extends Disposable implements IDecorationService { - - private readonly _decorations: Decoration[] = []; - private _container: HTMLElement | undefined; - private _screenElement: HTMLElement | undefined; - private _renderService: IRenderService | undefined; - private _animationFrame: number | undefined; - - constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { super(); } - - public attachToDom(screenElement: HTMLElement, renderService: IRenderService): void { - this._renderService = renderService; - this._screenElement = screenElement; - this._container = document.createElement('div'); - this._container.classList.add('xterm-decoration-container'); - screenElement.appendChild(this._container); - this.register(this._renderService.onRenderedBufferChange(() => this.refresh())); - this.register(this._renderService.onDimensionsChange(() => this.refresh(true))); - } - - public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { - if (decorationOptions.marker.isDisposed || !this._container) { - return undefined; - } - const decoration = this._instantiationService.createInstance(Decoration, decorationOptions, this._container); - this._decorations.push(decoration); - decoration.onDispose(() => this._decorations.splice(this._decorations.indexOf(decoration), 1)); - this._queueRefresh(); - return decoration; - } - - private _queueRefresh(): void { - if (this._animationFrame !== undefined) { - return; - } - this._animationFrame = window.requestAnimationFrame(() => { - this.refresh(); - this._animationFrame = undefined; - }); - } - - public refresh(shouldRecreate?: boolean): void { - if (!this._renderService) { - return; - } - for (const decoration of this._decorations) { - decoration.render(this._renderService, shouldRecreate); - } - } - - public dispose(): void { - for (const decoration of this._decorations) { - decoration.dispose(); - } - if (this._screenElement && this._container && this._screenElement.contains(this._container)) { - this._screenElement.removeChild(this._container); - } - } -} -export class Decoration extends Disposable implements IDecoration { - private readonly _marker: IMarker; - private _element: HTMLElement | undefined; - - public isDisposed: boolean = false; - - public get element(): HTMLElement | undefined { return this._element; } - public get marker(): IMarker { return this._marker; } - - private _onDispose = new EventEmitter(); - public get onDispose(): IEvent { return this._onDispose.event; } - - private _onRender = new EventEmitter(); - public get onRender(): IEvent { return this._onRender.event; } - - public x: number; - public anchor: 'left' | 'right'; - public width: number; - public height: number; - - constructor( - options: IDecorationOptions, - private readonly _container: HTMLElement, - @IBufferService private readonly _bufferService: IBufferService - ) { - super(); - this.x = options.x ?? 0; - this._marker = options.marker; - this._marker.onDispose(() => this.dispose()); - this.anchor = options.anchor || 'left'; - this.width = options.width || 1; - this.height = options.height || 1; - } - - public render(renderService: IRenderService, shouldRecreate?: boolean): void { - if (!this._element || shouldRecreate) { - this._createElement(renderService, shouldRecreate); - } - if (this._container && this._element && !this._container.contains(this._element)) { - this._container.append(this._element); - } - this._refreshStyle(renderService); - if (this._element) { - this._onRender.fire(this._element); - } - } - - private _createElement(renderService: IRenderService, shouldRecreate?: boolean): void { - if (shouldRecreate && this._element && this._container.contains(this._element)) { - this._container.removeChild(this._element); - } - this._element = document.createElement('div'); - this._element.classList.add('xterm-decoration'); - this._element.style.width = `${this.width * renderService.dimensions.actualCellWidth}px`; - this._element.style.height = `${this.height * renderService.dimensions.actualCellHeight}px`; - this._element.style.top = `${(this.marker.line - this._bufferService.buffers.active.ydisp) * renderService.dimensions.actualCellHeight}px`; - this._element.style.lineHeight = `${renderService.dimensions.actualCellHeight}px`; - - if (this.x && this.x > this._bufferService.cols) { - // exceeded the container width, so hide - this._element.style.display = 'none'; - } - if (this.anchor === 'right') { - this._element.style.right = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; - } else { - this._element.style.left = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : ''; - } - } - - private _refreshStyle(renderService: IRenderService): void { - if (!this._element) { - return; - } - const line = this.marker.line - this._bufferService.buffers.active.ydisp; - if (line < 0 || line > this._bufferService.rows) { - // outside of viewport - this._element.style.display = 'none'; - } else { - this._element.style.top = `${line * renderService.dimensions.actualCellHeight}px`; - this._element.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; - } - } - - public override dispose(): void { - if (this.isDisposed) { - return; - } - if (this._element && this._container.contains(this._element)) { - this._container.removeChild(this._element); - } - this.isDisposed = true; - this._onDispose.fire(); - } -} diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index da458abc..b2e619fe 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -10,7 +10,7 @@ import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IRenderDebouncer } from 'browser/Types'; -import { IOptionsService, IBufferService } from 'common/services/Services'; +import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; interface ISelectionState { @@ -39,8 +39,10 @@ export class RenderService extends Disposable implements IRenderService { private _onDimensionsChange = new EventEmitter(); public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } + private _onRenderedBufferChange = new EventEmitter<{ start: number, end: number }>(); + public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRenderedBufferChange.event; } private _onRender = new EventEmitter<{ start: number, end: number }>(); - public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRender.event; } + public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; } @@ -52,6 +54,7 @@ export class RenderService extends Disposable implements IRenderService { screenElement: HTMLElement, @IOptionsService optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IDecorationService decorationService: IDecorationService, @IBufferService bufferService: IBufferService ) { super(); @@ -70,6 +73,12 @@ export class RenderService extends Disposable implements IRenderService { this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); + // Do a full refresh whenever any decoration is added or removed. This may not actually result + // in changes but since decorations should be used sparingly or added/removed all in the same + // frame this should have minimal performance impact. + this.register(decorationService.onDecorationRegistered(() => this._fullRefresh())); + this.register(decorationService.onDecorationRemoved(() => this._fullRefresh())); + // No need to register this as renderer is explicitly disposed in RenderService.dispose this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); @@ -122,8 +131,9 @@ export class RenderService extends Disposable implements IRenderService { // Fire render event only if it was not a redraw if (!this._isNextRenderRedrawOnly) { - this._onRender.fire({ start, end }); + this._onRenderedBufferChange.fire({ start, end }); } + this._onRender.fire({ start, end }); this._isNextRenderRedrawOnly = true; } diff --git a/src/browser/services/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index 514d5803..d829e394 100644 --- a/src/browser/services/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -340,6 +340,9 @@ describe('SelectionService', () => { buffer.lines.set(0, stringToRow('foo bar')); selectionService.selectLineAt(0); assert.equal(selectionService.selectionText, 'foo bar', 'The selected text is correct'); + assert.deepEqual(selectionService.model.selectionStart, [0, 0]); + assert.deepEqual(selectionService.model.selectionEnd, undefined); + assert.deepEqual(selectionService.model.selectionStartLength, 20); assert.deepEqual(selectionService.model.finalSelectionStart, [0, 0]); assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 0], 'The actual selection spans the entire column'); }); @@ -350,6 +353,9 @@ describe('SelectionService', () => { buffer.lines.set(1, line2); selectionService.selectLineAt(0); assert.equal(selectionService.selectionText, 'foobar', 'The selected text is correct'); + assert.deepEqual(selectionService.model.selectionStart, [0, 0]); + assert.deepEqual(selectionService.model.selectionEnd, undefined); + assert.deepEqual(selectionService.model.selectionStartLength, 40); assert.deepEqual(selectionService.model.finalSelectionStart, [0, 0]); assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 1], 'The actual selection spans the entire column'); }); diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 1ea2395d..53020b53 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -11,7 +11,7 @@ import { SelectionModel } from 'browser/selection/SelectionModel'; import { CellData } from 'common/buffer/CellData'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { IMouseService, ISelectionService, IRenderService } from 'browser/services/Services'; -import { ILinkifier2 } from 'browser/Types'; +import { IBufferRange, ILinkifier2 } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; import { moveToCellSequence } from 'browser/input/MoveToCell'; @@ -1002,8 +1002,12 @@ export class SelectionService extends Disposable implements ISelectionService { */ protected _selectLineAt(line: number): void { const wrappedRange = this._bufferService.buffer.getWrappedRangeForLine(line); + const range: IBufferRange = { + start: { x: 0, y: wrappedRange.first }, + end: { x: this._bufferService.cols - 1, y: wrappedRange.last } + }; this._model.selectionStart = [0, wrappedRange.first]; - this._model.selectionEnd = [this._bufferService.cols, wrappedRange.last]; - this._model.selectionStartLength = 0; + this._model.selectionEnd = undefined; + this._model.selectionStartLength = getRangeLength(range, this._bufferService.cols); } } diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 7faf3f0f..7191d0ed 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -50,6 +50,10 @@ export interface IRenderService extends IDisposable { * or selections are rendered. */ onRenderedBufferChange: IEvent<{ start: number, end: number }>; + /** + * Fires on render + */ + onRender: IEvent<{ start: number, end: number }>; onRefreshRequest: IEvent<{ start: number, end: number }>; dimensions: IRenderDimensions; @@ -115,11 +119,3 @@ export interface ICharacterJoinerService { deregister(joinerId: number): boolean; getJoinedCharacters(row: number): [number, number][]; } - - -export const IDecorationService = createDecorator('DecorationService'); -export interface IDecorationService extends IDisposable { - registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; - refresh(): void; - attachToDom(screenElement: HTMLElement, renderService: IRenderService, bufferService: IBufferService): void; -} diff --git a/src/browser/Color.test.ts b/src/common/Color.test.ts similarity index 99% rename from src/browser/Color.test.ts rename to src/common/Color.test.ts index 0d410930..f16e6ffb 100644 --- a/src/browser/Color.test.ts +++ b/src/common/Color.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'browser/Color'; +import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'common/Color'; describe('Color', () => { diff --git a/src/browser/Color.ts b/src/common/Color.ts similarity index 95% rename from src/browser/Color.ts rename to src/common/Color.ts index 32e311db..b197cd66 100644 --- a/src/browser/Color.ts +++ b/src/common/Color.ts @@ -3,8 +3,7 @@ * @license MIT */ -import { IColor } from 'browser/Types'; -import { IColorRGB } from 'common/Types'; +import { IColor, IColorRGB } from 'common/Types'; /** * Helper functions where the source type is "channels" (individual color channels as numbers). @@ -173,13 +172,13 @@ export namespace rgba { let fgR = (fgRgba >> 24) & 0xFF; let fgG = (fgRgba >> 16) & 0xFF; let fgB = (fgRgba >> 8) & 0xFF; - let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) { // Reduce by 10% until the ratio is hit fgR -= Math.max(0, Math.ceil(fgR * 0.1)); fgG -= Math.max(0, Math.ceil(fgG * 0.1)); fgB -= Math.max(0, Math.ceil(fgB * 0.1)); - cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); } return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } @@ -193,13 +192,13 @@ export namespace rgba { let fgR = (fgRgba >> 24) & 0xFF; let fgG = (fgRgba >> 16) & 0xFF; let fgB = (fgRgba >> 8) & 0xFF; - let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) { // Increase by 10% until the ratio is hit fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1)); fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1)); fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1)); - cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB)); + cr = contrastRatio(rgb.relativeLuminance2(fgR, fgG, fgB), rgb.relativeLuminance2(bgR, bgG, bgB)); } return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } diff --git a/src/common/SortedList.test.ts b/src/common/SortedList.test.ts new file mode 100644 index 00000000..ecafdb8f --- /dev/null +++ b/src/common/SortedList.test.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { SortedList } from 'common/SortedList'; + +const deepStrictEqual = assert.deepStrictEqual; + +describe('SortedList', () => { + let list: SortedList; + function assertList(expected: number[]): void { + deepStrictEqual(Array.from(list.values()), expected); + } + + beforeEach(() => { + list = new SortedList(e => e); + }); + + describe('insert', () => { + it('should maintain sorted values', () => { + list.insert(10); + assertList([10]); + list.insert(8); + assertList([8, 10]); + list.insert(15); + assertList([8, 10, 15]); + list.insert(2); + assertList([2, 8, 10, 15]); + list.insert(1); + assertList([1, 2, 8, 10, 15]); + list.insert(6); + assertList([1, 2, 6, 8, 10, 15]); + }); + it('should allow duplicates of the same key', () => { + list.insert(5); + assertList([5]); + list.insert(5); + assertList([5, 5]); + list.insert(8); + assertList([5, 5, 8]); + list.insert(5); + assertList([5, 5, 5, 8]); + list.insert(8); + assertList([5, 5, 5, 8, 8]); + list.insert(6); + assertList([5, 5, 5, 6, 8, 8]); + }); + }); + it('delete', () => { + list.insert(1); + list.insert(2); + list.insert(4); + list.insert(3); + list.insert(5); + assertList([1, 2, 3, 4, 5]); + list.delete(1); + assertList([2, 3, 4, 5]); + list.delete(3); + assertList([2, 4, 5]); + list.delete(4); + assertList([2, 5]); + list.delete(5); + assertList([2]); + list.delete(2); + assertList([]); + }); + it('getKeyIterator', () => { + list.insert(5); + list.insert(5); + list.insert(8); + list.insert(5); + list.insert(8); + list.insert(6); + assertList([5, 5, 5, 6, 8, 8]); + deepStrictEqual(Array.from(list.getKeyIterator(1)), []); + deepStrictEqual(Array.from(list.getKeyIterator(5)), [5, 5, 5]); + deepStrictEqual(Array.from(list.getKeyIterator(6)), [6]); + deepStrictEqual(Array.from(list.getKeyIterator(8)), [8, 8]); + deepStrictEqual(Array.from(list.getKeyIterator(9)), []); + }); + it('clear', () => { + list.insert(1); + list.insert(2); + list.insert(4); + list.insert(3); + list.insert(5); + list.clear(); + assertList([]); + }); + it('custom key', () => { + const customList = new SortedList<{ key: number }>(e => e.key); + customList.insert({ key: 5 }); + customList.insert({ key: 2 }); + customList.insert({ key: 10 }); + customList.insert({ key: 5 }); + customList.insert({ key: 6 }); + deepStrictEqual(Array.from(customList.values()), [ + { key: 2 }, + { key: 5 }, + { key: 5 }, + { key: 6 }, + { key: 10 } + ]); + }); +}); diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts new file mode 100644 index 00000000..051c6702 --- /dev/null +++ b/src/common/SortedList.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * A generic list that is maintained in sorted order and allows values with duplicate keys. This + * list is based on binary search and as such locating a key will take O(log n) amortized, this + * includes the by key iterator. + */ +export class SortedList { + private readonly _array: T[] = []; + + constructor( + private readonly _getKey: (value: T) => number + ) { + } + + public clear(): void { + this._array.length = 0; + } + + public insert(value: T): void { + if (this._array.length === 0) { + this._array.push(value); + return; + } + const i = this._search(this._getKey(value), 0, this._array.length - 1); + this._array.splice(i, 0, value); + } + + public delete(value: T): boolean { + if (this._array.length === 0) { + return false; + } + const key = this._getKey(value); + let i = this._search(key, 0, this._array.length - 1); + if (this._getKey(this._array[i]) !== key) { + return false; + } + do { + if (this._array[i] === value) { + this._array.splice(i, 1); + return true; + } + } while (++i < this._array.length && this._getKey(this._array[i]) === key); + return false; + } + + public *getKeyIterator(key: number): IterableIterator { + if (this._array.length === 0) { + return; + } + let i = this._search(key, 0, this._array.length - 1); + if (i < 0 || i >= this._array.length) { + return; + } + if (this._getKey(this._array[i]) !== key) { + return; + } + do { + yield this._array[i]; + } while (++i < this._array.length && this._getKey(this._array[i]) === key); + } + + public values(): IterableIterator { + return this._array.values(); + } + + private _search(key: number, min: number, max: number): number { + if (max < min) { + return min; + } + let mid = Math.floor((min + max) / 2); + if (this._getKey(this._array[mid]) > key) { + return this._search(key, min, mid - 1); + } + if (this._getKey(this._array[mid]) < key) { + return this._search(key, mid + 1, max); + } + // Value found! Since keys can be duplicates, move the result index back to the lowest index + // that matches the key. + while (mid > 0 && this._getKey(this._array[mid - 1]) === key) { + mid--; + } + return mid; + } +} diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 58f6c709..11d9a8c5 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -11,6 +11,7 @@ import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; +import { IDecorationOptions, IDecoration } from 'xterm'; export class MockBufferService implements IBufferService { public serviceBrand: any; @@ -158,3 +159,15 @@ export class MockUnicodeService implements IUnicodeService { throw new Error('Method not implemented.'); } } + +export class MockDecorationService implements IDecorationService { + public serviceBrand: any; + public get decorations(): IterableIterator { return [].values(); } + public onDecorationRegistered = new EventEmitter().event; + public onDecorationRemoved = new EventEmitter().event; + public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; } + public reset(): void { } + public *getDecorationsAtLine(line: number): IterableIterator { } + public *getDecorationsAtCell(x: number, line: number): IterableIterator { } + public dispose(): void { } +} diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index fee426e1..c48b23ea 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -102,6 +102,11 @@ 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 +} export type IColorRGB = [number, number, number]; export interface IExtendedAttrs { diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 7596fef3..ab295784 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -585,20 +585,32 @@ export class Buffer implements IBuffer { return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; } - public clearMarkers(y?: number): void { + /** + * Clears markers on single line. + * @param y The line to clear. + */ + public clearMarkers(y: number): void { this._isClearing = true; - if (y !== undefined) { - for (let i = 0; i < this.markers.length; i++) { - if (this.markers[i].line === y) { - this.markers[i].dispose(); - this.markers.splice(i--, 1); - } + for (let i = 0; i < this.markers.length; i++) { + if (this.markers[i].line === y) { + this.markers[i].dispose(); + this.markers.splice(i--, 1); } - } else { - for (const marker of this.markers) { - marker.dispose(); + } + this._isClearing = false; + } + + /** + * Clears markers on all lines except for those on a particular line. + * @param excludeY The line to exclude. + */ + public clearAllMarkers(excludeY: number): void { + this._isClearing = true; + for (let i = 0; i < this.markers.length; i++) { + if (this.markers[i].line !== excludeY) { + this.markers[i].dispose(); + this.markers.splice(i--, 1); } - this.markers = []; } this._isClearing = false; } diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index 36b70b7f..f26b4b26 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -45,7 +45,8 @@ export interface IBuffer { getNullCell(attr?: IAttributeData): ICellData; getWhitespaceCell(attr?: IAttributeData): ICellData; addMarker(y: number): IMarker; - clearMarkers(y?: number): void; + clearMarkers(y: number): void; + clearAllMarkers(excludeY: number): void; } export interface IBufferSet extends IDisposable { diff --git a/src/common/data/EscapeSequences.ts b/src/common/data/EscapeSequences.ts index e35f01dd..0e034620 100644 --- a/src/common/data/EscapeSequences.ts +++ b/src/common/data/EscapeSequences.ts @@ -148,3 +148,6 @@ export namespace C1 { /** Application Program Command */ export const APC = '\x9f'; } +export namespace C1_ESCAPED { + export const ST = `${C0.ESC}\\`; +} diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index b4b3dce4..6d916a38 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -230,6 +230,8 @@ export function evaluateKeyboardEvent( // page up if (ev.shiftKey) { result.type = KeyboardResultType.PAGE_UP; + } else if (ev.ctrlKey) { + result.key = C0.ESC + '[5;' + (modifiers + 1) + '~'; } else { result.key = C0.ESC + '[5~'; } @@ -238,6 +240,8 @@ export function evaluateKeyboardEvent( // page down if (ev.shiftKey) { result.type = KeyboardResultType.PAGE_DOWN; + } else if (ev.ctrlKey) { + result.key = C0.ESC + '[6;' + (modifiers + 1) + '~'; } else { result.key = C0.ESC + '[6~'; } diff --git a/src/common/input/XParseColor.ts b/src/common/input/XParseColor.ts index 8c023a38..fd23ec4b 100644 --- a/src/common/input/XParseColor.ts +++ b/src/common/input/XParseColor.ts @@ -5,7 +5,7 @@ // 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits) -const RGB_REX = /^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/; +const RGB_REX = /^([\da-f])\/([\da-f])\/([\da-f])$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/; // '#...' rule - matching any hex digits const HASH_REX = /^[\da-f]+$/; diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts new file mode 100644 index 00000000..755f13b3 --- /dev/null +++ b/src/common/services/DecorationService.ts @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { css } from 'common/Color'; +import { EventEmitter } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IDecorationService, IInternalDecoration } from 'common/services/Services'; +import { SortedList } from 'common/SortedList'; +import { IColor } from 'common/Types'; +import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm'; + +export class DecorationService extends Disposable implements IDecorationService { + public serviceBrand: any; + + /** + * A list of all decorations, sorted by the marker's line value. This relies on the fact that + * while marker line values do change, they should all change by the same amount so this should + * never become out of order. + */ + private readonly _decorations: SortedList = new SortedList(e => e.marker.line); + + private _onDecorationRegistered = this.register(new EventEmitter()); + public get onDecorationRegistered(): IEvent { return this._onDecorationRegistered.event; } + private _onDecorationRemoved = this.register(new EventEmitter()); + public get onDecorationRemoved(): IEvent { return this._onDecorationRemoved.event; } + + public get decorations(): IterableIterator { return this._decorations.values(); } + + constructor() { + super(); + } + + public registerDecoration(options: IDecorationOptions): IDecoration | undefined { + if (options.marker.isDisposed) { + return undefined; + } + const decoration = new Decoration(options); + if (decoration) { + const markerDispose = decoration.marker.onDispose(() => decoration.dispose()); + decoration.onDispose(() => { + if (decoration) { + if (this._decorations.delete(decoration)) { + this._onDecorationRemoved.fire(decoration); + } + markerDispose.dispose(); + } + }); + this._decorations.insert(decoration); + this._onDecorationRegistered.fire(decoration); + } + return decoration; + } + + public reset(): void { + for (const d of this._decorations.values()) { + d.dispose(); + } + this._decorations.clear(); + } + + public *getDecorationsAtLine(line: number): IterableIterator { + return this._decorations.getKeyIterator(line); + } + + public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator { + let xmin = 0; + let xmax = 0; + for (const d of this._decorations.getKeyIterator(line)) { + xmin = d.options.x ?? 0; + xmax = xmin + (d.options.width ?? 1); + if (x >= xmin && x < xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) { + yield d; + } + } + } + + public dispose(): void { + for (const d of this._decorations.values()) { + this._onDecorationRemoved.fire(d); + } + this.reset(); + } +} + +class Decoration extends Disposable implements IInternalDecoration { + public readonly marker: IMarker; + public element: HTMLElement | undefined; + public isDisposed: boolean = false; + + public readonly onRenderEmitter = this.register(new EventEmitter()); + public readonly onRender = this.onRenderEmitter.event; + private _onDispose = this.register(new EventEmitter()); + public readonly onDispose = this._onDispose.event; + + private _cachedBg: IColor | undefined | null = null; + public get backgroundColorRGB(): IColor | undefined { + if (this._cachedBg === null) { + if (this.options.backgroundColor) { + this._cachedBg = css.toColor(this.options.backgroundColor); + } else { + this._cachedBg = undefined; + } + } + return this._cachedBg; + } + + private _cachedFg: IColor | undefined | null = null; + public get foregroundColorRGB(): IColor | undefined { + if (this._cachedFg === null) { + if (this.options.foregroundColor) { + this._cachedFg = css.toColor(this.options.foregroundColor); + } else { + this._cachedFg = undefined; + } + } + return this._cachedFg; + } + + constructor( + public readonly options: IDecorationOptions + ) { + super(); + this.marker = options.marker; + if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) { + this.options.overviewRulerOptions.position = 'full'; + } + } + + public override dispose(): void { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + this._onDispose.fire(); + super.dispose(); + } +} diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 43fe9981..4008a431 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -52,7 +52,8 @@ export const DEFAULT_OPTIONS: Readonly = { altClickMovesCursor: true, convertEol: false, termName: 'xterm', - cancelEvents: false + cancelEvents: false, + overviewRulerWidth: undefined }; const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 90dca988..c3190210 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { IEvent } from 'common/EventEmitter'; +import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; +import { IDecorationOptions, IDecoration } from 'xterm'; export const IBufferService = createDecorator('BufferService'); export interface IBufferService { @@ -245,6 +246,7 @@ export interface ITerminalOptions { windowsMode: boolean; windowOptions: IWindowOptions; wordSeparator: string; + overviewRulerWidth?: number; [key: string]: any; cancelEvents: boolean; @@ -298,3 +300,23 @@ export interface IUnicodeVersionProvider { readonly version: string; wcwidth(ucs: number): 0 | 1 | 2; } + +export const IDecorationService = createDecorator('DecorationService'); +export interface IDecorationService extends IDisposable { + serviceBrand: undefined; + readonly decorations: IterableIterator; + readonly onDecorationRegistered: IEvent; + readonly onDecorationRemoved: IEvent; + registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; + reset(): void; + /** Iterates over the decorations at a line (in no particular order). */ + getDecorationsAtLine(line: number): IterableIterator; + /** Iterates over the decorations at a cell (in no particular order). */ + getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator; +} +export interface IInternalDecoration extends IDecoration { + readonly options: IDecorationOptions; + readonly backgroundColorRGB: IColor | undefined; + readonly foregroundColorRGB: IColor | undefined; + readonly onRenderEmitter: IEventEmitter; +} diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json index e08695d7..00cecc06 100644 --- a/src/tsconfig-library-base.json +++ b/src/tsconfig-library-base.json @@ -4,6 +4,7 @@ "composite": true, "strict": true, "declarationMap": true, - "experimentalDecorators": true + "experimentalDecorators": true, + "downlevelIteration": true } } diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 4d3a15dd..c8cbbf66 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -362,7 +362,7 @@ describe('InputHandler Integration Tests', function(): void { await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), []); }); it('14 - GetWinSizePixels', async function(): Promise { - await page.evaluate(`window.term.setOption('windowOptions', { getWinSizePixels: true }); `); + await page.evaluate(`window.term.options.windowOptions = { getWinSizePixels: true }; `); await page.evaluate(`(() => { window._stack = []; const _h = window.term.onData(data => window._stack.push(data)); @@ -373,7 +373,7 @@ describe('InputHandler Integration Tests', function(): void { await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), [`\x1b[4;${d.height};${d.width}t`]); }); it('16 - GetCellSizePixels', async function(): Promise { - await page.evaluate(`window.term.setOption('windowOptions', { getCellSizePixels: true }); `); + await page.evaluate(`window.term.options.windowOptions = { getCellSizePixels: true }; `); await page.evaluate(`(() => { window._stack = []; const _h = window.term.onData(data => window._stack.push(data)); @@ -399,13 +399,13 @@ describe('InputHandler Integration Tests', function(): void { }); it('query single color', async () => { await writeSync(page, '\x1b]4;0;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\']); await writeSync(page, '\x1b]4;77;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x1b\\']); }); it('query multiple colors', async () => { await writeSync(page, '\x1b]4;0;?;77;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x1b\\']); }); it('set & query single color', async () => { await writeSync(page, '\x1b]4;0;?\x07'); @@ -413,10 +413,10 @@ describe('InputHandler Integration Tests', function(): void { assert.deepEqual(await page.evaluate('window._recordedData'), restore); // set new color & query await writeSync(page, '\x1b]4;0;rgb:01/02/03\x07\x1b]4;0;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x1b\\']); // restore should set old color await writeSync(page, restore[0] + '\x1b]4;0;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x07', restore[0]]); + assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x1b\\', restore[0]]); }); it('query & set colors mixed', async () => { await writeSync(page, '\x1b]4;0;?;77;?\x07'); @@ -424,11 +424,11 @@ describe('InputHandler Integration Tests', function(): void { await page.evaluate('window._recordedData.length = 0;'); // mixed call - change 0, query 43, change 77 await writeSync(page, '\x1b]4;0;rgb:01/02/03;43;?;77;#aabbcc\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;43;rgb:0000/d7d7/afaf\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;43;rgb:0000/d7d7/afaf\x1b\\']); await page.evaluate('window._recordedData.length = 0;'); // query new values for 0 + 77 await writeSync(page, '\x1b]4;0;?;77;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:0101/0202/0303\x07', '\x1b]4;77;rgb:aaaa/bbbb/cccc\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:0101/0202/0303\x1b\\', '\x1b]4;77;rgb:aaaa/bbbb/cccc\x1b\\']); await page.evaluate('window._recordedData.length = 0;'); // restore old values for 0 + 77 await writeSync(page, restore[0] + restore[1] + '\x1b]4;0;?;77;?\x07'); @@ -451,10 +451,10 @@ describe('InputHandler Integration Tests', function(): void { await writeSync(page, `\x1b]4;${i};?\x07`); const restore: string[] = await page.evaluate('window._recordedData'); await writeSync(page, `\x1b]4;${i};rgb:01/02/03\x07\x1b]4;${i};?\x07`); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x07`]); + assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x1b\\`]); // restore slot color await writeSync(page, `\x1b]104;${i}\x07\x1b]4;${i};?\x07`); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x07`, restore[0]]); + assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x1b\\`, restore[0]]); await page.evaluate('window._recordedData.length = 0;'); } }); @@ -491,62 +491,62 @@ describe('InputHandler Integration Tests', function(): void { }); it('query FG color', async () => { await writeSync(page, '\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); }); it('query BG color', async () => { await writeSync(page, '\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x1b\\']); }); it('query FG & BG color in one call', async () => { await writeSync(page, '\x1b]10;?;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07', '\x1b]11;rgb:0000/0000/0000\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\', '\x1b]11;rgb:0000/0000/0000\x1b\\']); }); it('set & query FG', async () => { await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\']); await writeSync(page, '\x1b]10;#ffffff\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07', '\x1b]10;rgb:ffff/ffff/ffff\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\', '\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); }); it('set & query BG', async () => { await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\']); await writeSync(page, '\x1b]11;#000000\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07', '\x1b]11;rgb:0000/0000/0000\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\', '\x1b]11;rgb:0000/0000/0000\x1b\\']); }); it('set & query cursor color', async () => { await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\']); await writeSync(page, '\x1b]12;#ffffff\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07', '\x1b]12;rgb:ffff/ffff/ffff\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\', '\x1b]12;rgb:ffff/ffff/ffff\x1b\\']); }); it('set & query FG & BG color in one call', async () => { await writeSync(page, '\x1b]10;#123456;rgb:aa/bb/cc\x07\x1b]10;?;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1212/3434/5656\x07', '\x1b]11;rgb:aaaa/bbbb/cccc\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1212/3434/5656\x1b\\', '\x1b]11;rgb:aaaa/bbbb/cccc\x1b\\']); await writeSync(page, '\x1b]10;#ffffff;#000000\x07'); }); it('OSC 110: restore FG color', async () => { await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\']); await page.evaluate('window._recordedData.length = 0;'); // restore await writeSync(page, '\x1b]110\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); }); it('OSC 111: restore BG color', async () => { await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\']); await page.evaluate('window._recordedData.length = 0;'); // restore await writeSync(page, '\x1b]111\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x1b\\']); }); it('OSC 112: restore cursor color', async () => { await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\']); await page.evaluate('window._recordedData.length = 0;'); // restore await writeSync(page, '\x1b]112\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:ffff/ffff/ffff\x07']); + assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:ffff/ffff/ffff\x1b\\']); }); }); }); diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index ebeb680f..16a9b32c 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -229,7 +229,7 @@ describe('Mouse Tracking Tests', async () => { window.calls = []; window.term.onData(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); window.term.onBinary(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); - window.term.setOption('fontSize', ${fontSize}); + window.term.options.fontSize = ${fontSize}; window.term.resize(${cols}, ${rows}); `); }); diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index ee6a0cfa..b1582be8 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -574,7 +574,7 @@ describe('API Integration Tests', function(): void { await page.evaluate(`window.term.scrollLines(10)`); await page.evaluate(`window.term.addMarker(3)`); await page.evaluate(`window.term.addMarker(4)`); - await page.evaluate(` + await page.evaluate(` for (let i = 0; i < window.term.markers.length; ++i) { const marker = window.term.markers[i]; marker.onDispose(() => window.disposeStack.push(marker)); @@ -732,48 +732,60 @@ describe('API Integration Tests', function(): void { }); describe('registerDecoration', () => { - it('should register decorations and render them', async () => { - await openTerminal(page); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await page.evaluate(`window.marker1 = window.term.addMarker(1)`); - await page.evaluate(`window.marker2 = window.term.addMarker(2)`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`); - await page.evaluate(`window.term.resize(10, 5)`); - assert.equal(await page.evaluate(`document.querySelectorAll('.xterm-screen .xterm-decoration').length`), 2); + describe('bufferDecorations', () => { + it('should register decorations and render them when terminal open is called', async () => { + await page.evaluate(`window.term = new Terminal({})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + await page.waitForSelector('.xterm-text-layer'); + await page.evaluate(`window.marker1 = window.term.addMarker(1)`); + await page.evaluate(`window.marker2 = window.term.addMarker(2)`); + await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); + await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`); + await openTerminal(page); + await pollFor(page, `document.querySelectorAll('.xterm-screen .xterm-decoration').length`, 2); + }); + it('should return undefined when the marker has already been disposed of', async () => { + await openTerminal(page); + await page.evaluate(`window.marker = window.term.addMarker(1)`); + await page.evaluate(`window.marker.dispose()`); + await pollFor(page, `window.decoration = window.term.registerDecoration({ marker: window.marker });`, undefined); + }); + it('should throw when a negative x offset is provided', async () => { + await openTerminal(page); + await page.evaluate(`window.marker = window.term.addMarker(1)`); + await page.evaluate(` + try { + window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 }); + } catch (e) { + window.throwMessage = e.message; + } + `); + await pollFor(page, 'window.throwMessage', 'This API only accepts positive integers'); + }); }); - it('on resize should dispose of the old decoration and create a new one', async () => { - await openTerminal(page); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await page.evaluate(`window.marker = window.term.addMarker(1)`); - await page.evaluate(`window.decoration = window.term.registerDecoration({ marker: window.marker })`); - await page.evaluate(`window.term.resize(10, 5)`); - assert.equal(await page.evaluate(`document.querySelectorAll('.xterm-screen .xterm-decoration').length`), 1); - }); - it('should return undefined when the marker has already been disposed of', async () => { - await openTerminal(page); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await page.evaluate(`window.marker = window.term.addMarker(1)`); - await page.evaluate(`window.marker.dispose()`); - assert.equal(await page.evaluate(`window.decoration = window.term.registerDecoration({ marker: window.marker });`), undefined); - }); - it('should throw when a negative x offset is provided', async () => { - await openTerminal(page); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await page.evaluate(`window.marker = window.term.addMarker(1)`); - await page.evaluate(` - try { - window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 }); - } catch (e) { - window.throwMessage = e.message; - } - `); - await pollFor(page, 'window.throwMessage', 'This API only accepts positive integers'); + describe('overviewRulerDecorations', () => { + it('should not add an overview ruler when width is not set', async () => { + await page.evaluate(`window.term = new Terminal({})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + await page.waitForSelector('.xterm-text-layer'); + await page.evaluate(`window.marker1 = window.term.addMarker(1)`); + await page.evaluate(`window.marker2 = window.term.addMarker(2)`); + await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); + await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); + await openTerminal(page); + await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0); + }); + it('should add an overview ruler when width is set', async () => { + await page.evaluate(`window.term = new Terminal({ overviewRulerWidth: 15 })`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + await page.waitForSelector('.xterm-text-layer'); + await page.evaluate(`window.marker1 = window.term.addMarker(1)`); + await page.evaluate(`window.marker2 = window.term.addMarker(2)`); + await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); + await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); + await openTerminal(page); + await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 1); + }); }); }); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2cd4daa6..15ee4650 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -266,6 +266,12 @@ declare module 'xterm' { * All features are disabled by default for security reasons. */ windowOptions?: IWindowOptions; + + /** + * The width, in pixels, of the canvas for the overview ruler. The overview + * ruler will be hidden when not set. + */ + overviewRulerWidth?: number; } /** @@ -394,7 +400,7 @@ declare module 'xterm' { } /** - * Represents a disposable with an + * Represents a disposable that tracks is disposed state. * @param onDispose event listener and * @param isDisposed property. */ @@ -427,49 +433,91 @@ declare module 'xterm' { readonly onRender: IEvent; /** - * The HTMLElement that gets created after the - * first _onRender call, or undefined if accessed before + * The element that the decoration is rendered to. This will be undefined + * until it is rendered for the first time by {@link IDecoration.onRender}. * that. */ - readonly element: HTMLElement | undefined; + element: HTMLElement | undefined; + + /** + * The options for the overview ruler that can be updated. + * This will only take effect when {@link IDecorationOptions.overviewRulerOptions} + * were provided initially. + */ + options: Pick; } + /** - * Options provided when registering a decoration - * containing a @param marker, @param anchor, - * @param x offset from the anchor, @param width in cells - * and @param height in cells. + * Overview ruler decoration options + */ + interface IDecorationOverviewRulerOptions { + color: string; + position?: 'left' | 'center' | 'right' | 'full'; + } + + /* + * Options that define the presentation of the decoration. */ export interface IDecorationOptions { /** * The line in the terminal where * the decoration will be displayed */ - marker: IMarker; + readonly marker: IMarker; /* * Where the decoration will be anchored - * defaults to the left edge */ - anchor?: 'right' | 'left'; + readonly anchor?: 'right' | 'left'; /** * The x position offset relative to the anchor - */ - x?: number; + */ + readonly x?: number; /** - * The width of the decoration in cells, which defaults to - * cell width + * The width of the decoration in cells, defaults to 1. */ - width?: number; + readonly width?: number; /** - * The height of the decoration in cells, which defaults to - * cell height + * The height of the decoration in cells, defaults to 1. */ - height?: number; + readonly height?: number; + + /** + * The background color of the cell(s). When 2 decorations both set the foreground color the + * last registered decoration will be used. Only the `#RRGGBB` format is supported. + */ + readonly backgroundColor?: string; + + /** + * The foreground color of the cell(s). When 2 decorations both set the foreground color the + * last registered decoration will be used. Only the `#RRGGBB` format is supported. + */ + readonly foregroundColor?: string; + + /** + * What layer to render the decoration at when {@link backgroundColor} or + * {@link foregroundColor} are used. `'bottom'` will render under the selection, `'top`' will + * render above the selection\*. + * + * *\* The selection will render on top regardless of layer on the canvas renderer due to how + * it renders selection separately.* + */ + readonly layer?: 'bottom' | 'top'; + + /** + * When defined, renders the decoration in the overview ruler to the right + * of the terminal. {@link ITerminalOptions.overviewRulerWidth} must be set + * in order to see the overview ruler. + * @param color The color of the decoration. + * @param position The position of the decoration. + */ + overviewRulerOptions?: IDecorationOverviewRulerOptions } /** @@ -939,7 +987,7 @@ declare module 'xterm' { /** * (EXPERIMENTAL) Adds a decoration to the terminal using - * @param decorationOptions, which takes a marker and an optional anchor, + * @param decorationOptions, which takes a marker and an optional anchor, * width, height, and x offset from the anchor. Returns the decoration or * undefined if the alt buffer is active or the marker has already been disposed of. * @throws when options include a negative x offset. diff --git a/yarn.lock b/yarn.lock index c85bca27..2523d316 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== @@ -144,6 +144,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== +"@babel/runtime@^7.15.4": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -355,9 +362,9 @@ integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== "@types/mocha@^8.2.1": - version "8.2.2" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.2.tgz#91daa226eb8c2ff261e6a8cbf8c7304641e095e0" - integrity sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw== + version "8.2.3" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323" + integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw== "@types/mocha@^9.0.0": version "9.0.0" @@ -375,9 +382,9 @@ integrity sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw== "@types/node@^12.12.37": - version "12.20.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.12.tgz#fd9c1c2cfab536a2383ed1ef70f94adea743a226" - integrity sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg== + version "12.20.50" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.50.tgz#14ba5198f1754ffd0472a2f84ab433b45ee0b65e" + integrity sha512-+9axpWx2b2JCVovr7Ilgt96uc6C1zBKOQMpGtRbWT9IoR/8ue32GGMfGA4woP8QyP2gBs6GQWEVM3tCybGCxDA== "@types/node@^14.14.44": version "14.14.44" @@ -390,9 +397,9 @@ integrity sha512-+hQX+WyJAOne7Fh3zF5CxPemILIbuhNcqHHodzK9caYOLnC8pD5efmPleRnw0z++LfKUC/sVNMwk0Gap+B0baA== "@types/puppeteer@^5.4.3": - version "5.4.3" - resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.3.tgz#cdca84aa7751d77448d8a477dbfa0af1f11485f2" - integrity sha512-3nE8YgR9DIsgttLW+eJf6mnXxq8Ge+27m5SU3knWmrlfl6+KOG0Bf9f7Ua7K+C4BnaTMAh3/UpySqdAYvrsvjg== + version "5.4.6" + resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.6.tgz#afc438e41dcbc27ca1ba0235ea464a372db2b21c" + integrity sha512-98Kghehs7+/GD9b56qryhqdqVCXUTbetTv3PlvDnmFRTHQH0j9DIp1f7rkAW3BAj4U3yoeSEQnKgdW8bDq0Y0Q== dependencies: "@types/node" "*" @@ -869,20 +876,10 @@ ansi-colors@4.1.1, ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^3.2.1: version "3.2.1" @@ -899,14 +896,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -1048,11 +1037,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -1108,7 +1092,7 @@ chai@^4.3.4: pathval "^1.1.1" type-detect "^4.0.5" -chalk@^2.0.0, chalk@^2.3.0: +chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -1138,21 +1122,6 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - chokidar@3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -1179,9 +1148,9 @@ clean-stack@^2.0.0: integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== cli-table@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" - integrity sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ== + version "0.3.11" + resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.11.tgz#ac69cdecbe81dccdba4889b9a18b7da312a9d3ee" + integrity sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ== dependencies: colors "1.0.3" @@ -1257,11 +1226,11 @@ colors@1.0.3: integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= columnify@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" - integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= + version "1.6.0" + resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.6.0.tgz#6989531713c9008bb29735e61e37acf5bd553cf3" + integrity sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q== dependencies: - strip-ansi "^3.0.0" + strip-ansi "^6.0.1" wcwidth "^1.0.0" combined-stream@^1.0.8: @@ -1271,7 +1240,7 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^2.12.1, commander@^2.20.0: +commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -1296,10 +1265,10 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -complex.js@^2.0.11: - version "2.0.12" - resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.12.tgz#fa4df97d8928e5f7b6a86b35bdeecc3a3eda8a22" - integrity sha512-oQX99fwL6LrTVg82gDY1dIWXy6qZRnRL35N+YhIX0N7tSwsa0KFy6IEMHTNuCW4mP7FS7MEqZ/2I/afzYwPldw== +complex.js@^2.0.15: + version "2.1.1" + resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.1.1.tgz#0675dac8e464ec431fb2ab7d30f41d889fb25c31" + integrity sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg== concat-map@0.0.1: version "0.0.1" @@ -1391,13 +1360,6 @@ debug@4, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - debug@4.3.3: version "4.3.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" @@ -1422,12 +1384,7 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -decimal.js@^10.0.0, decimal.js@^10.2.1: - version "10.2.1" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3" - integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== - -decimal.js@^10.3.1: +decimal.js@^10.0.0, decimal.js@^10.3.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== @@ -1506,11 +1463,6 @@ diff@5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -2040,10 +1992,10 @@ forwarded@~0.1.2: resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= -fraction.js@^4.0.13: - version "4.0.13" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.13.tgz#3c1c315fa16b35c85fffa95725a36fa729c69dfe" - integrity sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA== +fraction.js@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" + integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== fresh@0.5.2: version "0.5.2" @@ -2060,7 +2012,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.3.1, fsevents@~2.3.2: +fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -2116,7 +2068,7 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2135,18 +2087,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@7.2.0, glob@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -2159,10 +2099,10 @@ glob@7.2.0, glob@^7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== +glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -2437,11 +2377,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -2676,13 +2611,6 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" - integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== - dependencies: - argparse "^2.0.1" - js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -2813,13 +2741,6 @@ lodash@^4.17.13: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" - integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== - dependencies: - chalk "^4.0.0" - log-symbols@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" @@ -2843,14 +2764,15 @@ make-dir@^3.0.0, make-dir@^3.0.2: semver "^6.0.0" mathjs@^9.3.0: - version "9.3.2" - resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-9.3.2.tgz#6523dd5c963d200ff1cea0ff7963b10521b82185" - integrity sha512-0YKSKAeN9OkbIQrxfxnBT4kk/KlH71piWOsvVvAasyRIj/Xd/zlpc5VP/aFxwr+llOq2F3f6booPEu2fWv3yjQ== + version "9.5.2" + resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-9.5.2.tgz#e0f3279320dc6f49e45d99c4fcdd8b52231f0462" + integrity sha512-c0erTq0GP503/Ch2OtDOAn50GIOsuxTMjmE00NI/vKJFSWrDaQHRjx6ai+16xYv70yBSnnpUgHZGNf9FR9IwmA== dependencies: - complex.js "^2.0.11" - decimal.js "^10.2.1" + "@babel/runtime" "^7.15.4" + complex.js "^2.0.15" + decimal.js "^10.3.1" escape-latex "^1.2.0" - fraction.js "^4.0.13" + fraction.js "^4.1.1" javascript-natural-sort "^0.7.1" seedrandom "^3.0.5" tiny-emitter "^2.1.0" @@ -2948,47 +2870,9 @@ minimatch@3.0.4, minimatch@^3.0.4: brace-expansion "^1.1.7" minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mkdirp@^0.5.3: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -mocha@^8.3.2: - version "8.4.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.4.0.tgz#677be88bf15980a3cae03a73e10a0fc3997f0cff" - integrity sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ== - dependencies: - "@ungap/promise-all-settled" "1.1.2" - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.1" - debug "4.3.1" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "7.1.6" - growl "1.10.5" - he "1.2.0" - js-yaml "4.0.0" - log-symbols "4.0.0" - minimatch "3.0.4" - ms "2.1.3" - nanoid "3.1.20" - serialize-javascript "5.0.1" - strip-json-comments "3.1.1" - supports-color "8.1.1" - which "2.0.2" - wide-align "1.1.3" - workerpool "6.1.0" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== mocha@^9.2.0: version "9.2.0" @@ -3050,11 +2934,6 @@ nan@^2.14.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== -nanoid@3.1.20: - version "3.1.20" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" - integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== - nanoid@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" @@ -3476,13 +3355,6 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== - dependencies: - picomatch "^2.2.1" - readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -3497,6 +3369,11 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + regexp.prototype.flags@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" @@ -3628,7 +3505,7 @@ seedrandom@^3.0.5: resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== -semver@^5.3.0, semver@^5.4.1: +semver@^5.4.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -3664,13 +3541,6 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@5.0.1, serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== - dependencies: - randombytes "^2.1.0" - serialize-javascript@6.0.0, serialize-javascript@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -3678,6 +3548,13 @@ serialize-javascript@6.0.0, serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + serve-static@1.14.1: version "1.14.1" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" @@ -3842,14 +3719,6 @@ stack-utils@^2.0.3: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" @@ -3875,20 +3744,6 @@ string.prototype.trimstart@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - strip-ansi@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" @@ -3896,6 +3751,13 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" @@ -4046,42 +3908,11 @@ ts-loader@^9.1.2: micromatch "^4.0.0" semver "^7.3.4" -tslib@^1.13.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^1.8.1: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== -tslint@^6.1.3: - version "6.1.3" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.3.tgz#5c23b2eccc32487d5523bd3a470e9aa31789d904" - integrity sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg== - dependencies: - "@babel/code-frame" "^7.0.0" - builtin-modules "^1.1.1" - chalk "^2.3.0" - commander "^2.12.1" - diff "^4.0.1" - glob "^7.1.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" - mkdirp "^0.5.3" - resolve "^1.3.2" - semver "^5.3.0" - tslib "^1.13.0" - tsutils "^2.29.0" - -tsutils@^2.29.0: - version "2.29.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" - integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== - dependencies: - tslib "^1.8.1" - tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -4127,9 +3958,9 @@ type-is@~1.6.17, type-is@~1.6.18: mime-types "~2.1.24" typed-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.0.0.tgz#15ab3825845138a8b1113bd89e60cd6a435739e8" - integrity sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA== + version "2.1.0" + resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.1.0.tgz#ded6f8a442ba8749ff3fe75bc41419c8d46ccc3f" + integrity sha512-bctQIOqx2iVbWGDGPWwIm18QScpu2XRmkC19D8rQGFsjKSgteq/o1hTZvIG/wuDq8fanpBDrLkLq+aEN/6y5XQ== typedarray-to-buffer@^3.1.5: version "3.1.5" @@ -4139,9 +3970,9 @@ typedarray-to-buffer@^3.1.5: is-typedarray "^1.0.0" typescript@^4.2.3: - version "4.2.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" - integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== + version "4.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9" + integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== typescript@^4.4.4: version "4.4.4" @@ -4406,13 +4237,6 @@ which@2.0.2, which@^2.0.1: dependencies: isexe "^2.0.0" -wide-align@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" @@ -4423,11 +4247,6 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -workerpool@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b" - integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg== - workerpool@6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" @@ -4486,10 +4305,10 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xterm-benchmark@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/xterm-benchmark/-/xterm-benchmark-0.3.0.tgz#8702ae41672ff1e656423336f4e54699a3ab74e8" - integrity sha512-JTC1NjaqAWRHA3vPwbAocOJvz42ysYelrq7z2PS9egly8Hi5W7zfJIZlpkzEsZ8dXo0fcEm/6azmoYFy+TMjig== +xterm-benchmark@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/xterm-benchmark/-/xterm-benchmark-0.3.1.tgz#dcaaf808e40605c7c27a83b5a5b81f9c45045e24" + integrity sha512-JjsCrSxkYKWf5CmBt2BeXm83KQdStyoGWREWQ0jSFF5N8CYVbdKQoWgs56mmy6qWD5GDKxO+V89Cvnbc8YUFjw== dependencies: "@types/app-root-path" "^1.2.4" "@types/cli-table" "^0.3.0" @@ -4502,8 +4321,6 @@ xterm-benchmark@^0.3.0: columnify "^1.5.4" commander "^6.2.1" mathjs "^9.3.0" - mocha "^8.3.2" - tslint "^6.1.3" typescript "^4.2.3" y18n@^4.0.0: