From 0bce60799cbdc5c85783b5b028647ecc0a80fa39 Mon Sep 17 00:00:00 2001 From: Chapman Pendery Date: Thu, 25 Apr 2024 11:09:23 -0700 Subject: [PATCH 1/3] refactor: support dynamic windows prompt detection without regex Signed-off-by: Chapman Pendery --- .../common/capabilities/capabilities.ts | 1 + .../commandDetectionCapability.ts | 16 +++++++++++++++- .../common/xterm/shellIntegrationAddon.ts | 19 +++++++++++++++++++ .../browser/media/shellIntegration-bash.sh | 6 ++++++ .../browser/media/shellIntegration.ps1 | 8 +++++++- 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts index 546ebc1be620b..7fd0b01f81a23 100644 --- a/src/vs/platform/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -181,6 +181,7 @@ export interface ICommandDetectionCapability { readonly onCommandInvalidated: Event; readonly onCurrentCommandInvalidated: Event; setContinuationPrompt(value: string): void; + setPromptTerminator(value: string): void; setCwd(value: string): void; setIsWindowsPty(value: boolean): void; setIsCommandStorageDisabled(): void; diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index b6f604d9e883b..b45f4115b35ca 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -30,6 +30,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe protected _commands: TerminalCommand[] = []; private _cwd: string | undefined; + private _promptTerminator: string | undefined; private _currentCommand: PartialTerminalCommand = new PartialTerminalCommand(this._terminal); private _commandMarkers: IMarker[] = []; private _dimensions: ITerminalDimensions; @@ -54,6 +55,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe return this._currentCommand; } get cwd(): string | undefined { return this._cwd; } + get promptTerminator(): string | undefined { return this._promptTerminator } private get _isInputting(): boolean { return !!(this._currentCommand.commandStartMarker && !this._currentCommand.commandExecutedMarker); } @@ -452,6 +454,11 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe this._onCommandFinished.fire(newCommand); } } + + setPromptTerminator(promptTerminator: string) { + this._logService.debug('CommandDetectionCapability#setPromptTerminator', promptTerminator); + this._promptTerminator = promptTerminator; + } } /** @@ -922,12 +929,19 @@ class WindowsPtyHeuristics extends Disposable { if (!line) { return; } - // TODO: fine tune prompt regex to accomodate for unique configurations. const lineText = line.translateToString(true); if (!lineText) { return; } + // Dynamic prompt detection + if (this._capability.promptTerminator && lineText.trim().endsWith(this._capability.promptTerminator)) { + const adjustedPrompt = this._adjustPrompt(lineText, lineText, this._capability.promptTerminator); + if (adjustedPrompt) { + return adjustedPrompt; + } + } + // PowerShell const pwshPrompt = lineText.match(/(?(\(.+\)\s)?(?:PS.+>\s?))/)?.groups?.prompt; if (pwshPrompt) { diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index 249e67ab44dd2..be7f74693796b 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -398,6 +398,15 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._createOrGetCommandDetection(this._terminal).setIsWindowsPty(value === 'True' ? true : false); return true; } + case 'Prompt': { + const sanitizedValue = (value + .replace(/\\+x1b\[[0-9;]*m/g, '') + .replace(/\x1b\[[0-9;]*m/g, '') + .replace(/\\\[.*?\\\]/g, '') + ); + this._updatePromptTerminator(sanitizedValue); + return true; + } case 'Task': { this._createOrGetBufferMarkDetection(this._terminal); this.capabilities.get(TerminalCapability.CommandDetection)?.setIsCommandStorageDisabled(); @@ -422,6 +431,16 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._createOrGetCommandDetection(this._terminal).setContinuationPrompt(value); } + private _updatePromptTerminator(prompt: string) { + if (!this._terminal) { + return; + } + const promptTerminator = prompt?.trim()?.split('\n').pop()?.trim().split(' ').pop(); + if (promptTerminator) { + this._createOrGetCommandDetection(this._terminal).setPromptTerminator(promptTerminator); + } + } + private _updateCwd(value: string) { value = sanitizeCwd(value); this._createOrGetCwdDetection().updateCwd(value); 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..9c9fd224cdbd2 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -165,6 +165,11 @@ unset VSCODE_NONCE # Report continuation prompt builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" +__vsc_report_prompt() { + __vsc_prompt="$(builtin printf "%s" "${__vsc_original_PS1@P}" | sed 's/\x1b/\\\\x1b/g')" + builtin printf "\e]633;P;Prompt=%s\a" "$(__vsc_escape_value "$__vsc_prompt")" +} + __vsc_prompt_start() { builtin printf '\e]633;A\a' } @@ -229,6 +234,7 @@ __vsc_precmd() { __vsc_command_complete "$__vsc_status" __vsc_current_command="" __vsc_update_prompt + __vsc_report_prompt __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 c22f7d642247b..4e68338d26f5a 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -90,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 + # OSC 633 ; = ST + $Result += "$([char]0x1b)]633;P;Prompt=$(__VSCode-Escape-Value $OriginalPrompt)`a" + # Write command started $Result += "$([char]0x1b)]633;B`a" $Global:__LastHistoryId = $LastHistoryEntry.Id From be8078d78a03d5124d92058ceae501eee5aedc88 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 28 Apr 2024 08:52:20 -0700 Subject: [PATCH 2/3] Add missing semicolon --- .../terminal/common/capabilities/commandDetectionCapability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index b45f4115b35ca..d87a9aeaac326 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -55,7 +55,7 @@ export class CommandDetectionCapability extends Disposable implements ICommandDe return this._currentCommand; } get cwd(): string | undefined { return this._cwd; } - get promptTerminator(): string | undefined { return this._promptTerminator } + get promptTerminator(): string | undefined { return this._promptTerminator; } private get _isInputting(): boolean { return !!(this._currentCommand.commandStartMarker && !this._currentCommand.commandExecutedMarker); } From 89ee2dd4a621e570822588a1b95a95fc9b77e69c Mon Sep 17 00:00:00 2001 From: Chapman Pendery Date: Tue, 30 Apr 2024 13:17:31 -0700 Subject: [PATCH 3/3] fix: feedback Signed-off-by: Chapman Pendery --- .../capabilities/commandDetectionCapability.ts | 16 ++++++++-------- .../common/xterm/shellIntegrationAddon.ts | 10 ++++------ .../browser/media/shellIntegration-bash.sh | 11 ++++++++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts index d87a9aeaac326..c4b764f92d200 100644 --- a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -934,14 +934,6 @@ class WindowsPtyHeuristics extends Disposable { return; } - // Dynamic prompt detection - if (this._capability.promptTerminator && lineText.trim().endsWith(this._capability.promptTerminator)) { - const adjustedPrompt = this._adjustPrompt(lineText, lineText, this._capability.promptTerminator); - if (adjustedPrompt) { - return adjustedPrompt; - } - } - // PowerShell const pwshPrompt = lineText.match(/(?(\(.+\)\s)?(?:PS.+>\s?))/)?.groups?.prompt; if (pwshPrompt) { @@ -981,6 +973,14 @@ class WindowsPtyHeuristics extends Disposable { }; } + // Dynamic prompt detection + if (this._capability.promptTerminator && lineText.trim().endsWith(this._capability.promptTerminator)) { + const adjustedPrompt = this._adjustPrompt(lineText, lineText, this._capability.promptTerminator); + if (adjustedPrompt) { + return adjustedPrompt; + } + } + // Command Prompt const cmdMatch = lineText.match(/^(?(\(.+\)\s)?(?:[A-Z]:\\.*>))/); return cmdMatch?.groups?.prompt ? { diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index be7f74693796b..da8d1dd3fa055 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -399,11 +399,8 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return true; } case 'Prompt': { - const sanitizedValue = (value - .replace(/\\+x1b\[[0-9;]*m/g, '') - .replace(/\x1b\[[0-9;]*m/g, '') - .replace(/\\\[.*?\\\]/g, '') - ); + // Remove escape sequences from the user's prompt + const sanitizedValue = value.replace(/\x1b\[[0-9;]*m/g, ''); this._updatePromptTerminator(sanitizedValue); return true; } @@ -435,7 +432,8 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati if (!this._terminal) { return; } - const promptTerminator = prompt?.trim()?.split('\n').pop()?.trim().split(' ').pop(); + const lastPromptLine = prompt.substring(prompt.lastIndexOf('\n')).trim(); + const promptTerminator = lastPromptLine.substring(lastPromptLine.lastIndexOf(' ')).trim(); if (promptTerminator) { this._createOrGetCommandDetection(this._terminal).setPromptTerminator(promptTerminator); } 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 9c9fd224cdbd2..2f704aa34038f 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -117,13 +117,15 @@ __vsc_escape_value() { for (( i=0; i < "${#str}"; ++i )); do byte="${str:$i:1}" - # Escape backslashes, semi-colons and newlines + # Escape backslashes, semi-colons, escapes, and newlines if [ "$byte" = "\\" ]; then token="\\\\" elif [ "$byte" = ";" ]; then token="\\x3b" elif [ "$byte" = $'\n' ]; then token="\x0a" + elif [ "$byte" = $'\e' ]; then + token="\\x1b" else token="$byte" fi @@ -166,8 +168,11 @@ unset VSCODE_NONCE builtin printf "\e]633;P;ContinuationPrompt=$(echo "$PS2" | sed 's/\x1b/\\\\x1b/g')\a" __vsc_report_prompt() { - __vsc_prompt="$(builtin printf "%s" "${__vsc_original_PS1@P}" | sed 's/\x1b/\\\\x1b/g')" - builtin printf "\e]633;P;Prompt=%s\a" "$(__vsc_escape_value "$__vsc_prompt")" + # Expand the original PS1 similarly to how bash would normally + # See https://stackoverflow.com/a/37137981 for technique + __vsc_prompt=${__vsc_original_PS1@P} + __vsc_prompt="$(builtin printf "%s" "${__vsc_prompt//[$'\001'$'\002']}")" + builtin printf "\e]633;P;Prompt=%s\a" "$(__vsc_escape_value "${__vsc_prompt}")" } __vsc_prompt_start() {