Skip to content

Commit ca7a113

Browse files
committed
fix: clear scrollback command history
1 parent c43a9f4 commit ca7a113

7 files changed

Lines changed: 119 additions & 16 deletions

File tree

src/vs/platform/terminal/common/capabilities/capabilities.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export interface ICommandDetectionCapability {
239239
*/
240240
getCwdForLine(line: number): string | undefined;
241241
getCommandForLine(line: number): ITerminalCommand | ICurrentPartialCommand | undefined;
242-
clearCommandsInViewport(): void;
242+
clearCommands(): void;
243243
handlePromptStart(options?: IHandleCommandOptions): void;
244244
handleContinuationStart(): void;
245245
handleContinuationEnd(): void;
@@ -295,7 +295,7 @@ export interface IPartialCommandDetectionCapability {
295295
readonly type: TerminalCapability.PartialCommandDetection;
296296
readonly commands: readonly IMarker[];
297297
readonly onCommandFinished: Event<IMarker>;
298-
clearCommandsInViewport(): void;
298+
clearCommands(): void;
299299
}
300300

301301
interface IBaseTerminalCommand {

src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,10 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe
202202
}
203203
}
204204

205-
clearCommandsInViewport(): void {
206-
this._clearCommandsInViewport();
205+
clearCommands(): void {
206+
if (this._commands.length > 0) {
207+
this._onCommandInvalidated.fire(this._commands.splice(0));
208+
}
207209
}
208210

209211
setContinuationPrompt(value: string): void {

src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class PartialCommandDetectionCapability extends DisposableStore implement
3737
this.add(this._terminal.onData(e => this._onData(e)));
3838
this.add(this._terminal.parser.registerCsiHandler({ final: 'J' }, params => {
3939
if (params.length >= 1 && (params[0] === 2 || params[0] === 3)) {
40-
this.clearCommandsInViewport();
40+
this._clearCommandsInViewport();
4141
}
4242
// We don't want to override xterm.js' default behavior, just augment it
4343
return false;
@@ -66,7 +66,7 @@ export class PartialCommandDetectionCapability extends DisposableStore implement
6666
}
6767
}
6868

69-
clearCommandsInViewport(): void {
69+
private _clearCommandsInViewport(): void {
7070
// Find the number of commands on the tail end of the array that are within the viewport
7171
let count = 0;
7272
for (let i = this._commands.length - 1; i >= 0; i--) {
@@ -78,4 +78,8 @@ export class PartialCommandDetectionCapability extends DisposableStore implement
7878
// Remove them
7979
this._commands.splice(this._commands.length - count, count);
8080
}
81+
82+
clearCommands(): void {
83+
this._commands.length = 0;
84+
}
8185
}

src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,8 +773,9 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach
773773
}
774774

775775
clearBuffer(): void {
776-
this._capabilities.get(TerminalCapability.CommandDetection)?.clearCommandsInViewport();
777-
this._capabilities.get(TerminalCapability.PartialCommandDetection)?.clearCommandsInViewport();
776+
this._decorationAddon.clearDecorations();
777+
this._capabilities.get(TerminalCapability.CommandDetection)?.clearCommands();
778+
this._capabilities.get(TerminalCapability.PartialCommandDetection)?.clearCommands();
778779
this.raw.clear();
779780
// xterm.js does not clear the first prompt, so trigger these to simulate
780781
// the prompt being written

src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { workbenchInstantiationService } from '../../../../../test/browser/workb
1616
type TestTerminalCommandMatch = Pick<ITerminalCommand, 'command' | 'cwd' | 'exitCode'> & { marker: { line: number } };
1717

1818
class TestCommandDetectionCapability extends CommandDetectionCapability {
19-
clearCommands() {
19+
clearCommandsForTest() {
2020
this._commands.length = 0;
2121
}
2222
}
@@ -41,7 +41,7 @@ suite('CommandDetectionCapability', () => {
4141
deepStrictEqual(addEvents, capability.commands);
4242
// Clear the commands to avoid re-asserting past commands
4343
addEvents.length = 0;
44-
capability.clearCommands();
44+
capability.clearCommandsForTest();
4545
}
4646

4747
async function printStandardCommand(prompt: string, command: string, output: string, cwd: string | undefined, exitCode: number) {
@@ -93,6 +93,19 @@ suite('CommandDetectionCapability', () => {
9393
}]);
9494
});
9595

96+
test('should invalidate all commands when cleared', async () => {
97+
await printStandardCommand('$ ', 'echo foo', 'foo', undefined, 0);
98+
await printStandardCommand('$ ', 'echo bar', 'bar', undefined, 0);
99+
strictEqual(capability.commands.length, 2);
100+
101+
const invalidatedCommands: ITerminalCommand[] = [];
102+
store.add(capability.onCommandInvalidated(commands => invalidatedCommands.push(...commands)));
103+
capability.clearCommands();
104+
105+
deepStrictEqual(capability.commands, []);
106+
deepStrictEqual(invalidatedCommands.map(e => e.command), ['echo foo', 'echo bar']);
107+
});
108+
96109
test('should trim the command when command executed appears on the following line', async () => {
97110
await printStandardCommand('$ ', 'echo foo\r\n', 'foo', undefined, 0);
98111
await printCommandStart('$ ');

src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import type { IMarker, Terminal } from '@xterm/xterm';
7-
import { deepEqual, deepStrictEqual } from 'assert';
7+
import { deepEqual, deepStrictEqual, strictEqual } from 'assert';
88
import { importAMDNodeModule } from '../../../../../../amdX.js';
99
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
1010
import { PartialCommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/partialCommandDetectionCapability.js';
@@ -66,16 +66,18 @@ suite('PartialCommandDetectionCapability', () => {
6666
deepEqual(addEvents.length, 2);
6767
});
6868

69-
test('should clear commands in the viewport', async () => {
69+
test('should clear all commands including scrollback', async () => {
7070
await writeP(xterm, 'ab');
7171
xterm.input('\x0d');
7272
await writeP(xterm, '\r\n\r\n');
7373
await writeP(xterm, 'cd');
7474
xterm.input('\x0d');
7575
await writeP(xterm, '\r\n');
7676
deepStrictEqual(capability.commands.map(e => e.line), [0, 2]);
77+
await writeP(xterm, 'line\r\n'.repeat(xterm.rows));
78+
strictEqual(xterm.buffer.active.baseY > 0, true);
7779

78-
capability.clearCommandsInViewport();
80+
capability.clearCommands();
7981

8082
deepStrictEqual(capability.commands, []);
8183
});

src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { Terminal } from '@xterm/xterm';
6+
import type { IDecoration, IDecorationOptions, Terminal } from '@xterm/xterm';
77
import { deepStrictEqual, ok, strictEqual } from 'assert';
88
import { importAMDNodeModule } from '../../../../../../amdX.js';
99
import { Color, RGBA } from '../../../../../../base/common/color.js';
@@ -12,6 +12,9 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/
1212
import { IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';
1313
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
1414
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
15+
import { ITerminalCommand, TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
16+
import { CommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js';
17+
import { PartialCommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/partialCommandDetectionCapability.js';
1518
import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js';
1619
import { IThemeService } from '../../../../../../platform/theme/common/themeService.js';
1720
import { TestColorTheme, TestThemeService } from '../../../../../../platform/theme/test/common/testThemeService.js';
@@ -53,7 +56,11 @@ const defaultTerminalConfig: Partial<ITerminalConfiguration> = {
5356
scrollback: 10,
5457
fastScrollSensitivity: 2,
5558
mouseWheelScrollSensitivity: 1,
56-
unicodeVersion: '6'
59+
unicodeVersion: '6',
60+
shellIntegration: {
61+
enabled: true,
62+
decorationsEnabled: 'both'
63+
}
5764
};
5865

5966
suite('XtermTerminal', () => {
@@ -64,6 +71,7 @@ suite('XtermTerminal', () => {
6471
let themeService: TestThemeService;
6572
let xterm: XtermTerminal;
6673
let XTermBaseCtor: typeof Terminal;
74+
let capabilityStore: TerminalCapabilityStore;
6775

6876
function write(data: string): Promise<void> {
6977
return new Promise<void>((resolve) => {
@@ -90,7 +98,7 @@ suite('XtermTerminal', () => {
9098

9199
XTermBaseCtor = (await importAMDNodeModule<typeof import('@xterm/xterm')>('@xterm/xterm', 'lib/xterm.js')).Terminal;
92100

93-
const capabilityStore = store.add(new TerminalCapabilityStore());
101+
capabilityStore = store.add(new TerminalCapabilityStore());
94102
xterm = store.add(instantiationService.createInstance(XtermTerminal, undefined, XTermBaseCtor, {
95103
cols: 80,
96104
rows: 30,
@@ -109,6 +117,79 @@ suite('XtermTerminal', () => {
109117
strictEqual(xterm.raw.rows, 30);
110118
});
111119

120+
test('clearBuffer should clear rich and partial command history including scrollback', async () => {
121+
class TestTerminal extends XTermBaseCtor {
122+
override registerDecoration(options: IDecorationOptions): IDecoration | undefined {
123+
const disposeListeners = new Set<() => unknown>();
124+
let isDisposed = false;
125+
return {
126+
marker: options.marker,
127+
options,
128+
get isDisposed() { return isDisposed; },
129+
dispose: () => {
130+
isDisposed = true;
131+
for (const listener of disposeListeners) {
132+
listener();
133+
}
134+
disposeListeners.clear();
135+
},
136+
onDispose: (listener: () => unknown) => {
137+
disposeListeners.add(listener);
138+
return { dispose: () => disposeListeners.delete(listener) };
139+
},
140+
onRender: (listener: (element: HTMLElement) => unknown) => {
141+
listener(document.createElement('div'));
142+
return { dispose() { } };
143+
}
144+
} as unknown as IDecoration;
145+
}
146+
}
147+
capabilityStore = store.add(new TerminalCapabilityStore());
148+
xterm = store.add(instantiationService.createInstance(XtermTerminal, undefined, TestTerminal, {
149+
cols: 80,
150+
rows: 30,
151+
xtermColorProvider: { getBackgroundColor: () => undefined },
152+
capabilities: capabilityStore,
153+
disableShellIntegrationReporting: true,
154+
xtermAddonImporter: new TestXtermAddonImporter(),
155+
}, undefined));
156+
const commandDetection = store.add(instantiationService.createInstance(CommandDetectionCapability, xterm.raw));
157+
const onDidExecuteText = store.add(new Emitter<void>());
158+
const partialCommandDetection = store.add(new PartialCommandDetectionCapability(xterm.raw, onDidExecuteText.event));
159+
capabilityStore.add(TerminalCapability.CommandDetection, commandDetection);
160+
capabilityStore.add(TerminalCapability.PartialCommandDetection, partialCommandDetection);
161+
162+
xterm.raw.registerMarker(0);
163+
commandDetection.handlePromptStart();
164+
await write('$ ');
165+
commandDetection.handleCommandStart();
166+
await write('echo test');
167+
commandDetection.handleCommandExecuted();
168+
await write('\r\noutput\r\n');
169+
commandDetection.handleCommandFinished(0);
170+
171+
await write('partial');
172+
xterm.raw.input('\r');
173+
await write('\r\n');
174+
await write('line\r\n'.repeat(xterm.raw.rows));
175+
176+
strictEqual(xterm.raw.buffer.active.baseY > 0, true);
177+
strictEqual(commandDetection.commands.length, 1);
178+
strictEqual(partialCommandDetection.commands.length, 1);
179+
const decorations = (xterm.decorationAddon as unknown as { _decorations: Map<number, unknown> })._decorations;
180+
const clearedCommandMarkerId = commandDetection.commands[0].marker!.id;
181+
strictEqual(decorations.has(clearedCommandMarkerId), true);
182+
const invalidatedCommands: ITerminalCommand[] = [];
183+
store.add(commandDetection.onCommandInvalidated(commands => invalidatedCommands.push(...commands)));
184+
185+
xterm.clearBuffer();
186+
187+
deepStrictEqual(commandDetection.commands, []);
188+
deepStrictEqual(partialCommandDetection.commands, []);
189+
deepStrictEqual(invalidatedCommands.map(e => e.command), ['echo test']);
190+
strictEqual(decorations.has(clearedCommandMarkerId), false);
191+
});
192+
112193
suite('getContentsAsText', () => {
113194
test('should return all buffer contents when no markers provided', async () => {
114195
await write('line 1\r\nline 2\r\nline 3\r\nline 4\r\nline 5');

0 commit comments

Comments
 (0)