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:
Chapman Pendery
2024-03-08 18:32:25 -08:00
committed by GitHub
parent c4ab6cfa8d
commit ce8025aaed
11 changed files with 484 additions and 919 deletions
+6
View File
@@ -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
View File
@@ -1,4 +1,5 @@
node_modules/
build/
t*.md
.tui-test/
.tui-test/
tui-traces/
+381 -900
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -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",
+29
View File
@@ -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}"
+26 -1
View File
@@ -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
View File
@@ -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,
},
],
+1 -1
View File
@@ -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`, () => {
+9 -8
View File
@@ -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 ");
+6
View File
@@ -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();