diff --git a/addons/addon-search/src/SearchEngine.ts b/addons/addon-search/src/SearchEngine.ts index 7e89c03a17..1760bc2bd1 100644 --- a/addons/addon-search/src/SearchEngine.ts +++ b/addons/addon-search/src/SearchEngine.ts @@ -60,7 +60,7 @@ export class SearchEngine { this._terminal.clearSelection(); return undefined; } - if (startCol > this._terminal.cols) { + if (startCol >= this._terminal.cols) { throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`); } diff --git a/addons/addon-web-links/src/WebLinkProvider.ts b/addons/addon-web-links/src/WebLinkProvider.ts index 0a826eb3a0..5df5d268a2 100644 --- a/addons/addon-web-links/src/WebLinkProvider.ts +++ b/addons/addon-web-links/src/WebLinkProvider.ts @@ -57,7 +57,8 @@ function isUrl(urlString: string): boolean { export class LinkComputer { public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] { - const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); + const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`; + const rex = new RegExp(regex.source, flags); const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal); const line = lines.join(''); diff --git a/addons/addon-webgl/src/TextureAtlas.ts b/addons/addon-webgl/src/TextureAtlas.ts index 53d2d056f4..9d42ca9e1b 100644 --- a/addons/addon-webgl/src/TextureAtlas.ts +++ b/addons/addon-webgl/src/TextureAtlas.ts @@ -191,7 +191,7 @@ export class TextureAtlas implements ITextureAtlas { return newPage; } - const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1); + const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a - b); const mergedPageIndex = this.pages.length - mergingPages.length; // Merge into the new page diff --git a/bin/esbuild_all.mjs b/bin/esbuild_all.mjs index 1a3aadea56..789cb5df00 100644 --- a/bin/esbuild_all.mjs +++ b/bin/esbuild_all.mjs @@ -19,14 +19,17 @@ for (const addon of addons) { // Demo job - This requires the others to be built so it's not included when building all // jobs.push(createJob('demo-client', [`--demo-client`])); -await Promise.all(jobs.map((job, i) => { +const codes = await Promise.all(jobs.map(job => { return new Promise(r => { job.cp.on('exit', code => { log(`Finished \x1b[32m${job.name}\x1b[0m${code ? ' \x1b[31mwith errors\x1b[0m' : ''}`); - r(code); + r(code ?? 1); }); }); })); +if (codes.some(code => code !== 0)) { + process.exit(1); +} /** * @param {string} message diff --git a/bin/setup-full.mjs b/bin/setup-full.mjs index 2db85d87a6..35a62cbd7a 100644 --- a/bin/setup-full.mjs +++ b/bin/setup-full.mjs @@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process'; import { basename, dirname, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const nodeModulesPath = resolve(repoRoot, 'node_modules'); const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm'; @@ -13,7 +13,7 @@ const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm'; /** @param {string} message */ function log(message) { - console.info(`[setup-fast] ${message}`); + console.info(`[setup-full] ${message}`); } /** @param {string[]} args */ diff --git a/bin/test_integration.js b/bin/test_integration.js index 37e6aca7c5..0a2531e23b 100644 --- a/bin/test_integration.js +++ b/bin/test_integration.js @@ -12,7 +12,7 @@ let argv = process.argv.slice(2); let suiteFilter = undefined; while (argv.some(e => e.startsWith('--suite='))) { const i = argv.findIndex(e => e.startsWith('--suite=')); - const match = argv[i].match(/--suite=(?.+)/) + const match = argv[i].match(/--suite=(?.+)/); suiteFilter = match?.groups?.suitename ?? undefined; argv.splice(i, 1); } @@ -42,12 +42,18 @@ if (suiteFilter) { configs = configs.filter(e => e.name === suiteFilter); } +if (suiteFilter && configs.length === 0) { + console.error(`Unknown suite: ${suiteFilter}`); + process.exit(1); +} + function npmBinScript(script) { return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script)); } async function run() { + let exitCode = 0; for (const config of configs) { const command = npmBinScript('playwright'); const args = ['test', '-c', config.path, ...argv]; @@ -62,10 +68,13 @@ async function run() { if (run.error) { console.error(run.error); - process.exit(run.status ?? -1); + process.exit(run.status ?? 1); } - process.exit(run.status); + if (run.status) { + exitCode = run.status; + } } + process.exit(exitCode); } run(); diff --git a/bin/test_mousemodes.js b/bin/test_mousemodes.js index 0f12d208b9..8954c1d777 100644 --- a/bin/test_mousemodes.js +++ b/bin/test_mousemodes.js @@ -177,7 +177,7 @@ const ENC = { } function printMenu() { - console.log('\x1b[2J\x1b\[HTest mouse reports [Ctrl-C to exit]'); + console.log('\x1b[2J\x1b[HTest mouse reports [Ctrl-C to exit]'); console.log(); console.log(' Selected protocol [Ctrl-A to switch]'); const protocols = Object.keys(PROTOCOLS); diff --git a/bin/test_unit.js b/bin/test_unit.js index 58fdd9a321..2ff7f62507 100644 --- a/bin/test_unit.js +++ b/bin/test_unit.js @@ -21,7 +21,7 @@ if (process.argv.length > 2) { flagArgs = args.filter(e => e.startsWith('--')); // ability to inject particular test files via // npm run test [testFileA testFileB ...] - files = args.filter(e => !e.startsWith('--')); + const files = args.filter(e => !e.startsWith('--')); if (files.length) { testFiles = files; } @@ -39,8 +39,6 @@ if (checkCoverage) { ...testFiles, ...flagArgs ]; - console.info('executable', executable); - console.info('args', args); const run = cp.spawnSync( executable, args, @@ -51,7 +49,7 @@ if (checkCoverage) { stdio: 'inherit' } ); - process.exit(run.status); + process.exit(run.status ?? -1); } const run = cp.spawnSync( diff --git a/package.json b/package.json index 43e569d028..d3da864b6a 100644 --- a/package.json +++ b/package.json @@ -61,9 +61,9 @@ "test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"", "test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"", "test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000", - "benchmark": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json", - "benchmark-baseline": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js", - "benchmark-eval": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js", + "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json", + "benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js", + "benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js", "clean": "rm -rf lib out addons/*/lib addons/*/out", "vtfeatures": "node bin/extract_vtfeatures.js src/*/*.ts src/*/*/*.ts src/*.ts addons/**/src/*.ts", "prepackage": "npm run build", diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 4b6f6e8bbf..30b4ed3bb6 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -73,7 +73,9 @@ export interface IBufferLineStringCache { */ export class BufferLine implements IBufferLine { protected _data: Uint32Array; + /** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */ protected _combined: {[index: number]: string} = {}; + /** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */ protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; protected _stringCacheEntryRef: WeakRef | undefined; public length: number; @@ -205,9 +207,15 @@ export class BufferLine implements IBufferLine { cell.bg = this._data[$startIndex + Cell.BG]; if (cell.content & Content.IS_COMBINED_MASK) { cell.combinedData = this._combined[index]; + } else { + cell.combinedData = ''; } if (cell.bg & BgFlags.HAS_EXTENDED) { cell.extended = this._extendedAttrs[index]!; + } else { + // Do not mutate cell.extended in place: it may still reference this line's map entry from a + // prior loadCell into a reused CellData (e.g. $workCell during insert/delete). + cell.extended = DEFAULT_ATTR_DATA.extended.clone(); } return cell; } @@ -459,14 +467,7 @@ export class BufferLine implements IBufferLine { this._data.set(line._data); } this.length = line.length; - this._combined = {}; - for (const el in line._combined) { - this._combined[el] = line._combined[el]; - } - this._extendedAttrs = {}; - for (const el in line._extendedAttrs) { - this._extendedAttrs[el] = line._extendedAttrs[el]; - } + this._copySparseMapsFrom(line); this.isWrapped = line.isWrapped; } @@ -475,12 +476,7 @@ export class BufferLine implements IBufferLine { const newLine = new BufferLine(this._stringCache, 0, undefined, false); newLine._data = new Uint32Array(this._data); newLine.length = this.length; - for (const el in this._combined) { - newLine._combined[el] = this._combined[el]; - } - for (const el in this._extendedAttrs) { - newLine._extendedAttrs[el] = this._extendedAttrs[el]; - } + newLine._copySparseMapsFrom(this); newLine.isWrapped = this.isWrapped; return newLine; } @@ -511,27 +507,14 @@ export class BufferLine implements IBufferLine { for (let i = 0; i < Constants.CELL_INDICIES; i++) { this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i]; } - if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; - } + this._copyCellMapsFrom(src, srcCol + cell, destCol + cell); } } else { for (let cell = 0; cell < length; cell++) { for (let i = 0; i < Constants.CELL_INDICIES; i++) { this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i]; } - if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; - } - } - } - - // Move any combined data over as needed, FIXME: repeat for extended attrs - const srcCombinedKeys = Object.keys(src._combined); - for (let i = 0; i < srcCombinedKeys.length; i++) { - const key = parseInt(srcCombinedKeys[i], 10); - if (key >= srcCol) { - this._combined[key - srcCol + destCol] = src._combined[key]; + this._copyCellMapsFrom(src, srcCol + cell, destCol + cell); } } } @@ -620,4 +603,24 @@ export class BufferLine implements IBufferLine { cacheEntry.isTrimmed = false; } } + + /** Copy sparse map entries for a single cell when `_data` flags require them. */ + private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void { + const srcStart = srcCol * Constants.CELL_INDICIES; + if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) { + this._combined[destCol] = src._combined[srcCol]; + } + if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[destCol] = src._extendedAttrs[srcCol]; + } + } + + /** Rebuild sparse maps from another line, keyed only by `_data` flags. */ + private _copySparseMapsFrom(line: BufferLine): void { + this._combined = {}; + this._extendedAttrs = {}; + for (let i = 0; i < line.length; i++) { + this._copyCellMapsFrom(line, i, i); + } + } } diff --git a/test/benchmark/DecorationService.benchmark.ts b/test/benchmark/DecorationService.benchmark.ts index 534a1fa30e..1a2033de1b 100644 --- a/test/benchmark/DecorationService.benchmark.ts +++ b/test/benchmark/DecorationService.benchmark.ts @@ -4,8 +4,8 @@ */ import { perfContext, before, RuntimeCase } from 'xterm-benchmark'; -import { DecorationService } from '../../src/common/services/DecorationService'; -import { MockLogService, MockBufferService, MockOptionsService } from '../../src/common/TestUtils.test'; +import { DecorationService } from 'common/services/DecorationService'; +import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test'; const enum Constants { COLS = 80, ROWS = 30, diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index c2cecfdbbb..5c9f372fb5 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -4,12 +4,12 @@ */ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-benchmark'; -import { EscapeSequenceParser } from '../../src/common/parser/EscapeSequenceParser'; -import { C0, C1 } from '../../src/common/data/EscapeSequences'; -import { IDcsHandler, IOscHandler, IApcHandler, IParams } from '../../src/common/parser/Types'; -import { OscHandler } from '../../src/common/parser/OscParser'; -import { DcsHandler } from '../../src/common/parser/DcsParser'; -import { ApcHandler } from '../../src/common/parser/ApcParser'; +import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; +import { C0, C1 } from 'common/data/EscapeSequences'; +import { IDcsHandler, IOscHandler, IApcHandler, IParams } from 'common/parser/Types'; +import { OscHandler } from 'common/parser/OscParser'; +import { DcsHandler } from 'common/parser/DcsParser'; +import { ApcHandler } from 'common/parser/ApcParser'; const SIZE = 5000000; diff --git a/test/benchmark/Event.benchmark.ts b/test/benchmark/Event.benchmark.ts index ba3f8de9ce..9cd8216dc1 100644 --- a/test/benchmark/Event.benchmark.ts +++ b/test/benchmark/Event.benchmark.ts @@ -4,7 +4,7 @@ */ import { perfContext, before, RuntimeCase } from 'xterm-benchmark'; -import { Emitter } from '../../src/common/Event'; +import { Emitter } from 'common/Event'; const ITERATIONS = 1_000_000; diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index 97579cb30a..fb06b429e3 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -6,8 +6,8 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark'; import { spawn } from 'node-pty'; -import { Utf8ToUtf32, stringFromCodePoint } from '../../src/common/input/TextDecoder'; -import { CoreBrowserTerminal } from '../../src/browser/CoreBrowserTerminal'; +import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; +import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal'; perfContext('Terminal: ls -lR /usr/lib', () => { let content = ''; diff --git a/test/benchmark/tsconfig.json b/test/benchmark/tsconfig.json index fe2af282c2..c8e17ee617 100644 --- a/test/benchmark/tsconfig.json +++ b/test/benchmark/tsconfig.json @@ -11,7 +11,11 @@ "moduleResolution": "nodenext", "strict": false, "target": "es2021", - "module": "nodenext" + "module": "nodenext", + "paths": { + "common/*": ["../../src/common/*"], + "browser/*": ["../../src/browser/*"] + } }, "include": [ "./**/*",