Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
build:
strategy:
matrix:
version: [16, 18, 20, 22]
version: [18, 20, 22]
os: ["macos-latest", "windows-latest", "ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

### Requirements

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

### Installation

Expand Down
964 changes: 364 additions & 600 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "IDE style command line auto complete",
"type": "module",
"engines": {
"node": ">=16.6.0 <23.0.0"
"node": ">=18.0 <23.0.0"
},
"bin": {
"inshellisense": "./build/index.js",
Expand Down Expand Up @@ -42,7 +42,7 @@
},
"homepage": "https://github.com/microsoft/inshellisense#readme",
"dependencies": {
"@homebridge/node-pty-prebuilt-multiarch": "^0.11.14",
"@homebridge/node-pty-prebuilt-multiarch": "0.12.1-beta.0",
"@withfig/autocomplete": "2.675.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/headless": "^5.5.0",
Expand Down
11 changes: 0 additions & 11 deletions shell/shellIntegration.bash
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,13 @@ __is_update_cwd() {
builtin printf '\e]6973;CWD;%s\a' "$(__is_escape_value "$PWD")"
}

__is_report_prompt() {
if ((BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4)); then
__is_prompt=${__is_original_PS1@P}
else
__is_prompt=${__is_original_PS1}
fi
__is_prompt="$(builtin printf "%s" "${__is_prompt//[$'\001'$'\002']}")"
builtin printf "\e]6973;PROMPT;%s\a" "$(__is_escape_value "${__is_prompt}")"
}

if [[ -n "${bash_preexec_imported:-}" ]]; then
precmd_functions+=(__is_precmd)
fi

__is_precmd() {
__is_update_cwd
__is_update_prompt
__is_report_prompt
}

__is_update_prompt() {
Expand Down
1 change: 0 additions & 1 deletion shell/shellIntegration.fish
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ function __is_escape_value
;
end
function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;%s\a" $__is_cwd; end
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

if [ "$ISTERM_TESTING" = "1" ]
function is_user_prompt; printf '> '; end
Expand Down
9 changes: 1 addition & 8 deletions shell/shellIntegration.nu
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,14 @@ let __is_update_cwd = {
let pwd = do $__is_escape_value $env.PWD
$"\e]6973;CWD;($pwd)\a"
}
let __is_report_prompt = {
let __is_indicatorCommandType = $__is_original_PROMPT_INDICATOR | describe
mut __is_prompt_ind = if $__is_indicatorCommandType == "closure" { do $__is_original_PROMPT_INDICATOR } else { $__is_original_PROMPT_INDICATOR }
let __is_esc_prompt_ind = do $__is_escape_value $__is_prompt_ind
$"\e]6973;PROMPT;($__is_esc_prompt_ind)\a"
}
let __is_custom_PROMPT_COMMAND = {
let promptCommandType = $__is_original_PROMPT_COMMAND | describe
mut cmd = if $promptCommandType == "closure" { do $__is_original_PROMPT_COMMAND } else { $__is_original_PROMPT_COMMAND }
let pwd = do $__is_update_cwd
let prompt = do $__is_report_prompt
if 'ISTERM_TESTING' in $env {
$cmd = ""
}
$"\e]6973;PS\a($cmd)($pwd)($prompt)"
$"\e]6973;PS\a($cmd)($pwd)"
}
$env.PROMPT_COMMAND = $__is_custom_PROMPT_COMMAND

Expand Down
1 change: 0 additions & 1 deletion shell/shellIntegration.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ function Global:Prompt() {
$Result += $OriginalPrompt
$Result += "$([char]0x1b)]6973;PE`a"

$Result += "$([char]0x1b)]6973;PROMPT;$(__IS-Escape-Value $OriginalPrompt)`a"
$Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]6973;CWD;$(__IS-Escape-Value $pwd.ProviderPath)`a" }
return $Result
}
10 changes: 2 additions & 8 deletions shell/shellIntegration.xsh
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,14 @@ def __is_escape_value(value: str) -> str:
)

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

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

$PROMPT_FIELDS['__is_prompt_start'] = __is_prompt_start
$PROMPT_FIELDS['__is_prompt_end'] = __is_prompt_end
$PROMPT_FIELDS['__is_update_cwd'] = __is_update_cwd
$PROMPT_FIELDS['__is_report_prompt'] = __is_report_prompt
if 'ISTERM_TESTING' in ${...}:
$PROMPT = "> "

$PROMPT = "{__is_prompt_start}{__is_update_cwd}{__is_report_prompt}" + $PROMPT + "{__is_prompt_end}"
$PROMPT = "{__is_prompt_start}{__is_update_cwd}" + $PROMPT + "{__is_prompt_end}"
160 changes: 22 additions & 138 deletions src/isterm/commandManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@

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

const maxPromptPollDistance = 10;

type TerminalCommand = {
promptStartMarker?: IMarker;
promptEndMarker?: IMarker;
Expand All @@ -27,153 +24,55 @@ export type CommandState = {
export class CommandManager {
#activeCommand: TerminalCommand;
#terminal: Terminal;
#previousCommandLines: Set<number>;
#acceptedCommandLines: Set<number>;
#maxCursorY: number;
#shell: Shell;
#promptRewrites: boolean;
readonly #supportsProperOscPlacements = os.platform() !== "win32";
promptTerminator: string = "";

constructor(terminal: Terminal, shell: Shell) {
this.#terminal = terminal;
this.#shell = shell;
this.#activeCommand = {};
this.#maxCursorY = 0;
this.#previousCommandLines = new Set();
this.#acceptedCommandLines = new Set();
this.#promptRewrites = getShellPromptRewrites(shell);

if (this.#supportsProperOscPlacements) {
this.#terminal.parser.registerCsiHandler({ final: "J" }, (params) => {
if (params.at(0) == 3 || params.at(0) == 2) {
this.handleClear();
}
return false;
});
}
this.#terminal.parser.registerCsiHandler({ final: "J" }, (params) => {
if (params.at(0) == 3 || params.at(0) == 2) {
this.handleClear();
}
return false;
});
}
handlePromptStart() {
this.#activeCommand = { promptStartMarker: this.#terminal.registerMarker(0), hasOutput: false, cursorTerminated: false };
}

handlePromptEnd() {
if (this.#activeCommand.promptEndMarker != null) return;
if (this.#hasBeenAccepted()) {
this.#activeCommand = {};
return;
}

this.#activeCommand.promptEndMarker = this.#terminal.registerMarker(0);
if (this.#activeCommand.promptEndMarker?.line === this.#terminal.buffer.active.cursorY) {
this.#activeCommand.promptEndX = this.#terminal.buffer.active.cursorX;
}
if (this.#supportsProperOscPlacements) {
this.#activeCommand.promptText = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker?.line ?? 0)?.translateToString(true);
this.#previousCommandLines.add(this.#activeCommand.promptEndMarker?.line ?? -1);
}
}

handleClear() {
this.handlePromptStart();
this.#maxCursorY = 0;
this.#previousCommandLines = new Set();
this.#activeCommand.promptText = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker?.line ?? 0)?.translateToString(true);
}

private _getWindowsPrompt(y: number) {
const line = this.#terminal.buffer.active.getLine(y);
if (!line) {
return;
}
const lineText = line.translateToString(true);
if (!lineText) {
return;
}

// dynamic prompt terminator
if (this.promptTerminator && lineText.trim().endsWith(this.promptTerminator)) {
const adjustedPrompt = this._adjustPrompt(lineText, lineText, this.promptTerminator);
if (adjustedPrompt) {
return adjustedPrompt;
}
}

// User defined prompt
if (this.#shell == Shell.Bash) {
const bashPrompt = lineText.match(/^(?<prompt>\$\s?)/)?.groups?.prompt;
if (bashPrompt) {
const adjustedPrompt = this._adjustPrompt(bashPrompt, lineText, "$");
if (adjustedPrompt) {
return adjustedPrompt;
}
}
}

if (this.#shell == Shell.Fish) {
const fishPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
if (fishPrompt) {
const adjustedPrompt = this._adjustPrompt(fishPrompt, lineText, ">");
if (adjustedPrompt) {
return adjustedPrompt;
}
}
}

if (this.#shell == Shell.Nushell) {
const nushellPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
if (nushellPrompt) {
const adjustedPrompt = this._adjustPrompt(nushellPrompt, lineText, ">");
if (adjustedPrompt) {
return adjustedPrompt;
}
}
}

if (this.#shell == Shell.Xonsh) {
let xonshPrompt = lineText.match(/(?<prompt>.*@\s?)/)?.groups?.prompt;
if (xonshPrompt) {
const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, "@");
if (adjustedPrompt) {
return adjustedPrompt;
}
}

xonshPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
if (xonshPrompt) {
const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, ">");
if (adjustedPrompt) {
return adjustedPrompt;
}
}
}

if (this.#shell == Shell.Powershell || this.#shell == Shell.Pwsh) {
const pwshPrompt = lineText.match(/(?<prompt>(\(.+\)\s)?(?:PS.+>\s?))/)?.groups?.prompt;
if (pwshPrompt) {
const adjustedPrompt = this._adjustPrompt(pwshPrompt, lineText, ">");
if (adjustedPrompt) {
return adjustedPrompt;
}
}
}

if (this.#shell == Shell.Cmd) {
return lineText.match(/^(?<prompt>(\(.+\)\s)?(?:[A-Z]:\\.*>)|(> ))/)?.groups?.prompt;
}

// Custom prompts like starship end in the common \u276f character
const customPrompt = lineText.match(/.*\u276f(?=[^\u276f]*$)/g)?.[0];
if (customPrompt) {
const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, "\u276f");
if (adjustedPrompt) {
return adjustedPrompt;
}
}
#hasBeenAccepted() {
const commandLine = this.#activeCommand.promptStartMarker?.line ?? -1;
const hasBeenAccepted = this.#acceptedCommandLines.has(commandLine) && commandLine != -1;
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)
}

private _adjustPrompt(prompt: string | undefined, lineText: string, char: string): string | undefined {
if (!prompt) {
return;
}
// Conpty may not 'render' the space at the end of the prompt
if (lineText === prompt && prompt.endsWith(char)) {
prompt += " ";
}
return prompt;
handleClear() {
this.handlePromptStart();
this.#maxCursorY = 0;
this.#acceptedCommandLines.clear();
}

private _getFgPaletteColor(cell: IBufferCell | undefined): number | undefined {
Expand Down Expand Up @@ -205,6 +104,7 @@ export class CommandManager {
}

clearActiveCommand() {
this.#acceptedCommandLines.add(this.#activeCommand.promptEndMarker?.line ?? -1);
this.#activeCommand = {};
}

Expand Down Expand Up @@ -283,7 +183,6 @@ export class CommandManager {
}

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

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

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

// if we haven't fond the prompt yet, poll over the next 5 lines searching for it
if (this.#activeCommand.promptText == null && withinPollDistance) {
for (let i = globalCursorPosition; i < this.#activeCommand.promptEndMarker.line + maxPromptPollDistance; i++) {
if (this.#previousCommandLines.has(i) && !this.#promptRewrites) continue;
const promptResult = this._getWindowsPrompt(i);
if (promptResult != null) {
this.#activeCommand.promptEndMarker = this.#terminal.registerMarker(i - globalCursorPosition);
this.#activeCommand.promptEndX = promptResult.length;
this.#activeCommand.promptText = promptResult;
this.#previousCommandLines.add(i);
break;
}
}
}

// if the prompt is set, now parse out the values from the terminal
if (this.#activeCommand.promptText != null) {
const commandLines = this._getCommandLines();
Expand Down
15 changes: 2 additions & 13 deletions src/isterm/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export class ISTerm implements IPty {
rows,
cwd: process.cwd(),
env: { ...convertToPtyEnv(shell, underTest, login), ...env },
useConpty: true,
useConptyDll: true,
});
this.pid = this.#pty.pid;
this.cols = this.#pty.cols;
Expand Down Expand Up @@ -140,19 +142,6 @@ export class ISTerm implements IPty {
}
break;
}
case IstermOscPt.Prompt: {
const prompt = data.split(";").slice(1).join(";");
if (prompt != null) {
const sanitizedPrompt = this._sanitizedPrompt(this._deserializeIsMessage(prompt));
const lastPromptLine = sanitizedPrompt.substring(sanitizedPrompt.lastIndexOf("\n")).trim();
const promptTerminator = lastPromptLine.substring(lastPromptLine.lastIndexOf(" ")).trim();
if (promptTerminator) {
this.#commandManager.promptTerminator = promptTerminator;
log.debug({ msg: "prompt terminator", promptTerminator });
}
}
break;
}
default:
return false;
}
Expand Down
1 change: 0 additions & 1 deletion src/utils/ansi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export enum IstermOscPt {
PromptStarted = "PS",
PromptEnded = "PE",
CurrentWorkingDirectory = "CWD",
Prompt = "PROMPT",
}

export const IstermPromptStart = IS_OSC + IstermOscPt.PromptStarted + BEL;
Expand Down
Loading