diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts index e30a8f384bf97a52e059cccb32394b3ad138709a..5e15cd18e0e6913389d422413a1f54d1f6147a59 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTypeAheadAddon.ts @@ -79,8 +79,12 @@ class Cursor implements ICoordinate { this._baseY = buffer.baseY; } + public getLine() { + return this.buffer.getLine(this._y + this._baseY); + } + public getCell(loadInto?: IBufferCell) { - return this.buffer.getLine(this._y + this._baseY)?.getCell(this._x, loadInto); + return this.getLine()?.getCell(this._x, loadInto); } public moveTo(coordinate: ICoordinate) { @@ -403,17 +407,22 @@ class BackspacePrediction implements IPrediction { pos: ICoordinate; oldAttributes: string; oldChar: string; + eol: boolean; }; constructor(private readonly terminal: Terminal) { } public apply(_: IBuffer, cursor: Cursor) { + // at eol if everything to the right is whitespace (zsh will emit a "clear line" code in this case) + // todo: can be optimized if `getTrimmedLength` is exposed from xterm + const eol = !cursor.getLine()?.translateToString(true, cursor.x); + const move = cursor.shift(-1); const cell = cursor.getCell(); this.appliedAt = cell - ? { pos: cursor.coordinate, oldAttributes: attributesToSeq(cell), oldChar: cell.getChars() } - : { pos: cursor.coordinate, oldAttributes: '', oldChar: '' }; + ? { eol, pos: cursor.coordinate, oldAttributes: attributesToSeq(cell), oldChar: cell.getChars() } + : { eol, pos: cursor.coordinate, oldAttributes: '', oldChar: '' }; - return cursor.shift(-1) + DELETE_CHAR; + return move + DELETE_CHAR; } public rollback(cursor: Cursor) { @@ -434,8 +443,7 @@ class BackspacePrediction implements IPrediction { } public matches(input: StringReader) { - const isEOL = this.appliedAt?.oldChar === ''; - if (isEOL) { + if (this.appliedAt?.eol) { const r1 = input.eatGradually(`\b${CSI}K`); if (r1 !== MatchResult.Failure) { return r1; diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts index 0f152766dde63877f3c79063aa2cb4c7c40f86a0..2d91a7f5093c38f17a4db03554420b425555cc43 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalTypeahead.test.ts @@ -213,6 +213,16 @@ suite('Workbench - Terminal Typeahead', () => { assert.strictEqual(addon.stats?.accuracy, 1); }); + test('restores old character after invalid backspace', () => { + const t = createMockTerminal({ lines: ['hel|lo'] }); + addon.activate(t.terminal); + (addon as any).lastRow = { y: 1, startingX: 1 }; + t.onData('\x7F'); + t.expectWritten(`${CSI}2;4H${CSI}X`); + expectProcessed('x', `${CSI}?25l${CSI}0ml${CSI}2;4H${CSI}0mx${CSI}?25h`); + assert.strictEqual(addon.stats?.accuracy, 0); + }); + test('waits for validation before deleting to left of cursor', () => { const t = createMockTerminal({ lines: ['hello|'] }); addon.activate(t.terminal); @@ -332,6 +342,10 @@ function createMockTerminal({ lines, cursorAttrs }: { return { length: s.length, getCell: (x: number) => mockCell(s[x - 1] || ''), + translateToString: (trim: boolean, start = 0, end = s.length) => { + const out = s.slice(start, end); + return trim ? out.trimRight() : out; + }, }; }, }