From 74f288831db201d97ccb943ddf569dfa649102c7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:16:36 -0700 Subject: [PATCH] Detect ghost text in input Part of #210662 --- .../commandDetection/promptInputModel.ts | 80 +++++++++++++++++-- .../terminal.developer.contribution.ts | 8 +- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts index 7131e0386ad71..02ad46c9fdde8 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/promptInputModel.ts @@ -11,7 +11,7 @@ import { debounce } from 'vs/base/common/decorators'; // Importing types is safe in any layer // eslint-disable-next-line local/code-import-patterns -import type { Terminal, IMarker, IBufferLine, IBuffer } from '@xterm/headless'; +import type { Terminal, IMarker, IBufferCell, IBufferLine, IBuffer } from '@xterm/headless'; const enum PromptInputState { Unknown, @@ -26,6 +26,13 @@ export interface IPromptInputModel { readonly value: string; readonly cursorIndex: number; + readonly ghostTextIndex: number; + + /** + * Gets the prompt input as a user-friendly string where `|` is the cursor position and `[` and + * `]` wrap any ghost text. + */ + getCombinedString(): string; } export class PromptInputModel extends Disposable implements IPromptInputModel { @@ -41,6 +48,9 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { private _cursorIndex: number = 0; get cursorIndex() { return this._cursorIndex; } + private _ghostTextIndex: number = -1; + get ghostTextIndex() { return this._ghostTextIndex; } + private readonly _onDidStartInput = this._register(new Emitter()); readonly onDidStartInput = this._onDidStartInput.event; private readonly _onDidChangeInput = this._register(new Emitter()); @@ -67,6 +77,18 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { this._continuationPrompt = value; } + getCombinedString(): string { + const value = this._value.replaceAll('\n', '\u23CE'); + let result = `${value.substring(0, this.cursorIndex)}|`; + if (this.ghostTextIndex !== -1) { + result += `${value.substring(this.cursorIndex, this.ghostTextIndex)}[`; + result += `${value.substring(this.ghostTextIndex)}]`; + } else { + result += value.substring(this.cursorIndex); + } + return result; + } + private _handleCommandStart(command: { marker: IMarker }) { if (this._state === PromptInputState.Input) { return; @@ -111,7 +133,7 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { const buffer = this._xterm.buffer.active; let line = buffer.getLine(commandStartY); const commandLine = line?.translateToString(true, this._commandStartX); - if (!commandLine || !line) { + if (!line || commandLine === undefined) { this._logService.trace(`PromptInputModel#_sync: no line`); return; } @@ -122,9 +144,14 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { // Get cursor index const absoluteCursorY = buffer.baseY + buffer.cursorY; this._cursorIndex = absoluteCursorY === commandStartY ? this._getRelativeCursorIndex(this._commandStartX, buffer, line) : commandLine.length + 1; + this._ghostTextIndex = -1; + + // Detect ghost text by looking for italic or dim text in or after the cursor and + // non-italic/dim text in the cell closest non-whitespace cell before the cursor + if (absoluteCursorY === commandStartY && buffer.cursorX > 1) { + this._ghostTextIndex = this._scanForGhostText(buffer, line); + } - // IDEA: Detect ghost text based on SGR and cursor. This might work by checking for italic - // or dim only to avoid false positives from shells that do immediate coloring. // IDEA: Detect line continuation if it's not set // From command start line to cursor line @@ -160,12 +187,51 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { } if (this._logService.getLevel() === LogLevel.Trace) { - this._logService.trace(`PromptInputModel#_sync: Input="${this._value.substring(0, this._cursorIndex)}|${this.value.substring(this._cursorIndex)}"`); + this._logService.trace(`PromptInputModel#_sync: ${this.getCombinedString()}`); } this._onDidChangeInput.fire(); } + /** + * Detect ghost text by looking for italic or dim text in or after the cursor and + * non-italic/dim text in the cell closest non-whitespace cell before the cursor. + */ + private _scanForGhostText(buffer: IBuffer, line: IBufferLine): number { + // Check last non-whitespace character has non-ghost text styles + let ghostTextIndex = -1; + let proceedWithGhostTextCheck = false; + let x = buffer.cursorX; + while (x > 1) { + const cell = line.getCell(--x); + if (!cell) { + break; + } + if (cell.getChars().trim().length > 0) { + proceedWithGhostTextCheck = !this._isCellStyledLikeGhostText(cell); + break; + } + } + + // Check to the end of the line for possible ghost text. For example pwsh's ghost text + // can look like this `Get-|Ch[ildItem]` + if (proceedWithGhostTextCheck) { + let x = buffer.cursorX; + while (x < line.length) { + const cell = line.getCell(x++); + if (!cell || cell.getCode() === 0) { + break; + } + if (this._isCellStyledLikeGhostText(cell)) { + ghostTextIndex = this._cursorIndex; + break; + } + } + } + + return ghostTextIndex; + } + private _trimContinuationPrompt(lineText: string): string { if (this._lineContainsContinuationPrompt(lineText)) { lineText = lineText.substring(this._continuationPrompt!.length); @@ -192,4 +258,8 @@ export class PromptInputModel extends Disposable implements IPromptInputModel { private _getRelativeCursorIndex(startCellX: number, buffer: IBuffer, line: IBufferLine): number { return line?.translateToString(true, startCellX, buffer.cursorX).length ?? 0; } + + private _isCellStyledLikeGhostText(cell: IBufferCell): boolean { + return !!(cell.isItalic() || cell.isDim()); + } } diff --git a/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts b/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts index 444a671795517..744cf3622c356 100644 --- a/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/developer/browser/terminal.developer.contribution.ts @@ -245,11 +245,11 @@ class DevModeContribution extends Disposable implements ITerminalContribution { private _updatePromptInputStatusBar(commandDetection: ICommandDetectionCapability) { const promptInputModel = commandDetection.promptInputModel; if (promptInputModel) { - const promptInput = promptInputModel.value.replaceAll('\n', '\u23CE'); + const name = localize('terminalDevMode', 'Terminal Dev Mode'); this._statusbarEntry = { - name: localize('terminalDevMode', 'Terminal Dev Mode'), - text: `$(terminal) ${promptInput.substring(0, promptInputModel.cursorIndex)}|${promptInput.substring(promptInputModel.cursorIndex)}`, - ariaLabel: localize('terminalDevMode', 'Terminal Dev Mode'), + name, + text: `$(terminal) ${promptInputModel.getCombinedString()}`, + ariaLabel: name, kind: 'prominent' }; if (!this._statusbarEntryAccessor.value) {