diff --git a/addons/addon-search/src/SearchLineCache.ts b/addons/addon-search/src/SearchLineCache.ts index 535a3cc5..175f3919 100644 --- a/addons/addon-search/src/SearchLineCache.ts +++ b/addons/addon-search/src/SearchLineCache.ts @@ -38,6 +38,9 @@ export class SearchLineCache extends Disposable { private _linesCache: LineCacheEntry[] | undefined; private _linesCacheTimeout = this._register(new MutableDisposable()); private _linesCacheDisposables = this._register(new MutableDisposable()); + // Track access to avoid recreating a timeout on every init call which occurs once per search + // result (findNext/findPrevious -> _highlightAllMatches -> find loop). + private _lastAccessTimestamp = 0; constructor(private readonly _terminal: Terminal) { super(); @@ -57,15 +60,34 @@ export class SearchLineCache extends Disposable { ); } - this._linesCacheTimeout.value = disposableTimeout(() => this._destroyLinesCache(), Constants.LINES_CACHE_TIME_TO_LIVE); + this._lastAccessTimestamp = Date.now(); + if (!this._linesCacheTimeout.value) { + this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE); + } } private _destroyLinesCache(): void { this._linesCache = undefined; + this._lastAccessTimestamp = 0; this._linesCacheDisposables.clear(); this._linesCacheTimeout.clear(); } + private _scheduleLinesCacheTimeout(delay: number): void { + this._linesCacheTimeout.value = disposableTimeout(() => { + if (!this._linesCache) { + return; + } + const now = Date.now(); + const elapsed = now - this._lastAccessTimestamp; + if (elapsed >= Constants.LINES_CACHE_TIME_TO_LIVE) { + this._destroyLinesCache(); + return; + } + this._scheduleLinesCacheTimeout(Constants.LINES_CACHE_TIME_TO_LIVE - elapsed); + }, delay); + } + public getLineFromCache(row: number): LineCacheEntry | undefined { return this._linesCache?.[row]; } diff --git a/demo/client/components/window/testWindow.ts b/demo/client/components/window/testWindow.ts index e811ce78..12c6e995 100644 --- a/demo/client/components/window/testWindow.ts +++ b/demo/client/components/window/testWindow.ts @@ -671,7 +671,9 @@ function sgrTest(term: Terminal): void { { ps: 47, name: 'Background White' }, { ps: 49, name: 'Background default' }, { ps: 53, name: 'Overlined' }, - { ps: 55, name: 'Not overlined' } + { ps: 55, name: 'Not overlined' }, + { ps: 221, name: 'Not bold (kitty)' }, + { ps: 222, name: 'Not faint (kitty)' } ]; const maxNameLength = entries.reduce((p, c) => Math.max(c.name.length, p), 0); for (const e of entries) { @@ -684,7 +686,9 @@ function sgrTest(term: Terminal): void { const comboEntries: { ps: number[] }[] = [ { ps: [1, 2, 3, 4, 5, 6, 7, 9] }, { ps: [2, 41] }, - { ps: [4, 53] } + { ps: [4, 53] }, + { ps: [1, 2, 221] }, + { ps: [1, 2, 222] } ]; term.write('\n\n\r'); term.writeln(`Combinations`); diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 5a46570e..dd312af1 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -176,6 +176,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { x = Math.max(x, MINIMUM_COLS); y = Math.max(y, MINIMUM_ROWS); + // Flush pending writes before resize to avoid race conditions where async + // writes are processed with incorrect dimensions + this._writeBuffer.flushSync(); + this._bufferService.resize(x, y); } diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 59966fd7..5ba2342a 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -716,6 +716,22 @@ describe('InputHandler', () => { await inputHandler.parseP('\x1b[22m'); assert.equal(!!inputHandler.curAttrData.isDim(), false); }); + it('SGR 221 resets bold only (kitty)', async () => { + await inputHandler.parseP('\x1b[1;2m'); + assert.equal(!!inputHandler.curAttrData.isBold(), true); + assert.equal(!!inputHandler.curAttrData.isDim(), true); + await inputHandler.parseP('\x1b[221m'); + assert.equal(!!inputHandler.curAttrData.isBold(), false); + assert.equal(!!inputHandler.curAttrData.isDim(), true); + }); + it('SGR 222 resets faint only (kitty)', async () => { + await inputHandler.parseP('\x1b[1;2m'); + assert.equal(!!inputHandler.curAttrData.isBold(), true); + assert.equal(!!inputHandler.curAttrData.isDim(), true); + await inputHandler.parseP('\x1b[222m'); + assert.equal(!!inputHandler.curAttrData.isBold(), true); + assert.equal(!!inputHandler.curAttrData.isDim(), false); + }); it('italic', async () => { await inputHandler.parseP('\x1b[3m'); assert.equal(!!inputHandler.curAttrData.isItalic(), true); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index fa252113..2e57278e 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2538,6 +2538,8 @@ export class InputHandler extends Disposable implements IInputHandler { * | 53 | Overlined. | #Y | * | 55 | Not Overlined. | #Y | * | 58 | Underline color: Extended color. | #P[Support for RGB and indexed colors, see below.] | + * | 221 | Not bold (kitty extension). | #Y | + * | 222 | Not faint (kitty extension). | #Y | * | 90 - 97 | Bright foreground color (analogous to 30 - 37). | #Y | * | 100 - 107 | Bright background color (analogous to 40 - 47). | #Y | * @@ -2670,6 +2672,12 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 55) { // not overline attr.bg &= ~BgFlags.OVERLINE; + } else if (p === 221) { + // not bold (kitty extension) + attr.fg &= ~FgFlags.BOLD; + } else if (p === 222) { + // not faint (kitty extension) + attr.bg &= ~BgFlags.DIM; } else if (p === 59) { attr.extended = attr.extended.clone(); attr.extended.underlineColor = -1; diff --git a/src/common/input/WriteBuffer.test.ts b/src/common/input/WriteBuffer.test.ts index 89106423..c534bbae 100644 --- a/src/common/input/WriteBuffer.test.ts +++ b/src/common/input/WriteBuffer.test.ts @@ -106,5 +106,24 @@ describe('WriteBuffer', () => { wb.writeSync('1', 10); assert.equal(last, '11'); // 1 + 10 sub calls = 11 }); + it('flushSync processes all pending writes', done => { + wb.write('a', () => { cbStack.push('a'); }); + wb.write('b', () => { cbStack.push('b'); }); + wb.write('c', () => { cbStack.push('c'); }); + wb.flushSync(); + assert.deepEqual(stack, ['a', 'b', 'c']); + assert.deepEqual(cbStack, ['a', 'b', 'c']); + wb.write('x', () => { cbStack.push('x'); }); + wb.write('', () => { + assert.deepEqual(stack, ['a', 'b', 'c', 'x', '']); + assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']); + done(); + }); + }); + it('flushSync with no pending writes is a no-op', () => { + wb.flushSync(); + assert.deepEqual(stack, []); + assert.deepEqual(cbStack, []); + }); }); }); diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index 801cf3ef..f6d83ba8 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -54,6 +54,38 @@ export class WriteBuffer extends Disposable { this._didUserInput = true; } + /** + * Flushes all pending writes synchronously. This is useful when you need to + * ensure all queued data is processed before performing an operation that + * depends upon everything being parsed like resize. + * + * Note: This is unreliable with async parser handlers as it does not wait for + * promises to resolve. + */ + public flushSync(): void { + // exit early if another sync write loop is active + if (this._isSyncWriting) { + return; + } + this._isSyncWriting = true; + + // Process all pending chunks synchronously + let chunk: string | Uint8Array | undefined; + while (chunk = this._writeBuffer.shift()) { + this._action(chunk); + const cb = this._callbacks.shift(); + if (cb) cb(); + } + + // Reset buffer state + this._pendingData = 0; + this._bufferOffset = 0x7FFFFFFF; + this._writeBuffer.length = 0; + this._callbacks.length = 0; + + this._isSyncWriting = false; + } + /** * @deprecated Unreliable, to be removed soon. */