Skip to content

Commit 21a4594

Browse files
authored
feat: use conpty 1.22 for better prompt tracking (#340)
* feat: use conpty 1.22 for better prompt tracking Signed-off-by: Chapman Pendery <[email protected]> * fix: style Signed-off-by: Chapman Pendery <[email protected]> * ci: try using 18-24 Signed-off-by: Chapman Pendery <[email protected]> * ci: use 18-22 Signed-off-by: Chapman Pendery <[email protected]> * docs: update to show node 18-22 lts support Signed-off-by: Chapman Pendery <[email protected]> --------- Signed-off-by: Chapman Pendery <[email protected]>
1 parent 999b8e9 commit 21a4594

14 files changed

+398
-834
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
build:
1010
strategy:
1111
matrix:
12-
version: [16, 18, 20, 22]
12+
version: [18, 20, 22]
1313
os: ["macos-latest", "windows-latest", "ubuntu-latest"]
1414
runs-on: ${{ matrix.os }}
1515
steps:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
### Requirements
1010

11-
- Node.js 22.X, 20.X, 18.X, 16.X (16.6.0 >=)
11+
- Node.js 22.X, 20.X, 18.X
1212

1313
### Installation
1414

package-lock.json

Lines changed: 364 additions & 600 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "IDE style command line auto complete",
55
"type": "module",
66
"engines": {
7-
"node": ">=16.6.0 <23.0.0"
7+
"node": ">=18.0 <23.0.0"
88
},
99
"bin": {
1010
"inshellisense": "./build/index.js",
@@ -42,7 +42,7 @@
4242
},
4343
"homepage": "https://github.com/microsoft/inshellisense#readme",
4444
"dependencies": {
45-
"@homebridge/node-pty-prebuilt-multiarch": "^0.11.14",
45+
"@homebridge/node-pty-prebuilt-multiarch": "0.12.1-beta.0",
4646
"@withfig/autocomplete": "2.675.0",
4747
"@xterm/addon-unicode11": "^0.8.0",
4848
"@xterm/headless": "^5.5.0",

shell/shellIntegration.bash

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,13 @@ __is_update_cwd() {
6060
builtin printf '\e]6973;CWD;%s\a' "$(__is_escape_value "$PWD")"
6161
}
6262

63-
__is_report_prompt() {
64-
if ((BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4)); then
65-
__is_prompt=${__is_original_PS1@P}
66-
else
67-
__is_prompt=${__is_original_PS1}
68-
fi
69-
__is_prompt="$(builtin printf "%s" "${__is_prompt//[$'\001'$'\002']}")"
70-
builtin printf "\e]6973;PROMPT;%s\a" "$(__is_escape_value "${__is_prompt}")"
71-
}
72-
7363
if [[ -n "${bash_preexec_imported:-}" ]]; then
7464
precmd_functions+=(__is_precmd)
7565
fi
7666

7767
__is_precmd() {
7868
__is_update_cwd
7969
__is_update_prompt
80-
__is_report_prompt
8170
}
8271

8372
__is_update_prompt() {

shell/shellIntegration.fish

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ function __is_escape_value
1414
;
1515
end
1616
function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;%s\a" $__is_cwd; end
17-
function __is_report_prompt --on-event fish_prompt; set __is_prompt (__is_escape_value (is_user_prompt)); printf "\e]6973;PROMPT;%s\a" $__is_prompt; end
1817
1918
if [ "$ISTERM_TESTING" = "1" ]
2019
function is_user_prompt; printf '> '; end

shell/shellIntegration.nu

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,14 @@ let __is_update_cwd = {
66
let pwd = do $__is_escape_value $env.PWD
77
$"\e]6973;CWD;($pwd)\a"
88
}
9-
let __is_report_prompt = {
10-
let __is_indicatorCommandType = $__is_original_PROMPT_INDICATOR | describe
11-
mut __is_prompt_ind = if $__is_indicatorCommandType == "closure" { do $__is_original_PROMPT_INDICATOR } else { $__is_original_PROMPT_INDICATOR }
12-
let __is_esc_prompt_ind = do $__is_escape_value $__is_prompt_ind
13-
$"\e]6973;PROMPT;($__is_esc_prompt_ind)\a"
14-
}
159
let __is_custom_PROMPT_COMMAND = {
1610
let promptCommandType = $__is_original_PROMPT_COMMAND | describe
1711
mut cmd = if $promptCommandType == "closure" { do $__is_original_PROMPT_COMMAND } else { $__is_original_PROMPT_COMMAND }
1812
let pwd = do $__is_update_cwd
19-
let prompt = do $__is_report_prompt
2013
if 'ISTERM_TESTING' in $env {
2114
$cmd = ""
2215
}
23-
$"\e]6973;PS\a($cmd)($pwd)($prompt)"
16+
$"\e]6973;PS\a($cmd)($pwd)"
2417
}
2518
$env.PROMPT_COMMAND = $__is_custom_PROMPT_COMMAND
2619

shell/shellIntegration.ps1

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ function Global:Prompt() {
2121
$Result += $OriginalPrompt
2222
$Result += "$([char]0x1b)]6973;PE`a"
2323

24-
$Result += "$([char]0x1b)]6973;PROMPT;$(__IS-Escape-Value $OriginalPrompt)`a"
2524
$Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]6973;CWD;$(__IS-Escape-Value $pwd.ProviderPath)`a" }
2625
return $Result
2726
}

shell/shellIntegration.xsh

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,14 @@ def __is_escape_value(value: str) -> str:
1818
)
1919

2020
def __is_update_cwd() -> str:
21-
return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\x07"
21+
return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\x07" + "\002"
2222

2323
__is_original_prompt = $PROMPT
24-
def __is_report_prompt() -> str:
25-
prompt = ""
26-
formatted_prompt = XSH.shell.prompt_formatter(__is_original_prompt)
27-
prompt = "".join([text for _, text in XSH.shell.format_color(formatted_prompt)])
28-
return f"\x1b]6973;PROMPT;{__is_escape_value(prompt)}\x07" + "\002"
2924

3025
$PROMPT_FIELDS['__is_prompt_start'] = __is_prompt_start
3126
$PROMPT_FIELDS['__is_prompt_end'] = __is_prompt_end
3227
$PROMPT_FIELDS['__is_update_cwd'] = __is_update_cwd
33-
$PROMPT_FIELDS['__is_report_prompt'] = __is_report_prompt
3428
if 'ISTERM_TESTING' in ${...}:
3529
$PROMPT = "> "
3630

37-
$PROMPT = "{__is_prompt_start}{__is_update_cwd}{__is_report_prompt}" + $PROMPT + "{__is_prompt_end}"
31+
$PROMPT = "{__is_prompt_start}{__is_update_cwd}" + $PROMPT + "{__is_prompt_end}"

src/isterm/commandManager.ts

Lines changed: 22 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33

44
import convert from "color-convert";
55
import { IBufferCell, IBufferLine, IMarker, Terminal } from "@xterm/headless";
6-
import os from "node:os";
76
import { getShellPromptRewrites, Shell } from "../utils/shell.js";
87
import log from "../utils/log.js";
98

10-
const maxPromptPollDistance = 10;
11-
129
type TerminalCommand = {
1310
promptStartMarker?: IMarker;
1411
promptEndMarker?: IMarker;
@@ -27,153 +24,55 @@ export type CommandState = {
2724
export class CommandManager {
2825
#activeCommand: TerminalCommand;
2926
#terminal: Terminal;
30-
#previousCommandLines: Set<number>;
27+
#acceptedCommandLines: Set<number>;
3128
#maxCursorY: number;
3229
#shell: Shell;
3330
#promptRewrites: boolean;
34-
readonly #supportsProperOscPlacements = os.platform() !== "win32";
35-
promptTerminator: string = "";
3631

3732
constructor(terminal: Terminal, shell: Shell) {
3833
this.#terminal = terminal;
3934
this.#shell = shell;
4035
this.#activeCommand = {};
4136
this.#maxCursorY = 0;
42-
this.#previousCommandLines = new Set();
37+
this.#acceptedCommandLines = new Set();
4338
this.#promptRewrites = getShellPromptRewrites(shell);
4439

45-
if (this.#supportsProperOscPlacements) {
46-
this.#terminal.parser.registerCsiHandler({ final: "J" }, (params) => {
47-
if (params.at(0) == 3 || params.at(0) == 2) {
48-
this.handleClear();
49-
}
50-
return false;
51-
});
52-
}
40+
this.#terminal.parser.registerCsiHandler({ final: "J" }, (params) => {
41+
if (params.at(0) == 3 || params.at(0) == 2) {
42+
this.handleClear();
43+
}
44+
return false;
45+
});
5346
}
5447
handlePromptStart() {
5548
this.#activeCommand = { promptStartMarker: this.#terminal.registerMarker(0), hasOutput: false, cursorTerminated: false };
5649
}
5750

5851
handlePromptEnd() {
5952
if (this.#activeCommand.promptEndMarker != null) return;
53+
if (this.#hasBeenAccepted()) {
54+
this.#activeCommand = {};
55+
return;
56+
}
6057

6158
this.#activeCommand.promptEndMarker = this.#terminal.registerMarker(0);
6259
if (this.#activeCommand.promptEndMarker?.line === this.#terminal.buffer.active.cursorY) {
6360
this.#activeCommand.promptEndX = this.#terminal.buffer.active.cursorX;
6461
}
65-
if (this.#supportsProperOscPlacements) {
66-
this.#activeCommand.promptText = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker?.line ?? 0)?.translateToString(true);
67-
this.#previousCommandLines.add(this.#activeCommand.promptEndMarker?.line ?? -1);
68-
}
69-
}
7062

71-
handleClear() {
72-
this.handlePromptStart();
73-
this.#maxCursorY = 0;
74-
this.#previousCommandLines = new Set();
63+
this.#activeCommand.promptText = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker?.line ?? 0)?.translateToString(true);
7564
}
7665

77-
private _getWindowsPrompt(y: number) {
78-
const line = this.#terminal.buffer.active.getLine(y);
79-
if (!line) {
80-
return;
81-
}
82-
const lineText = line.translateToString(true);
83-
if (!lineText) {
84-
return;
85-
}
86-
87-
// dynamic prompt terminator
88-
if (this.promptTerminator && lineText.trim().endsWith(this.promptTerminator)) {
89-
const adjustedPrompt = this._adjustPrompt(lineText, lineText, this.promptTerminator);
90-
if (adjustedPrompt) {
91-
return adjustedPrompt;
92-
}
93-
}
94-
95-
// User defined prompt
96-
if (this.#shell == Shell.Bash) {
97-
const bashPrompt = lineText.match(/^(?<prompt>\$\s?)/)?.groups?.prompt;
98-
if (bashPrompt) {
99-
const adjustedPrompt = this._adjustPrompt(bashPrompt, lineText, "$");
100-
if (adjustedPrompt) {
101-
return adjustedPrompt;
102-
}
103-
}
104-
}
105-
106-
if (this.#shell == Shell.Fish) {
107-
const fishPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
108-
if (fishPrompt) {
109-
const adjustedPrompt = this._adjustPrompt(fishPrompt, lineText, ">");
110-
if (adjustedPrompt) {
111-
return adjustedPrompt;
112-
}
113-
}
114-
}
115-
116-
if (this.#shell == Shell.Nushell) {
117-
const nushellPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
118-
if (nushellPrompt) {
119-
const adjustedPrompt = this._adjustPrompt(nushellPrompt, lineText, ">");
120-
if (adjustedPrompt) {
121-
return adjustedPrompt;
122-
}
123-
}
124-
}
125-
126-
if (this.#shell == Shell.Xonsh) {
127-
let xonshPrompt = lineText.match(/(?<prompt>.*@\s?)/)?.groups?.prompt;
128-
if (xonshPrompt) {
129-
const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, "@");
130-
if (adjustedPrompt) {
131-
return adjustedPrompt;
132-
}
133-
}
134-
135-
xonshPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
136-
if (xonshPrompt) {
137-
const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, ">");
138-
if (adjustedPrompt) {
139-
return adjustedPrompt;
140-
}
141-
}
142-
}
143-
144-
if (this.#shell == Shell.Powershell || this.#shell == Shell.Pwsh) {
145-
const pwshPrompt = lineText.match(/(?<prompt>(\(.+\)\s)?(?:PS.+>\s?))/)?.groups?.prompt;
146-
if (pwshPrompt) {
147-
const adjustedPrompt = this._adjustPrompt(pwshPrompt, lineText, ">");
148-
if (adjustedPrompt) {
149-
return adjustedPrompt;
150-
}
151-
}
152-
}
153-
154-
if (this.#shell == Shell.Cmd) {
155-
return lineText.match(/^(?<prompt>(\(.+\)\s)?(?:[A-Z]:\\.*>)|(> ))/)?.groups?.prompt;
156-
}
157-
158-
// Custom prompts like starship end in the common \u276f character
159-
const customPrompt = lineText.match(/.*\u276f(?=[^\u276f]*$)/g)?.[0];
160-
if (customPrompt) {
161-
const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, "\u276f");
162-
if (adjustedPrompt) {
163-
return adjustedPrompt;
164-
}
165-
}
66+
#hasBeenAccepted() {
67+
const commandLine = this.#activeCommand.promptStartMarker?.line ?? -1;
68+
const hasBeenAccepted = this.#acceptedCommandLines.has(commandLine) && commandLine != -1;
69+
return this.#promptRewrites && hasBeenAccepted; // this is a prompt + command that was accepted and is now being re-written by the shell for display purposes (e.g. nu)
16670
}
16771

168-
private _adjustPrompt(prompt: string | undefined, lineText: string, char: string): string | undefined {
169-
if (!prompt) {
170-
return;
171-
}
172-
// Conpty may not 'render' the space at the end of the prompt
173-
if (lineText === prompt && prompt.endsWith(char)) {
174-
prompt += " ";
175-
}
176-
return prompt;
72+
handleClear() {
73+
this.handlePromptStart();
74+
this.#maxCursorY = 0;
75+
this.#acceptedCommandLines.clear();
17776
}
17877

17978
private _getFgPaletteColor(cell: IBufferCell | undefined): number | undefined {
@@ -205,6 +104,7 @@ export class CommandManager {
205104
}
206105

207106
clearActiveCommand() {
107+
this.#acceptedCommandLines.add(this.#activeCommand.promptEndMarker?.line ?? -1);
208108
this.#activeCommand = {};
209109
}
210110

@@ -283,7 +183,6 @@ export class CommandManager {
283183
}
284184

285185
const globalCursorPosition = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
286-
const withinPollDistance = globalCursorPosition < this.#activeCommand.promptEndMarker.line + 5;
287186
this.#maxCursorY = Math.max(this.#maxCursorY, globalCursorPosition);
288187

289188
if (globalCursorPosition < this.#activeCommand.promptStartMarker.line || globalCursorPosition < this.#maxCursorY) {
@@ -294,21 +193,6 @@ export class CommandManager {
294193

295194
if (this.#activeCommand.promptEndMarker == null) return;
296195

297-
// if we haven't fond the prompt yet, poll over the next 5 lines searching for it
298-
if (this.#activeCommand.promptText == null && withinPollDistance) {
299-
for (let i = globalCursorPosition; i < this.#activeCommand.promptEndMarker.line + maxPromptPollDistance; i++) {
300-
if (this.#previousCommandLines.has(i) && !this.#promptRewrites) continue;
301-
const promptResult = this._getWindowsPrompt(i);
302-
if (promptResult != null) {
303-
this.#activeCommand.promptEndMarker = this.#terminal.registerMarker(i - globalCursorPosition);
304-
this.#activeCommand.promptEndX = promptResult.length;
305-
this.#activeCommand.promptText = promptResult;
306-
this.#previousCommandLines.add(i);
307-
break;
308-
}
309-
}
310-
}
311-
312196
// if the prompt is set, now parse out the values from the terminal
313197
if (this.#activeCommand.promptText != null) {
314198
const commandLines = this._getCommandLines();

src/isterm/pty.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export class ISTerm implements IPty {
5959
rows,
6060
cwd: process.cwd(),
6161
env: { ...convertToPtyEnv(shell, underTest, login), ...env },
62+
useConpty: true,
63+
useConptyDll: true,
6264
});
6365
this.pid = this.#pty.pid;
6466
this.cols = this.#pty.cols;
@@ -140,19 +142,6 @@ export class ISTerm implements IPty {
140142
}
141143
break;
142144
}
143-
case IstermOscPt.Prompt: {
144-
const prompt = data.split(";").slice(1).join(";");
145-
if (prompt != null) {
146-
const sanitizedPrompt = this._sanitizedPrompt(this._deserializeIsMessage(prompt));
147-
const lastPromptLine = sanitizedPrompt.substring(sanitizedPrompt.lastIndexOf("\n")).trim();
148-
const promptTerminator = lastPromptLine.substring(lastPromptLine.lastIndexOf(" ")).trim();
149-
if (promptTerminator) {
150-
this.#commandManager.promptTerminator = promptTerminator;
151-
log.debug({ msg: "prompt terminator", promptTerminator });
152-
}
153-
}
154-
break;
155-
}
156145
default:
157146
return false;
158147
}

src/utils/ansi.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export enum IstermOscPt {
1313
PromptStarted = "PS",
1414
PromptEnded = "PE",
1515
CurrentWorkingDirectory = "CWD",
16-
Prompt = "PROMPT",
1716
}
1817

1918
export const IstermPromptStart = IS_OSC + IstermOscPt.PromptStarted + BEL;

0 commit comments

Comments
 (0)