diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 546ebc1be620b..60ce986cf4bca 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -182,6 +182,7 @@ export interface ICommandDetectionCapability { readonly onCurrentCommandInvalidated: Event; setContinuationPrompt(value: string): void; setCwd(value: string): void; + setPromptHeight(value: number): void; setIsWindowsPty(value: boolean): void; setIsCommandStorageDisabled(): void; /** diff --git a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts index b0ecb40815559..46c68b14b1352 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts @@ -213,6 +213,7 @@ export class TerminalCommand implements ITerminalCommand { export interface ICurrentPartialCommand { promptStartMarker?: IMarker; + promptHeight?: number; commandStartMarker?: IMarker; commandStartX?: number; @@ -244,12 +245,23 @@ export interface ICurrentPartialCommand { */ isInvalid?: boolean; + /** + * Whether the command start marker has been adjusted on Windows. + */ + isAdjusted?: boolean; + + /** + * Whether the command start marker adjustment has been attempt on new terminal input. + */ + isInputAdjusted?: boolean; + getPromptRowCount(): number; getCommandRowCount(): number; } export class PartialTerminalCommand implements ICurrentPartialCommand { promptStartMarker?: IMarker; + promptHeight?: number; commandStartMarker?: IMarker; commandStartX?: number; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index b6f604d9e883b..6180a10172d82 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -154,6 +154,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe }; this._register(this._terminal.onResize(e => this._handleResize(e))); this._register(this._terminal.onCursorMove(() => this._handleCursorMove())); + this._register(this._terminal.onData(() => this._handleInput())); } private _handleResize(e: { cols: number; rows: number }) { @@ -207,6 +208,10 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._cwd = value; } + setPromptHeight(value: number) { + this._currentCommand.promptHeight = value; + } + setIsWindowsPty(value: boolean) { if (value && !(this._ptyHeuristics.value instanceof WindowsPtyHeuristics)) { const that = this; @@ -280,6 +285,10 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe return undefined; } + private _handleInput(): void { + this._ptyHeuristics.value.handleInput(); + } + handlePromptStart(options?: IHandleCommandOptions): void { // Adjust the last command's finished marker when needed. The standard position for the // finished marker `D` to appear is at the same position as the following prompt started @@ -499,6 +508,8 @@ class UnixPtyHeuristics extends Disposable { })); } + handleInput() { } + handleCommandStart(options?: IHandleCommandOptions) { this._hooks.commitCommandFinished(); @@ -652,6 +663,28 @@ class WindowsPtyHeuristics extends Disposable { } } + /** + * Attempt to adjust the command start marker when input is handled for the first time. + */ + handleInput() { + const currentY = this._terminal.buffer.active.baseY + this._terminal.buffer.active.cursorY; + + const hasWrappingInPrompt = Array.from({ length: (this._capability.currentCommand.promptHeight ?? 0) + 1 }, (_, i) => currentY - i).find(y => this._terminal.buffer.active.getLine(y)?.isWrapped) !== undefined; + const hasActiveCommand = this._capability.currentCommand.commandStartX !== undefined && this._capability.currentCommand.commandExecutedX === undefined; + const hasAdjusted = this._capability.currentCommand.isAdjusted === true || this._capability.currentCommand.isInputAdjusted === true; + + if (!hasActiveCommand || hasAdjusted || hasWrappingInPrompt) { + return; + } + this._capability.currentCommand.isInputAdjusted = true; + this._logService.debug('CommandDetectionCapability#handleInput attempting start marker adjustment'); + + this._tryAdjustCommandStartMarkerScannedLineCount = 0; + this._tryAdjustCommandStartMarkerPollCount = 0; + this._tryAdjustCommandStartMarkerScheduler = new RunOnceScheduler(() => this._tryAdjustCommandStartMarker(this._terminal.registerMarker(0)!), AdjustCommandStartMarkerConstants.Interval); + this._tryAdjustCommandStartMarkerScheduler.schedule(); + } + handleCommandStart() { this._capability.currentCommand.commandStartX = this._terminal.buffer.active.cursorX; @@ -713,21 +746,24 @@ class WindowsPtyHeuristics extends Disposable { if (prompt) { const adjustedPrompt = typeof prompt === 'string' ? prompt : prompt.prompt; this._capability.currentCommand.commandStartMarker = this._terminal.registerMarker(0)!; - if (typeof prompt === 'object' && prompt.likelySingleLine) { - this._logService.debug('CommandDetectionCapability#_tryAdjustCommandStartMarker adjusted promptStart', `${this._capability.currentCommand.promptStartMarker?.line} -> ${this._capability.currentCommand.commandStartMarker.line}`); - this._capability.currentCommand.promptStartMarker?.dispose(); - this._capability.currentCommand.promptStartMarker = cloneMarker(this._terminal, this._capability.currentCommand.commandStartMarker); - // Adjust the last command if it's not in the same position as the following - // prompt start marker - const lastCommand = this._capability.commands.at(-1); - if (lastCommand && this._capability.currentCommand.commandStartMarker.line !== lastCommand.endMarker?.line) { - lastCommand.endMarker?.dispose(); - lastCommand.endMarker = cloneMarker(this._terminal, this._capability.currentCommand.commandStartMarker); - } + + // Adjust the prompt start marker to the command start marker + this._logService.debug('CommandDetectionCapability#_tryAdjustCommandStartMarker adjusted promptStart', `${this._capability.currentCommand.promptStartMarker?.line} -> ${this._capability.currentCommand.commandStartMarker.line}`); + this._capability.currentCommand.promptStartMarker?.dispose(); + this._capability.currentCommand.promptStartMarker = cloneMarker(this._terminal, this._capability.currentCommand.commandStartMarker, -((this._capability.currentCommand.promptHeight ?? 1) - 1)); + + // Adjust the last command if it's not in the same position as the following + // prompt start marker + const lastCommand = this._capability.commands.at(-1); + if (lastCommand && this._capability.currentCommand.commandStartMarker.line !== lastCommand.endMarker?.line) { + lastCommand.endMarker?.dispose(); + lastCommand.endMarker = cloneMarker(this._terminal, this._capability.currentCommand.commandStartMarker, -((this._capability.currentCommand.promptHeight ?? 1) - 1)); } + // use the regex to set the position as it's possible input has occurred this._capability.currentCommand.commandStartX = adjustedPrompt.length; this._logService.debug('CommandDetectionCapability#_tryAdjustCommandStartMarker adjusted commandStart', `${start.line} -> ${this._capability.currentCommand.commandStartMarker.line}:${this._capability.currentCommand.commandStartX}`); + this._capability.currentCommand.isAdjusted = true; this._flushPendingHandleCommandStartTask(); return; } @@ -1049,5 +1085,7 @@ function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number } function cloneMarker(xterm: Terminal, marker: IXtermMarker, offset: number = 0): IXtermMarker | undefined { - return xterm.registerMarker(marker.line - (xterm.buffer.active.baseY + xterm.buffer.active.cursorY) + offset); + const cursorY = xterm.buffer.active.baseY + xterm.buffer.active.cursorY; + const cursorYOffset = marker.line - cursorY + offset; + return xterm.registerMarker((cursorY + cursorYOffset) < 0 ? -cursorY : cursorYOffset); } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 249e67ab44dd2..e6cf5eb2de25f 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -403,6 +403,10 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this.capabilities.get(TerminalCapability.CommandDetection)?.setIsCommandStorageDisabled(); return true; } + case 'PromptHeight': { + this.capabilities.get(TerminalCapability.CommandDetection)?.setPromptHeight(parseInt(value)); + return true; + } } } case VSCodeOscPt.SetMark: { diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index cd03a920dbc9b..e226b9a28b991 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -182,6 +182,11 @@ __vsc_update_cwd() { builtin printf '\e]633;P;Cwd=%s\a' "$(__vsc_escape_value "$__vsc_cwd")" } +__vsc_update_prompt_height() { + __vsc_prompt_height="$(("$(builtin printf "%s" "${PS1@P}" | wc -l)" + 1))" + builtin printf '\e]633;P;PromptHeight=%s\a' "$(__vsc_escape_value "$__vsc_prompt_height")" +} + __vsc_command_output_start() { builtin printf '\e]633;E;%s;%s\a' "$(__vsc_escape_value "${__vsc_current_command}")" $__vsc_nonce builtin printf '\e]633;C\a' @@ -229,6 +234,7 @@ __vsc_precmd() { __vsc_command_complete "$__vsc_status" __vsc_current_command="" __vsc_update_prompt + __vsc_update_prompt_height __vsc_first_prompt=1 } diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index 0a620fb4dbcca..90bc9f74efeca 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -52,17 +52,12 @@ if ($env:VSCODE_ENV_APPEND) { function Global:__VSCode-Escape-Value([string]$value) { # NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`. # Replace any non-alphanumeric characters. - $Result = [regex]::Replace($value, '[\\\n;]', { param($match) + [regex]::Replace($value, "[$([char]0x1b)\\\n;]", { param($match) # Encode the (ascii) matches as `\x` -Join ( [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ } ) }) - # `e is only availabel in pwsh 6+ - if ($PSVersionTable.PSVersion.Major -lt 6) { - $Result = $Result -replace "`e", '\x1b' - } - $Result } function Global:Prompt() { @@ -95,7 +90,13 @@ function Global:Prompt() { Write-Error "failure" -ea ignore } # Run the original prompt - $Result += $Global:__VSCodeOriginalPrompt.Invoke() + $OriginalPrompt += $Global:__VSCodeOriginalPrompt.Invoke() + $Result += $OriginalPrompt + + # Prompt height + # OSC 633 ; = ST + $Result += "$([char]0x1b)]633;P;PromptHeight=$(__VSCode-Escape-Value ($OriginalPrompt -Split '\n').Count)`a" + # Write command started $Result += "$([char]0x1b)]633;B`a" $Global:__LastHistoryId = $LastHistoryEntry.Id