promptInputModel.ts ×40

Frontier kind: Code frontier

unlabeled · c_b11f156800ea

44 tests · 12543 LOC · 60 files · introduces 0 tests · 336 LOC · 2 files

Introduces — evidence that enters the hierarchy at this concept

Code
43 ranges336 lines · 2 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1847 ranges12543 lines · 60 files · Browse complete extent
All tests (intent)
44 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

2 files ranked by introduced lines: 336 introduced LOC across 43 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts 319 introduced LOC · 40 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- promptInputModel.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { IBuffer, IBufferCell, IBufferLine, IMarker, Terminal } from '@xterm/headless';
7 > import { throttle } from '../../../../../base/common/decorators.js';
8 > import { Emitter, Event } from '../../../../../base/common/event.js';
9 > import { Disposable } from '../../../../../base/common/lifecycle.js';
10 > import { ILogService, LogLevel } from '../../../../log/common/log.js';
11 > import { PosixShellType, TerminalShellType } from '../../terminal.js';
12 > import type { ITerminalCommand } from '../capabilities.js';
13 >
14 > export const enum PromptInputState {
15 > Unknown = 0,
16 > Input = 1,
17 > Execute = 2,
18 > }
19 >
20 > /**
21 > * A model of the prompt input state using shell integration and analyzing the terminal buffer. This
22 > * may not be 100% accurate but provides a best guess.
23 > */
24 > export interface IPromptInputModel extends IPromptInputModelState {
25 > readonly state: PromptInputState;
26 >
27 > readonly onDidStartInput: Event<IPromptInputModelState>;
28 > readonly onDidChangeInput: Event<IPromptInputModelState>;
29 > readonly onDidFinishInput: Event<IPromptInputModelState>;
30 > /**
31 > * Fires immediately before {@link onDidFinishInput} when a SIGINT/Ctrl+C/^C is detected.
32 > */
33 > readonly onDidInterrupt: Event<IPromptInputModelState>;
34 >
35 > /**
36 > * Gets the prompt input as a user-friendly string where `|` is the cursor position and `[` and
37 > * `]` wrap any ghost text.
38 > *
39 > * @param emptyStringWhenEmpty If true, an empty string is returned when the prompt input is
40 > * empty (as opposed to '|').
41 > */
42 > getCombinedString(emptyStringWhenEmpty?: boolean): string;
43 >
44 > setShellType(shellType?: TerminalShellType): void;
45 > }
46 >
47 > export interface IPromptInputModelState {
48 > /**
49 > * The full prompt input include ghost text.
50 > */
51 > readonly value: string;
52 > /**
53 > * The prompt input up to the cursor index, this will always exclude the ghost text.
54 > */
55 > readonly prefix: string;
56 > /**
57 > * The prompt input from the cursor to the end, this _does not_ include ghost text.
58 > */
59 > readonly suffix: string;
60 > /**
61 > * The index of the cursor in {@link value}.
62 > */
63 > readonly cursorIndex: number;
64 > /**
65 > * The index of the start of ghost text in {@link value}. This is -1 when there is no ghost
66 > * text.
67 > */
68 > readonly ghostTextIndex: number;
69 > }
70 >
71 > export interface ISerializedPromptInputModel {
72 > readonly modelState: IPromptInputModelState;
73 > readonly commandStartX: number;
74 > readonly lastPromptLine: string | undefined;
75 > readonly continuationPrompt: string | undefined;
76 > readonly lastUserInput: string;
77 > }
78 >
79 > export class PromptInputModel extends Disposable implements IPromptInputModel {
80 > private _state: PromptInputState = PromptInputState.Unknown;
81 > get state() { return this._state; }
82 >
83 > private _commandStartMarker: IMarker | undefined;
84 > private _commandStartX: number = 0;
85 > private _lastPromptLine: string | undefined;
86 > private _continuationPrompt: string | undefined;
87 > private _shellType: TerminalShellType | undefined;
88 >
89 > private _lastUserInput: string = '';
90 >
91 > private _value: string = '';
92 > get value() { return this._value; }
93 > get prefix() { return this._value.substring(0, this._cursorIndex); }
94 > get suffix() { return this._value.substring(this._cursorIndex, this._ghostTextIndex === -1 ? undefined : this._ghostTextIndex); }
95 >
96 > private _cursorIndex: number = 0;
97 > get cursorIndex() { return this._cursorIndex; }
98 >
99 > private _ghostTextIndex: number = -1;
100 > get ghostTextIndex() { return this._ghostTextIndex; }
101 >
102 > private readonly _onDidStartInput = this._register(new Emitter<IPromptInputModelState>());
103 > readonly onDidStartInput = this._onDidStartInput.event;
104 > private readonly _onDidChangeInput = this._register(new Emitter<IPromptInputModelState>());
105 > readonly onDidChangeInput = this._onDidChangeInput.event;
106 > private readonly _onDidFinishInput = this._register(new Emitter<IPromptInputModelState>());
107 > readonly onDidFinishInput = this._onDidFinishInput.event;
108 > private readonly _onDidInterrupt = this._register(new Emitter<IPromptInputModelState>());
109 > readonly onDidInterrupt = this._onDidInterrupt.event;
110 >
111 > constructor(
112 > private readonly _xterm: Terminal,
113 > onCommandStart: Event<ITerminalCommand>,
114 > onCommandStartChanged: Event<void>,
115 > onCommandExecuted: Event<ITerminalCommand>,
116 > onCommandFinished: Event<ITerminalCommand>,
117 > @ILogService private readonly _logService: ILogService
118 > ) {
119 > super();
120 >
121 > this._register(Event.any(
122 > this._xterm.onCursorMove,
123 > this._xterm.onData,
124 > this._xterm.onWriteParsed,
125 > )(() => this._sync()));
126 > this._register(this._xterm.onData(e => this._handleUserInput(e)));
127 >
128 > this._register(onCommandStart(e => this._handleCommandStart(e as { marker: IMarker })));
129 > this._register(onCommandStartChanged(() => this._handleCommandStartChanged()));
130 > this._register(onCommandExecuted(() => this._handleCommandExecuted()));
131 > this._register(onCommandFinished(() => this._handleCommandFinished()));
132 >
133 > this._register(this.onDidStartInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidStartInput')));
134 > this._register(this.onDidChangeInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidChangeInput')));
135 > this._register(this.onDidFinishInput(() => this._logCombinedStringIfTrace('PromptInputModel#onDidFinishInput')));
136 > this._register(this.onDidInterrupt(() => this._logCombinedStringIfTrace('PromptInputModel#onDidInterrupt')));
137 > }
138 >
139 > private _logCombinedStringIfTrace(message: string) {
140 > // Only generate the combined string if trace
141 > if (this._logService.getLevel() === LogLevel.Trace) {
142 this._logService.trace(message, this.getCombinedString());
143 }
145 >
146 > setShellType(shellType: TerminalShellType): void {
147 this._shellType = shellType;
148 }
150 > setContinuationPrompt(value: string): void {
151 this._continuationPrompt = value;
152 this._sync();
153 }
155 > setLastPromptLine(value: string): void {
156 this._lastPromptLine = value;
157 this._sync();
158 }
160 > setConfidentCommandLine(value: string): void {
161 if (this._value !== value) {
162 this._value = value;
166 }
167 }
169 > getCombinedString(emptyStringWhenEmpty?: boolean): string {
170 > const value = this._value.replaceAll('\n', '\u23CE');
171 > if (this._cursorIndex === -1) {
172 return value;
173 }
174 > let result = `${value.substring(0, this.cursorIndex)}|`; promptInputModel.ts
175 > if (this.ghostTextIndex !== -1) {
176 result += `${value.substring(this.cursorIndex, this.ghostTextIndex)}[`;
177 result += `${value.substring(this.ghostTextIndex)}]`;
178 > } else { promptInputModel.ts
179 > result += value.substring(this.cursorIndex);
180 > }
181 > if (result === '|' && emptyStringWhenEmpty) {
182 return '';
183 }
184 > return result; promptInputModel.ts
185 > }
186 >
187 > serialize(): ISerializedPromptInputModel {
188 return {
189 modelState: this._createStateObject(),
194 };
195 }
197 > deserialize(serialized: ISerializedPromptInputModel): void {
198 this._value = serialized.modelState.value;
199 this._cursorIndex = serialized.modelState.cursorIndex;
204 this._lastUserInput = serialized.lastUserInput;
205 }
207 > private _handleCommandStart(command: { marker: IMarker }) {
208 > if (this._state === PromptInputState.Input) {
209 return;
210 }
212 > this._state = PromptInputState.Input;
213 > this._commandStartMarker = command.marker;
214 > this._commandStartX = this._xterm.buffer.active.cursorX;
215 > this._value = '';
216 > this._cursorIndex = 0;
217 > this._onDidStartInput.fire(this._createStateObject());
218 > this._onDidChangeInput.fire(this._createStateObject());
219 >
220 > // Trigger a sync if prompt terminator is set as that could adjust the command start X
221 > if (this._lastPromptLine) {
222 if (this._commandStartX !== this._lastPromptLine.length) {
223 const line = this._xterm.buffer.active.getLine(this._commandStartMarker.line);
228 }
229 }
231 >
232 > private _handleCommandStartChanged() {
233 if (this._state !== PromptInputState.Input) {
234 return;
239 this._sync();
240 }
242 > private _handleCommandExecuted() {
243 if (this._state === PromptInputState.Execute) {
244 return;
263 this._onDidChangeInput.fire(event);
264 }
266 > private _handleCommandFinished() {
267 // Clear the prompt input value when command finishes to prepare for the next command
268 // This prevents runCommand from detecting leftover text and sending ^C unnecessarily
270 this._onDidChangeInput.fire(this._createStateObject());
271 }
273 > @throttle(0)
274 > private _sync() {
275 > try {
276 > this._doSync();
277 > } catch (e) {
278 this._logService.error('Error while syncing prompt input model', e);
279 }
281 >
282 > private _doSync() {
283 > if (this._state !== PromptInputState.Input) {
284 > return;
285 > }
286 >
287 > let commandStartY = this._commandStartMarker?.line;
288 > if (commandStartY === undefined) {
289 return;
290 }
292 > const buffer = this._xterm.buffer.active;
293 > let line = buffer.getLine(commandStartY);
294 > const absoluteCursorY = buffer.baseY + buffer.cursorY;
295 > let cursorIndex: number | undefined;
296 >
297 > let commandLine = line?.translateToString(true, this._commandStartX);
298 > if (this._shellType === PosixShellType.Fish && (!line || !commandLine)) {
299 commandStartY += 1;
300 line = buffer.getLine(commandStartY);
304 }
305 }
306 > if (line === undefined || commandLine === undefined) { promptInputModel.ts
307 this._logService.trace(`PromptInputModel#_sync: no line`);
308 return;
309 }
311 > let value = commandLine;
312 > let ghostTextIndex = -1;
313 > if (cursorIndex === undefined) {
314 > if (absoluteCursorY === commandStartY) {
315 > cursorIndex = Math.min(this._getRelativeCursorIndex(this._commandStartX, buffer, line), commandLine.length);
316 > } else {
317 cursorIndex = commandLine.trimEnd().length;
318 }
320 >
321 > // From command start line to cursor line
322 > for (let y = commandStartY + 1; y <= absoluteCursorY; y++) {
323 const nextLine = buffer.getLine(y);
324 const lineText = nextLine?.translateToString(true);
366 }
367 }
369 > // Below cursor line
370 > for (let y = absoluteCursorY + 1; y < buffer.baseY + this._xterm.rows; y++) {
371 > const belowCursorLine = buffer.getLine(y);
372 > const lineText = belowCursorLine?.translateToString(true);
373 > if (lineText && belowCursorLine) {
374 if (this._shellType === PosixShellType.Fish) {
375 value += `${lineText}`;
379 value += lineText;
380 }
381 > } else { promptInputModel.ts
382 > break;
383 > }
384 > }
385 >
386 > if (this._logService.getLevel() === LogLevel.Trace) {
387 this._logService.trace(`PromptInputModel#_sync: ${this.getCombinedString()}`);
388 }
390 > // Adjust trailing whitespace
391 > {
392 > let trailingWhitespace = this._value.length - this._value.trimEnd().length;
393 >
394 > // Handle backspace key
395 > if (this._lastUserInput === '\x7F') {
396 this._lastUserInput = '';
397 if (cursorIndex === this._cursorIndex - 1) {
407 }
408 }
410 > // Handle delete key
411 > if (this._lastUserInput === '\x1b[3~') {
412 this._lastUserInput = '';
413 if (cursorIndex === this._cursorIndex) {
415 }
416 }
418 > const valueLines = value.split('\n');
419 > const isMultiLine = valueLines.length > 1;
420 > const valueEndTrimmed = value.trimEnd();
421 > if (!isMultiLine) {
422 > // Adjust trimmed whitespace value based on cursor position
423 > if (valueEndTrimmed.length < value.length) {
424 // Handle space key
425 if (this._lastUserInput === ' ') {
431 trailingWhitespace = Math.max(cursorIndex - valueEndTrimmed.length, trailingWhitespace, 0);
432 }
434 > // Handle case where a non-space character is inserted in the middle of trailing whitespace
435 > const charBeforeCursor = cursorIndex === 0 ? '' : value[cursorIndex - 1];
436 > if (trailingWhitespace > 0 && cursorIndex === this._cursorIndex + 1 && this._lastUserInput !== '' && charBeforeCursor !== ' ') {
437 trailingWhitespace = this._value.length - this._cursorIndex;
438 }
440 >
441 > if (isMultiLine) {
442 valueLines[valueLines.length - 1] = valueLines.at(-1)?.trimEnd() ?? '';
443 const continuationOffset = (valueLines.length - 1) * (this._continuationPrompt?.length ?? 0);
444 trailingWhitespace = Math.max(0, cursorIndex - value.length - continuationOffset);
445 }
447 > value = valueLines.map(e => e.trimEnd()).join('\n') + ' '.repeat(trailingWhitespace);
448 > }
449 >
450 > ghostTextIndex = this._scanForGhostText(buffer, line, cursorIndex);
451 >
452 > if (this._value !== value || this._cursorIndex !== cursorIndex || this._ghostTextIndex !== ghostTextIndex) {
453 > this._value = value;
454 > this._cursorIndex = cursorIndex;
455 > this._ghostTextIndex = ghostTextIndex;
456 > this._onDidChangeInput.fire(this._createStateObject());
457 > }
458 > }
459 >
460 > private _handleUserInput(e: string) {
461 this._lastUserInput = e;
462 }
464 > /**
465 > * Detect ghost text by looking for italic or dim text in or after the cursor and
466 > * non-italic/dim text in the first non-whitespace cell following command start and before the cursor.
467 > */
468 > private _scanForGhostText(buffer: IBuffer, line: IBufferLine, cursorIndex: number): number {
469 > if (!this.value.trim().length) {
470 > return -1;
471 > }
472 // Check last non-whitespace character has non-ghost text styles
473 let ghostTextIndex = -1;
511 }
512
513 > if (ghostTextIndex > -1 && this.value.substring(ghostTextIndex).endsWith(' ')) { promptInputModel.ts
514 this._value = this.value.trim();
515 if (!this.value.substring(ghostTextIndex)) {
518 }
519 return ghostTextIndex;
521 >
522 > private _scanForGhostTextAdvanced(buffer: IBuffer, line: IBufferLine, cursorIndex: number): number {
523 let ghostTextIndex = -1;
524 let currentPos = buffer.cursorX; // Start scanning from the cursor position
592 return ghostTextIndex >= cursorIndex ? ghostTextIndex : -1;
593 }
595 > /**
596 > * 5+ spaces preceding the position, following the command start,
597 > * indicates that we're likely in a right prompt at the current position
598 > */
599 > private _isPositionRightPrompt(line: IBufferLine, position: number): boolean {
600 let count = 0;
601 for (let i = position - 1; i >= this._commandStartX; i--) {
615 return false;
616 }
618 > private _getCellStyleAsString(cell: IBufferCell): string {
619 return `${cell.getFgColor()}${cell.getBgColor()}${cell.isBold()}${cell.isItalic()}${cell.isDim()}${cell.isUnderline()}${cell.isBlink()}${cell.isInverse()}${cell.isInvisible()}${cell.isStrikethrough()}${cell.isOverline()}${cell.getFgColorMode()}${cell.getBgColorMode()}`;
620 }
622 > private _cellStylesMatch(a: IBufferCell | undefined, b: IBufferCell | undefined): boolean {
623 if (!a || !b) {
624 return false;
638 && a?.getFgColorMode() === b?.getFgColorMode();
639 }
641 > private _trimContinuationPrompt(lineText: string): string {
642 if (this._lineContainsContinuationPrompt(lineText)) {
643 lineText = lineText.substring(this._continuationPrompt!.length);
645 return lineText;
646 }
648 > private _lineContainsContinuationPrompt(lineText: string): boolean {
649 return !!(this._continuationPrompt && lineText.startsWith(this._continuationPrompt.trimEnd()));
650 }
652 > private _getContinuationPromptCellWidth(line: IBufferLine, lineText: string): number {
653 if (!this._continuationPrompt || !lineText.startsWith(this._continuationPrompt.trimEnd())) {
654 return 0;
666 return x;
667 }
669 > private _getRelativeCursorIndex(startCellX: number, buffer: IBuffer, line: IBufferLine): number {
670 > return line?.translateToString(false, startCellX, buffer.cursorX).length ?? 0;
671 > }
672 >
673 > private _isCellStyledLikeGhostText(cell: IBufferCell): boolean {
674 return !!(cell.isItalic() || cell.isDim());
675 }
677 > private _createStateObject(): IPromptInputModelState {
678 > return Object.freeze({
679 > value: this._value,
680 > prefix: this.prefix,
681 > suffix: this.suffix,
682 > cursorIndex: this._cursorIndex,
683 > ghostTextIndex: this._ghostTextIndex
684 > });
685 > }
686 > }
src/vs/platform/terminal/test/common/terminalTestHelpers.ts 17 introduced LOC · 3 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- terminalTestHelpers.ts
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { ILogger } from '@xterm/headless';
7 >
8 > /**
9 > * A logger for xterm.js that suppresses noisy warnings during tests.
10 > */
11 > export const TestXtermLogger: ILogger = {
12 > trace: () => { },
13 > debug: () => { },
14 > info: () => { },
15 > warn: (message: string) => {
16 if (message.includes('task queue')) {
17 return;