mirror of
https://github.com/wavetermdev/inshellisense.git
synced 2026-08-05 13:43:30 -07:00
feat: implement xonsh support (#195)
* feat: implement xonsh support (partial) Signed-off-by: Chapman Pendery <cpendery@vt.edu> * feat: implement xonsh support (partial) Signed-off-by: Chapman Pendery <cpendery@vt.edu> * refactor: xonsh support to work with new shell integration mechanism Signed-off-by: Chapman Pendery <cpendery@vt.edu> * build: drop old xonsh integration Signed-off-by: Chapman Pendery <cpendery@vt.edu> * feat: enable xonsh testing Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: generator test Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: newline chars Signed-off-by: Chapman Pendery <cpendery@vt.edu> * fix: drop only clause Signed-off-by: Chapman Pendery <cpendery@vt.edu> --------- Signed-off-by: Chapman Pendery <cpendery@vt.edu>
This commit is contained in:
@@ -52,6 +52,12 @@ jobs:
|
||||
sudo chmod -R 755 /usr/share/zsh
|
||||
sudo chown -R root:root /usr/share/zsh
|
||||
|
||||
- name: setup windows shells
|
||||
if: matrix.os == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
python -m pip install 'xonsh[full]'
|
||||
|
||||
- run: npm test
|
||||
|
||||
- run: npm run build
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
build/
|
||||
t*.md
|
||||
.tui-test/
|
||||
.tui-test/
|
||||
tui-traces/
|
||||
Generated
+381
-900
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -40,12 +40,12 @@
|
||||
"homepage": "https://github.com/microsoft/inshellisense#readme",
|
||||
"dependencies": {
|
||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.11.12",
|
||||
"@microsoft/tui-test": "^0.0.1-rc.3",
|
||||
"@withfig/autocomplete": "2.651.0",
|
||||
"ajv": "^8.12.0",
|
||||
"ansi-escapes": "^6.2.0",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"chalk": "^5.3.0",
|
||||
"color-convert": "^2.0.1",
|
||||
"commander": "^11.0.0",
|
||||
"find-process": "^1.4.7",
|
||||
"wcwidth": "^1.0.1",
|
||||
@@ -54,7 +54,9 @@
|
||||
"xterm-headless": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/tui-test": "^0.0.1-rc.3",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/color-convert": "^2.0.3",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/react": "^18.2.24",
|
||||
"@types/wcwidth": "^1.0.2",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import os
|
||||
|
||||
def __is_prompt_start() -> str:
|
||||
return "\001" + "\x1b]6973;PS\x07"
|
||||
|
||||
|
||||
def __is_prompt_end() -> str:
|
||||
return "\001" + "\x1b]6973;PE\x07" + "\002"
|
||||
|
||||
|
||||
def __is_escape_value(value: str) -> str:
|
||||
byte_list = [bytes([byte]).decode("utf-8") for byte in list(value.encode("utf-8"))]
|
||||
return "".join(
|
||||
[
|
||||
"\\x3b" if byte == ";" else "\\\\" if byte == "\\" else byte
|
||||
for byte in byte_list
|
||||
]
|
||||
)
|
||||
|
||||
def __is_update_cwd() -> str:
|
||||
return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\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
|
||||
if $ISTERM_TESTING:
|
||||
$PROMPT = "> "
|
||||
|
||||
$PROMPT = "{__is_prompt_start}{__is_update_cwd}" + $PROMPT + "{__is_prompt_end}"
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
import convert from "color-convert";
|
||||
import { IBufferCell, IMarker, Terminal } from "xterm-headless";
|
||||
import os from "node:os";
|
||||
import { Shell } from "../utils/shell.js";
|
||||
@@ -98,6 +99,24 @@ export class CommandManager {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (inshellisenseConfig.promptRegex?.pwsh != null && this.#shell == Shell.Pwsh) {
|
||||
const customPwshPrompt = lineText.match(new RegExp(inshellisenseConfig.promptRegex?.pwsh.regex))?.groups?.prompt;
|
||||
@@ -149,8 +168,14 @@ export class CommandManager {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
private _getFgPaletteColor(cell: IBufferCell | undefined): number | undefined {
|
||||
if (cell?.isFgDefault()) return 0;
|
||||
if (cell?.isFgPalette()) return cell.getFgColor();
|
||||
if (cell?.isFgRGB()) return convert.hex.ansi256(cell.getFgColor().toString(16));
|
||||
}
|
||||
|
||||
private _isSuggestion(cell: IBufferCell | undefined): boolean {
|
||||
const color = cell?.getFgColor();
|
||||
const color = this._getFgPaletteColor(cell);
|
||||
const dim = (cell?.isDim() ?? 0) > 0;
|
||||
const italic = (cell?.isItalic() ?? 0) > 0;
|
||||
const dullColor = color == 8 || color == 7 || (color ?? 0) > 235 || (color == 15 && dim);
|
||||
|
||||
+16
-2
@@ -6,9 +6,10 @@ import process from "node:process";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import url from "node:url";
|
||||
import fs from "node:fs";
|
||||
|
||||
import pty, { IPty, IEvent } from "@homebridge/node-pty-prebuilt-multiarch";
|
||||
import { Shell, userZdotdir, zdotdir } from "../utils/shell.js";
|
||||
import { Shell, getPythonPath, userZdotdir, zdotdir } from "../utils/shell.js";
|
||||
import { IsTermOscPs, IstermOscPt, IstermPromptStart, IstermPromptEnd } from "../utils/ansi.js";
|
||||
import xterm from "xterm-headless";
|
||||
import { CommandManager, CommandState } from "./commandManager.js";
|
||||
@@ -252,7 +253,7 @@ export const spawn = async (options: ISTermOptions): Promise<ISTerm> => {
|
||||
|
||||
const convertToPtyTarget = async (shell: Shell) => {
|
||||
const platform = os.platform();
|
||||
const shellTarget = shell == Shell.Bash && platform == "win32" ? await gitBashPath() : platform == "win32" ? `${shell}.exe` : shell;
|
||||
let shellTarget = shell == Shell.Bash && platform == "win32" ? await gitBashPath() : platform == "win32" ? `${shell}.exe` : shell;
|
||||
const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");
|
||||
let shellArgs: string[] = [];
|
||||
|
||||
@@ -267,6 +268,19 @@ const convertToPtyTarget = async (shell: Shell) => {
|
||||
case Shell.Fish:
|
||||
shellArgs = ["--init-command", `. ${path.join(shellFolderPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`];
|
||||
break;
|
||||
case Shell.Xonsh: {
|
||||
const sharedConfig = os.platform() == "win32" ? path.join("C:\\ProgramData", "xonsh", "xonshrc") : path.join("etc", "xonsh", "xonshrc");
|
||||
const userConfigs = [
|
||||
path.join(os.homedir(), ".xonshrc"),
|
||||
path.join(os.homedir(), ".config", "xonsh", "rc.xsh"),
|
||||
path.join(os.homedir(), ".config", "xonsh", "rc.d"),
|
||||
];
|
||||
const configs = [sharedConfig, ...userConfigs].filter((config) => fs.existsSync(config));
|
||||
|
||||
shellArgs = ["-m", "xonsh", "--rc", ...configs, path.join(shellFolderPath, "shellIntegration.xsh")];
|
||||
shellTarget = await getPythonPath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { shellTarget, shellArgs };
|
||||
|
||||
@@ -416,16 +416,16 @@ exports[`parseCommand fullyTypedSuggestion 1`] = `
|
||||
|
||||
exports[`parseCommand generatorUsingPartialInput 1`] = `
|
||||
{
|
||||
"charactersToDrop": 21,
|
||||
"charactersToDrop": 27,
|
||||
"suggestions": [
|
||||
{
|
||||
"allNames": [
|
||||
"Microsoft.Azure.WebJobs",
|
||||
"Microsoft.Azure.WebJobs.Core",
|
||||
],
|
||||
"description": "This package contains the runtime assemblies for Microsoft.Azure.WebJobs.Host. It also adds rich diagnostics capabilities which makes it easier to monitor the WebJobs in the dashboard. For more information, please visit http://go.microsoft.com/fwlink/?LinkID=320971",
|
||||
"description": "This library simplifies the task of adding background processing to your Microsoft Azure Web Sites. The SDK uses Microsoft Azure Storage, triggering a function in your program when items are added to Queues and Blobs. A dashboard provides rich monitoring and diagnostics for the programs that you write by using the SDK. For more information, please visit http://go.microsoft.com/fwlink/?LinkID=320971",
|
||||
"icon": "📀",
|
||||
"insertValue": "Microsoft.Azure.WebJobs",
|
||||
"name": "Microsoft.Azure.WebJobs",
|
||||
"insertValue": "Microsoft.Azure.WebJobs.Core",
|
||||
"name": "Microsoft.Azure.WebJobs.Core",
|
||||
"priority": 60,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -21,7 +21,7 @@ const testData = [
|
||||
{ name: "nestedNonCommands", command: "az az ", skip: true }, // TODO: fix skipped test
|
||||
{ name: "loadSpec", command: "aws acm add" },
|
||||
{ name: "noArgsArgumentGiven", command: "gcc lab ", maxSuggestions: 3 },
|
||||
{ name: "generatorUsingPartialInput", command: "dotnet add package Microsoft.Azure.WebJo", maxSuggestions: 1 },
|
||||
{ name: "generatorUsingPartialInput", command: "dotnet add package Microsoft.Azure.WebJobs.Cor", maxSuggestions: 1 },
|
||||
];
|
||||
|
||||
describe(`parseCommand`, () => {
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import { test, expect, Shell } from "@microsoft/tui-test";
|
||||
import os from "node:os";
|
||||
|
||||
const windowsShells = [Shell.Cmd, Shell.Powershell, Shell.WindowsPowershell];
|
||||
const windowsShells = [Shell.Cmd, Shell.Powershell, Shell.WindowsPowershell, "xonsh"];
|
||||
const unixShells = [Shell.Bash, Shell.Fish, Shell.Zsh];
|
||||
const shells = os.platform() == "win32" ? windowsShells : unixShells;
|
||||
|
||||
shells.map((activeShell) => {
|
||||
const returnChar = activeShell == "xonsh" ? "\n" : "\r";
|
||||
test.describe(`[${activeShell}]`, () => {
|
||||
test.use({ program: { file: "is", args: ["-V", "-T", "-s", activeShell] } });
|
||||
|
||||
@@ -113,7 +114,7 @@ shells.map((activeShell) => {
|
||||
test("ui on bottom of the screen", async ({ terminal }) => {
|
||||
await expect(terminal.getByText("> ")).toBeVisible();
|
||||
terminal.resize(80, 10);
|
||||
terminal.write("\r".repeat(10));
|
||||
terminal.write(returnChar.repeat(10));
|
||||
|
||||
terminal.write("git ");
|
||||
await expect(terminal.getByText("archive", { strict: false })).toBeVisible();
|
||||
@@ -122,7 +123,7 @@ shells.map((activeShell) => {
|
||||
test("command detection after command execution", async ({ terminal }) => {
|
||||
await expect(terminal.getByText("> ")).toBeVisible();
|
||||
|
||||
terminal.write(`echo "hello"\r`);
|
||||
terminal.write(`echo "hello"${returnChar}`);
|
||||
await expect(terminal.getByText("hello", { strict: false })).toBeVisible();
|
||||
|
||||
terminal.write("git ");
|
||||
@@ -132,7 +133,7 @@ shells.map((activeShell) => {
|
||||
test("command detection after command execution", async ({ terminal }) => {
|
||||
await expect(terminal.getByText("> ")).toBeVisible();
|
||||
|
||||
terminal.write(`echo "hello"\r`);
|
||||
terminal.write(`echo "hello"${returnChar}`);
|
||||
await expect(terminal.getByText("hello", { strict: false })).toBeVisible();
|
||||
|
||||
terminal.write("git ");
|
||||
@@ -145,7 +146,7 @@ shells.map((activeShell) => {
|
||||
terminal.write("git ");
|
||||
await expect(terminal.getByText("archive", { strict: false })).toBeVisible();
|
||||
|
||||
terminal.write("\r");
|
||||
terminal.write(returnChar);
|
||||
await expect(terminal.getByText("archive", { strict: false })).not.toBeVisible();
|
||||
});
|
||||
|
||||
@@ -155,7 +156,7 @@ shells.map((activeShell) => {
|
||||
terminal.write("clear");
|
||||
await expect(terminal.getByText("clear")).toBeVisible();
|
||||
|
||||
terminal.write("\r");
|
||||
terminal.write(returnChar);
|
||||
await expect(terminal.getByText("clear")).not.toBeVisible();
|
||||
|
||||
terminal.keyUp();
|
||||
@@ -172,10 +173,10 @@ shells.map((activeShell) => {
|
||||
test.skip("command detection with suggestions", async ({ terminal }) => {
|
||||
await expect(terminal.getByText("> ")).toBeVisible();
|
||||
|
||||
terminal.write(`dotnet add item\r`);
|
||||
terminal.write(`dotnet add item${returnChar}`);
|
||||
await expect(terminal.getByText("dotnet", { strict: false })).toBeVisible();
|
||||
|
||||
terminal.write("clear\r");
|
||||
terminal.write(`clear${returnChar}`);
|
||||
await expect(terminal.getByText("dotnet", { strict: false })).not.toBeVisible();
|
||||
|
||||
terminal.write("dotnet add ");
|
||||
|
||||
@@ -17,6 +17,7 @@ export enum Shell {
|
||||
Zsh = "zsh",
|
||||
Fish = "fish",
|
||||
Cmd = "cmd",
|
||||
Xonsh = "xonsh",
|
||||
}
|
||||
|
||||
export const supportedShells = [
|
||||
@@ -26,6 +27,7 @@ export const supportedShells = [
|
||||
Shell.Zsh,
|
||||
Shell.Fish,
|
||||
process.platform == "win32" ? Shell.Cmd : null,
|
||||
Shell.Xonsh,
|
||||
].filter((shell) => shell != null) as Shell[];
|
||||
|
||||
export const userZdotdir = process.env?.ZDOTDIR ?? os.homedir() ?? `~`;
|
||||
@@ -72,6 +74,10 @@ export const gitBashPath = async (): Promise<string> => {
|
||||
throw new Error("unable to find a git bash executable installed");
|
||||
};
|
||||
|
||||
export const getPythonPath = async (): Promise<string> => {
|
||||
return await which("python", { nothrow: true });
|
||||
};
|
||||
|
||||
const getGitBashPaths = async (): Promise<string[]> => {
|
||||
const gitDirs: Set<string> = new Set();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user