@microsoft/inshellisense 0.0.1-rc.8 → 0.0.1-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,8 +8,7 @@
8
8
 
9
9
  ### Requirements
10
10
 
11
- - `21 < node >= 16.x`
12
- - node-gyp dependencies installed for your platform (see [node-gyp](https://github.com/nodejs/node-gyp) for more details)
11
+ - Node.js 20.X, 18.X, 16.X (16.6.0 >=)
13
12
 
14
13
  ### Installation
15
14
 
@@ -31,7 +30,7 @@ After completing the installation, you can run `is` to start the autocomplete se
31
30
 
32
31
  #### Keybindings
33
32
 
34
- All other keys are passed through to the shell. The keybindings below are only captured when the inshellisense suggestions are visible, otherwise they are passed through to the shell as well.
33
+ All other keys are passed through to the shell. The keybindings below are only captured when the inshellisense suggestions are visible, otherwise they are passed through to the shell as well. These can be customized in the [config](#configuration).
35
34
 
36
35
  | Action | Keybinding |
37
36
  | ------------------------- | -------------- |
@@ -50,6 +49,46 @@ inshellisense supports the following shells:
50
49
  - [pwsh](https://github.com/PowerShell/PowerShell)
51
50
  - [powershell](https://learn.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell) (Windows Powershell)
52
51
  - [cmd](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd) _(experimental)_
52
+ - [xonsh](https://xon.sh/)
53
+
54
+ ## Configuration
55
+
56
+ All configuration is done through a [toml](https://toml.io/) file located at `~/.inshellisenserc`. The [JSON schema](https://json-schema.org/) for the configuration file can be found [here](https://github.com/microsoft/inshellisense/blob/main/src/utils/config.ts).
57
+
58
+ ### Keybindings
59
+
60
+ You can customize the keybindings for inshellisense by adding a `bindings` section to your config file. The following is the default configuration for the [keybindings](#keybindings):
61
+
62
+ ```toml
63
+ [bindings.acceptSuggestion]
64
+ key = "tab"
65
+ # shift and tab are optional and default to false
66
+ shift = false
67
+ ctrl = false
68
+
69
+ [bindings.nextSuggestion]
70
+ key = "down"
71
+
72
+ [bindings.previousSuggestion]
73
+ key = "up"
74
+
75
+ [bindings.dismissSuggestions]
76
+ key = "escape"
77
+ ```
78
+
79
+ Key names are matched against the Node.js [keypress](https://nodejs.org/api/readline.html#readlineemitkeypresseventsstream-interface) events.
80
+
81
+ ### Custom Prompts (Windows)
82
+
83
+ If you are using a custom prompt in your shell (anything that is not the default PS1), you will need to set up a custom prompt in the inshellisense config file. This is because Windows strips details from your prompt which are required for inshellisense to work. To do this, update your config file in your home directory and add the following configuration:
84
+
85
+ ```toml
86
+ [[prompt.bash]]
87
+ regex = "(?<prompt>^>\\s*)" # the prompt match group will be used to detect the prompt
88
+ postfix = ">" # the postfix is the last expected character in your prompt
89
+ ```
90
+
91
+ This example adds custom prompt detection for bash where the prompt is expected to be only `> `. You can add similar configurations for other shells as well as well as multiple configurations for each shell.
53
92
 
54
93
  ## Contributing
55
94
 
@@ -7,6 +7,7 @@ import { Shell } from "../utils/shell.js";
7
7
  const action = async (input) => {
8
8
  const suggestions = await getSuggestions(input, process.cwd(), os.platform() === "win32" ? Shell.Cmd : Shell.Bash);
9
9
  process.stdout.write(JSON.stringify(suggestions));
10
+ process.exit(0);
10
11
  };
11
12
  const cmd = new Command("complete");
12
13
  cmd.description(`generates a completion for the provided input`);
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import convert from "color-convert";
3
4
  import os from "node:os";
4
5
  import { Shell } from "../utils/shell.js";
5
6
  import log from "../utils/log.js";
@@ -57,11 +58,13 @@ export class CommandManager {
57
58
  // User defined prompt
58
59
  const inshellisenseConfig = getConfig();
59
60
  if (this.#shell == Shell.Bash) {
60
- if (inshellisenseConfig.promptRegex?.bash != null) {
61
- const customBashPrompt = lineText.match(new RegExp(inshellisenseConfig.promptRegex?.bash.regex))?.groups?.prompt;
62
- const adjustedPrompt = this._adjustPrompt(customBashPrompt, lineText, inshellisenseConfig.promptRegex?.bash.postfix);
63
- if (adjustedPrompt) {
64
- return adjustedPrompt;
61
+ if (inshellisenseConfig?.prompt?.bash != null) {
62
+ for (const { regex, postfix } of inshellisenseConfig.prompt.bash) {
63
+ const customPrompt = lineText.match(new RegExp(regex))?.groups?.prompt;
64
+ const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, postfix);
65
+ if (adjustedPrompt) {
66
+ return adjustedPrompt;
67
+ }
65
68
  }
66
69
  }
67
70
  const bashPrompt = lineText.match(/^(?<prompt>.*\$\s?)/)?.groups?.prompt;
@@ -72,21 +75,50 @@ export class CommandManager {
72
75
  }
73
76
  }
74
77
  }
75
- if (this.#shell == Shell.Powershell || this.#shell == Shell.Pwsh) {
76
- if (inshellisenseConfig.promptRegex?.pwsh != null && this.#shell == Shell.Pwsh) {
77
- const customPwshPrompt = lineText.match(new RegExp(inshellisenseConfig.promptRegex?.pwsh.regex))?.groups?.prompt;
78
- const adjustedPrompt = this._adjustPrompt(customPwshPrompt, lineText, inshellisenseConfig.promptRegex?.pwsh.postfix);
78
+ if (this.#shell == Shell.Xonsh) {
79
+ if (inshellisenseConfig?.prompt?.xonsh != null) {
80
+ for (const { regex, postfix } of inshellisenseConfig.prompt.xonsh) {
81
+ const customPrompt = lineText.match(new RegExp(regex))?.groups?.prompt;
82
+ const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, postfix);
83
+ if (adjustedPrompt) {
84
+ return adjustedPrompt;
85
+ }
86
+ }
87
+ }
88
+ let xonshPrompt = lineText.match(/(?<prompt>.*@\s?)/)?.groups?.prompt;
89
+ if (xonshPrompt) {
90
+ const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, "@");
79
91
  if (adjustedPrompt) {
80
92
  return adjustedPrompt;
81
93
  }
82
94
  }
83
- if (inshellisenseConfig.promptRegex?.powershell != null && this.#shell == Shell.Powershell) {
84
- const customPowershellPrompt = lineText.match(new RegExp(inshellisenseConfig.promptRegex?.powershell.regex))?.groups?.prompt;
85
- const adjustedPrompt = this._adjustPrompt(customPowershellPrompt, lineText, inshellisenseConfig.promptRegex?.powershell.postfix);
95
+ xonshPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
96
+ if (xonshPrompt) {
97
+ const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, ">");
86
98
  if (adjustedPrompt) {
87
99
  return adjustedPrompt;
88
100
  }
89
101
  }
102
+ }
103
+ if (this.#shell == Shell.Powershell || this.#shell == Shell.Pwsh) {
104
+ if (inshellisenseConfig?.prompt?.powershell != null) {
105
+ for (const { regex, postfix } of inshellisenseConfig.prompt.powershell) {
106
+ const customPrompt = lineText.match(new RegExp(regex))?.groups?.prompt;
107
+ const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, postfix);
108
+ if (adjustedPrompt) {
109
+ return adjustedPrompt;
110
+ }
111
+ }
112
+ }
113
+ if (inshellisenseConfig?.prompt?.pwsh != null) {
114
+ for (const { regex, postfix } of inshellisenseConfig.prompt.pwsh) {
115
+ const customPrompt = lineText.match(new RegExp(regex))?.groups?.prompt;
116
+ const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, postfix);
117
+ if (adjustedPrompt) {
118
+ return adjustedPrompt;
119
+ }
120
+ }
121
+ }
90
122
  const pwshPrompt = lineText.match(/(?<prompt>(\(.+\)\s)?(?:PS.+>\s?))/)?.groups?.prompt;
91
123
  if (pwshPrompt) {
92
124
  const adjustedPrompt = this._adjustPrompt(pwshPrompt, lineText, ">");
@@ -117,8 +149,16 @@ export class CommandManager {
117
149
  }
118
150
  return prompt;
119
151
  }
152
+ _getFgPaletteColor(cell) {
153
+ if (cell?.isFgDefault())
154
+ return 0;
155
+ if (cell?.isFgPalette())
156
+ return cell.getFgColor();
157
+ if (cell?.isFgRGB())
158
+ return convert.hex.ansi256(cell.getFgColor().toString(16));
159
+ }
120
160
  _isSuggestion(cell) {
121
- const color = cell?.getFgColor();
161
+ const color = this._getFgPaletteColor(cell);
122
162
  const dim = (cell?.isDim() ?? 0) > 0;
123
163
  const italic = (cell?.isItalic() ?? 0) > 0;
124
164
  const dullColor = color == 8 || color == 7 || (color ?? 0) > 235 || (color == 15 && dim);
@@ -5,8 +5,9 @@ import process from "node:process";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import url from "node:url";
8
+ import fs from "node:fs";
8
9
  import pty from "@homebridge/node-pty-prebuilt-multiarch";
9
- import { Shell, userZdotdir, zdotdir } from "../utils/shell.js";
10
+ import { Shell, getPythonPath, userZdotdir, zdotdir } from "../utils/shell.js";
10
11
  import { IsTermOscPs, IstermOscPt, IstermPromptStart, IstermPromptEnd } from "../utils/ansi.js";
11
12
  import xterm from "xterm-headless";
12
13
  import { CommandManager } from "./commandManager.js";
@@ -218,7 +219,7 @@ export const spawn = async (options) => {
218
219
  };
219
220
  const convertToPtyTarget = async (shell) => {
220
221
  const platform = os.platform();
221
- const shellTarget = shell == Shell.Bash && platform == "win32" ? await gitBashPath() : platform == "win32" ? `${shell}.exe` : shell;
222
+ let shellTarget = shell == Shell.Bash && platform == "win32" ? await gitBashPath() : platform == "win32" ? `${shell}.exe` : shell;
222
223
  const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");
223
224
  let shellArgs = [];
224
225
  switch (shell) {
@@ -232,6 +233,18 @@ const convertToPtyTarget = async (shell) => {
232
233
  case Shell.Fish:
233
234
  shellArgs = ["--init-command", `. ${path.join(shellFolderPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`];
234
235
  break;
236
+ case Shell.Xonsh: {
237
+ const sharedConfig = os.platform() == "win32" ? path.join("C:\\ProgramData", "xonsh", "xonshrc") : path.join("etc", "xonsh", "xonshrc");
238
+ const userConfigs = [
239
+ path.join(os.homedir(), ".xonshrc"),
240
+ path.join(os.homedir(), ".config", "xonsh", "rc.xsh"),
241
+ path.join(os.homedir(), ".config", "xonsh", "rc.d"),
242
+ ];
243
+ const configs = [sharedConfig, ...userConfigs].filter((config) => fs.existsSync(config));
244
+ shellArgs = ["-m", "xonsh", "--rc", ...configs, path.join(shellFolderPath, "shellIntegration.xsh")];
245
+ shellTarget = await getPythonPath();
246
+ break;
247
+ }
235
248
  }
236
249
  return { shellTarget, shellArgs };
237
250
  };
@@ -23,13 +23,14 @@ export const runGenerator = async (generator, tokens, cwd) => {
23
23
  if (script) {
24
24
  const shellInput = typeof script === "function" ? script(tokens) : script;
25
25
  const scriptOutput = Array.isArray(shellInput)
26
- ? await executeShellCommand({ command: shellInput.at(0) ?? "", args: shellInput.slice(1) })
27
- : await executeShellCommand(shellInput);
26
+ ? await executeShellCommand({ command: shellInput.at(0) ?? "", args: shellInput.slice(1), cwd })
27
+ : await executeShellCommand({ ...shellInput, cwd });
28
+ const scriptStdout = scriptOutput.stdout.trim();
28
29
  if (postProcess) {
29
- suggestions.push(...postProcess(scriptOutput.stdout, tokens));
30
+ suggestions.push(...postProcess(scriptStdout, tokens));
30
31
  }
31
32
  else if (splitOn) {
32
- suggestions.push(...scriptOutput.stdout.split(splitOn).map((s) => ({ name: s })));
33
+ suggestions.push(...scriptStdout.split(splitOn).map((s) => ({ name: s })));
33
34
  }
34
35
  }
35
36
  if (custom) {
@@ -47,7 +48,8 @@ export const runGenerator = async (generator, tokens, cwd) => {
47
48
  return suggestions;
48
49
  }
49
50
  catch (e) {
50
- log.debug({ msg: "generator failed", e, script, splitOn, template });
51
+ const err = typeof e === "string" ? e : e instanceof Error ? e.message : e;
52
+ log.debug({ msg: "generator failed", err, script, splitOn, template });
51
53
  }
52
54
  return suggestions;
53
55
  };
@@ -15,7 +15,11 @@ var SuggestionIcons;
15
15
  SuggestionIcons["Special"] = "\u2B50";
16
16
  SuggestionIcons["Default"] = "\uD83D\uDCC0";
17
17
  })(SuggestionIcons || (SuggestionIcons = {}));
18
- const getIcon = (suggestionType) => {
18
+ const getIcon = (icon, suggestionType) => {
19
+ // TODO: enable fig icons once spacing is better
20
+ // if (icon && /[^\u0000-\u00ff]/.test(icon)) {
21
+ // return icon;
22
+ // }
19
23
  switch (suggestionType) {
20
24
  case "arg":
21
25
  return SuggestionIcons.Argument;
@@ -45,7 +49,7 @@ const toSuggestion = (suggestion, name, type) => {
45
49
  return {
46
50
  name: name ?? getLong(suggestion.name),
47
51
  description: suggestion.description,
48
- icon: getIcon(type ?? suggestion.type),
52
+ icon: getIcon(suggestion.icon, type ?? suggestion.type),
49
53
  allNames: suggestion.name instanceof Array ? suggestion.name : [suggestion.name],
50
54
  priority: suggestion.priority ?? 50,
51
55
  insertValue: suggestion.insertValue,
@@ -66,7 +70,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
66
70
  ? {
67
71
  name: matchedName,
68
72
  description: s.description,
69
- icon: getIcon(s.type ?? suggestionType),
73
+ icon: getIcon(s.icon, s.type ?? suggestionType),
70
74
  allNames: s.name,
71
75
  priority: s.priority ?? 50,
72
76
  insertValue: s.insertValue,
@@ -77,7 +81,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
77
81
  ? {
78
82
  name: s.name,
79
83
  description: s.description,
80
- icon: getIcon(s.type ?? suggestionType),
84
+ icon: getIcon(s.icon, s.type ?? suggestionType),
81
85
  allNames: [s.name],
82
86
  priority: s.priority ?? 50,
83
87
  insertValue: s.insertValue,
@@ -96,7 +100,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
96
100
  ? {
97
101
  name: matchedName,
98
102
  description: s.description,
99
- icon: getIcon(s.type ?? suggestionType),
103
+ icon: getIcon(s.icon, s.type ?? suggestionType),
100
104
  allNames: s.name,
101
105
  insertValue: s.insertValue,
102
106
  priority: s.priority ?? 50,
@@ -107,7 +111,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
107
111
  ? {
108
112
  name: s.name,
109
113
  description: s.description,
110
- icon: getIcon(s.type ?? suggestionType),
114
+ icon: getIcon(s.icon, s.type ?? suggestionType),
111
115
  allNames: [s.name],
112
116
  insertValue: s.insertValue,
113
117
  priority: s.priority ?? 50,
@@ -120,8 +124,10 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
120
124
  const generatorSuggestions = async (generator, acceptedTokens, filterStrategy, partialCmd, cwd) => {
121
125
  const generators = generator instanceof Array ? generator : generator ? [generator] : [];
122
126
  const tokens = acceptedTokens.map((t) => t.token);
127
+ if (partialCmd)
128
+ tokens.push(partialCmd);
123
129
  const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat();
124
- return filter(suggestions, filterStrategy, partialCmd, undefined);
130
+ return filter(suggestions.map((suggestion) => ({ ...suggestion, priority: suggestion.priority ?? 60 })), filterStrategy, partialCmd, undefined);
125
131
  };
126
132
  const templateSuggestions = async (templates, filterStrategy, partialCmd, cwd) => {
127
133
  return filter(await runTemplates(templates ?? [], cwd), filterStrategy, partialCmd, undefined);
@@ -4,11 +4,11 @@ import fsAsync from "node:fs/promises";
4
4
  import log from "../utils/log.js";
5
5
  const filepathsTemplate = async (cwd) => {
6
6
  const files = await fsAsync.readdir(cwd, { withFileTypes: true });
7
- return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "filepaths" } }));
7
+ return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 55, context: { templateType: "filepaths" } }));
8
8
  };
9
9
  const foldersTemplate = async (cwd) => {
10
10
  const files = await fsAsync.readdir(cwd, { withFileTypes: true });
11
- return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "folders" } }));
11
+ return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 55, context: { templateType: "folders" } }));
12
12
  };
13
13
  // TODO: implement history template
14
14
  const historyTemplate = () => {
@@ -5,6 +5,7 @@ import { renderBox, truncateText, truncateMultilineText } from "./utils.js";
5
5
  import ansi from "ansi-escapes";
6
6
  import chalk from "chalk";
7
7
  import log from "../utils/log.js";
8
+ import { getConfig } from "../utils/config.js";
8
9
  const maxSuggestions = 5;
9
10
  const suggestionWidth = 40;
10
11
  const descriptionWidth = 30;
@@ -57,7 +58,7 @@ export class SuggestionManager {
57
58
  }
58
59
  _renderSuggestions(suggestions, activeSuggestionIdx, x) {
59
60
  return renderBox(suggestions.map((suggestion, idx) => {
60
- const suggestionText = `${suggestion.icon} ${suggestion.name}`.padEnd(suggestionWidth - borderWidth, " ");
61
+ const suggestionText = `${suggestion.icon} ${suggestion.name}`;
61
62
  const truncatedSuggestion = truncateText(suggestionText, suggestionWidth - 2);
62
63
  return idx == activeSuggestionIdx ? chalk.bgHex(activeSuggestionBackgroundColor)(truncatedSuggestion) : truncatedSuggestion;
63
64
  }), suggestionWidth, x);
@@ -120,20 +121,21 @@ export class SuggestionManager {
120
121
  };
121
122
  }
122
123
  update(keyPress) {
123
- const { name } = keyPress;
124
+ const { name, shift, ctrl } = keyPress;
124
125
  if (!this.#suggestBlob) {
125
126
  return false;
126
127
  }
127
- if (name == "escape") {
128
+ const { dismissSuggestions: { key: dismissKey, shift: dismissShift, control: dismissCtrl }, acceptSuggestion: { key: acceptKey, shift: acceptShift, control: acceptCtrl }, nextSuggestion: { key: nextKey, shift: nextShift, control: nextCtrl }, previousSuggestion: { key: prevKey, shift: prevShift, control: prevCtrl }, } = getConfig().bindings;
129
+ if (name == dismissKey && shift == !!dismissShift && ctrl == !!dismissCtrl) {
128
130
  this.#suggestBlob = undefined;
129
131
  }
130
- else if (name == "up") {
132
+ else if (name == prevKey && shift == !!prevShift && ctrl == !!prevCtrl) {
131
133
  this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
132
134
  }
133
- else if (name == "down") {
135
+ else if (name == nextKey && shift == !!nextShift && ctrl == !!nextCtrl) {
134
136
  this.#activeSuggestionIdx = Math.min(this.#activeSuggestionIdx + 1, (this.#suggestBlob?.suggestions.length ?? 1) - 1);
135
137
  }
136
- else if (name == "tab") {
138
+ else if (name == acceptKey && shift == !!acceptShift && ctrl == !!acceptCtrl) {
137
139
  const removals = "\u007F".repeat(this.#suggestBlob?.charactersToDrop ?? 0);
138
140
  const suggestion = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx);
139
141
  const chars = suggestion?.insertValue ?? suggestion?.name + " ";
@@ -1,9 +1,9 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import chalk from "chalk";
4
- import { deleteConfigFolder } from "../utils/config.js";
4
+ import { deleteCacheFolder } from "../utils/config.js";
5
5
  export const render = async () => {
6
- deleteConfigFolder();
7
- process.stdout.write(chalk.green("✓") + " successfully deleted the .inshellisense config folder \n");
6
+ deleteCacheFolder();
7
+ process.stdout.write(chalk.green("✓") + " successfully deleted the .inshellisense cache folder \n");
8
8
  process.stdout.write(chalk.magenta("•") + " to complete the uninstall, run the the command: " + chalk.underline(chalk.cyan("npm uninstall -g @microsoft/inshellisense")) + "\n");
9
9
  };
package/build/ui/utils.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import ansi from "ansi-escapes";
4
4
  import wrapAnsi from "wrap-ansi";
5
5
  import chalk from "chalk";
6
+ import wcwdith from "wcwidth";
6
7
  /**
7
8
  * Renders a box around the given rows
8
9
  * @param rows the text content to be included in the box, must be <= width - 2
@@ -36,6 +37,7 @@ export const truncateMultilineText = (description, width, maxHeight) => {
36
37
  */
37
38
  export const truncateText = (text, width) => {
38
39
  const textPoints = [...text];
39
- const slicedText = textPoints.slice(0, width - 1);
40
+ const wcOffset = Math.max(wcwdith(text) - textPoints.length, 0);
41
+ const slicedText = textPoints.slice(0, width - 1 - wcOffset);
40
42
  return slicedText.length == textPoints.length ? text.padEnd(width) : (slicedText.join("") + "…").padEnd(width);
41
43
  };
@@ -4,64 +4,103 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import fs from "node:fs";
6
6
  import fsAsync from "node:fs/promises";
7
+ import toml from "toml";
7
8
  import _Ajv from "ajv";
8
9
  const Ajv = _Ajv;
9
10
  const ajv = new Ajv();
11
+ const bindingSchema = {
12
+ type: "object",
13
+ nullable: true,
14
+ properties: {
15
+ shift: { type: "boolean", nullable: true },
16
+ control: { type: "boolean", nullable: true },
17
+ key: { type: "string" },
18
+ },
19
+ required: ["key"],
20
+ };
21
+ const promptPatternsSchema = {
22
+ type: "array",
23
+ nullable: true,
24
+ items: {
25
+ type: "object",
26
+ properties: {
27
+ regex: { type: "string" },
28
+ postfix: { type: "string" },
29
+ },
30
+ required: ["regex", "postfix"],
31
+ },
32
+ };
10
33
  const configSchema = {
11
34
  type: "object",
35
+ nullable: true,
12
36
  properties: {
13
- promptRegex: {
37
+ bindings: {
14
38
  type: "object",
39
+ nullable: true,
15
40
  properties: {
16
- bash: {
17
- type: "object",
18
- nullable: true,
19
- properties: {
20
- regex: { type: "string" },
21
- postfix: { type: "string" },
22
- },
23
- required: ["regex", "postfix"],
24
- },
25
- pwsh: {
26
- type: "object",
27
- nullable: true,
28
- properties: {
29
- regex: { type: "string" },
30
- postfix: { type: "string" },
31
- },
32
- required: ["regex", "postfix"],
33
- },
34
- powershell: {
35
- type: "object",
36
- nullable: true,
37
- properties: {
38
- regex: { type: "string" },
39
- postfix: { type: "string" },
40
- },
41
- required: ["regex", "postfix"],
42
- },
41
+ nextSuggestion: bindingSchema,
42
+ previousSuggestion: bindingSchema,
43
+ dismissSuggestions: bindingSchema,
44
+ acceptSuggestion: bindingSchema,
43
45
  },
46
+ },
47
+ prompt: {
48
+ type: "object",
44
49
  nullable: true,
50
+ properties: {
51
+ bash: promptPatternsSchema,
52
+ pwsh: promptPatternsSchema,
53
+ powershell: promptPatternsSchema,
54
+ xonsh: promptPatternsSchema,
55
+ },
45
56
  },
46
57
  },
47
58
  additionalProperties: false,
48
59
  };
49
- const configFolder = ".inshellisense";
50
- const cachePath = path.join(os.homedir(), configFolder, "config.json");
51
- let globalConfig = {};
60
+ const configFile = ".inshellisenserc";
61
+ const cachePath = path.join(os.homedir(), ".inshellisense");
62
+ const configPath = path.join(os.homedir(), configFile);
63
+ let globalConfig = {
64
+ bindings: {
65
+ nextSuggestion: { key: "down" },
66
+ previousSuggestion: { key: "up" },
67
+ acceptSuggestion: { key: "tab" },
68
+ dismissSuggestions: { key: "escape" },
69
+ },
70
+ };
52
71
  export const getConfig = () => globalConfig;
53
72
  export const loadConfig = async (program) => {
54
- if (fs.existsSync(cachePath)) {
55
- const config = JSON.parse((await fsAsync.readFile(cachePath)).toString());
73
+ if (fs.existsSync(configPath)) {
74
+ let config;
75
+ try {
76
+ config = toml.parse((await fsAsync.readFile(configPath)).toString());
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ }
79
+ catch (e) {
80
+ program.error(`${configFile} is invalid toml. Parsing error on line ${e.line}, column ${e.column}: ${e.message}`);
81
+ }
56
82
  const isValid = ajv.validate(configSchema, config);
57
83
  if (!isValid) {
58
- program.error("inshellisense config is invalid: " + ajv.errorsText());
84
+ program.error(`${configFile} is invalid: ${ajv.errorsText()}`);
59
85
  }
60
- globalConfig = config;
86
+ globalConfig = {
87
+ bindings: {
88
+ nextSuggestion: config?.bindings?.nextSuggestion ?? globalConfig.bindings.nextSuggestion,
89
+ previousSuggestion: config?.bindings?.previousSuggestion ?? globalConfig.bindings.previousSuggestion,
90
+ acceptSuggestion: config?.bindings?.acceptSuggestion ?? globalConfig.bindings.acceptSuggestion,
91
+ dismissSuggestions: config?.bindings?.dismissSuggestions ?? globalConfig.bindings.dismissSuggestions,
92
+ },
93
+ prompt: {
94
+ bash: config.prompt?.bash,
95
+ powershell: config.prompt?.powershell,
96
+ xonsh: config.prompt?.xonsh,
97
+ pwsh: config.prompt?.pwsh,
98
+ },
99
+ };
61
100
  }
62
101
  };
63
- export const deleteConfigFolder = async () => {
64
- const cliConfigPath = path.join(os.homedir(), configFolder);
102
+ export const deleteCacheFolder = async () => {
103
+ const cliConfigPath = path.join(os.homedir(), cachePath);
65
104
  if (fs.existsSync(cliConfigPath)) {
66
105
  fs.rmSync(cliConfigPath, { recursive: true });
67
106
  }
@@ -16,6 +16,7 @@ export var Shell;
16
16
  Shell["Zsh"] = "zsh";
17
17
  Shell["Fish"] = "fish";
18
18
  Shell["Cmd"] = "cmd";
19
+ Shell["Xonsh"] = "xonsh";
19
20
  })(Shell || (Shell = {}));
20
21
  export const supportedShells = [
21
22
  Shell.Bash,
@@ -24,6 +25,7 @@ export const supportedShells = [
24
25
  Shell.Zsh,
25
26
  Shell.Fish,
26
27
  process.platform == "win32" ? Shell.Cmd : null,
28
+ Shell.Xonsh,
27
29
  ].filter((shell) => shell != null);
28
30
  export const userZdotdir = process.env?.ZDOTDIR ?? os.homedir() ?? `~`;
29
31
  export const zdotdir = path.join(os.tmpdir(), `is-zsh`);
@@ -66,6 +68,9 @@ export const gitBashPath = async () => {
66
68
  }
67
69
  throw new Error("unable to find a git bash executable installed");
68
70
  };
71
+ export const getPythonPath = async () => {
72
+ return await which("python", { nothrow: true });
73
+ };
69
74
  const getGitBashPaths = async () => {
70
75
  const gitDirs = new Set();
71
76
  const gitExePath = await which("git.exe", { nothrow: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/inshellisense",
3
- "version": "0.0.1-rc.8",
3
+ "version": "0.0.1-rc.9",
4
4
  "description": "IDE style command line auto complete",
5
5
  "type": "module",
6
6
  "engines": {
@@ -40,22 +40,27 @@
40
40
  "homepage": "https://github.com/microsoft/inshellisense#readme",
41
41
  "dependencies": {
42
42
  "@homebridge/node-pty-prebuilt-multiarch": "^0.11.12",
43
- "@microsoft/tui-test": "^0.0.1-rc.3",
44
43
  "@withfig/autocomplete": "2.651.0",
45
44
  "ajv": "^8.12.0",
46
45
  "ansi-escapes": "^6.2.0",
47
46
  "ansi-styles": "^6.2.1",
48
47
  "chalk": "^5.3.0",
48
+ "color-convert": "^2.0.1",
49
49
  "commander": "^11.0.0",
50
50
  "find-process": "^1.4.7",
51
+ "toml": "^3.0.0",
52
+ "wcwidth": "^1.0.1",
51
53
  "which": "^4.0.0",
52
54
  "wrap-ansi": "^8.1.0",
53
55
  "xterm-headless": "^5.3.0"
54
56
  },
55
57
  "devDependencies": {
58
+ "@microsoft/tui-test": "^0.0.1-rc.3",
56
59
  "@tsconfig/node18": "^18.2.2",
60
+ "@types/color-convert": "^2.0.3",
57
61
  "@types/jest": "^29.5.5",
58
62
  "@types/react": "^18.2.24",
63
+ "@types/wcwidth": "^1.0.2",
59
64
  "@types/which": "^3.0.3",
60
65
  "@typescript-eslint/eslint-plugin": "^6.7.4",
61
66
  "@typescript-eslint/parser": "^6.7.4",
@@ -0,0 +1,29 @@
1
+ import os
2
+
3
+ def __is_prompt_start() -> str:
4
+ return "\001" + "\x1b]6973;PS\x07"
5
+
6
+
7
+ def __is_prompt_end() -> str:
8
+ return "\001" + "\x1b]6973;PE\x07" + "\002"
9
+
10
+
11
+ def __is_escape_value(value: str) -> str:
12
+ byte_list = [bytes([byte]).decode("utf-8") for byte in list(value.encode("utf-8"))]
13
+ return "".join(
14
+ [
15
+ "\\x3b" if byte == ";" else "\\\\" if byte == "\\" else byte
16
+ for byte in byte_list
17
+ ]
18
+ )
19
+
20
+ def __is_update_cwd() -> str:
21
+ return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\x07" + "\002"
22
+
23
+ $PROMPT_FIELDS['__is_prompt_start'] = __is_prompt_start
24
+ $PROMPT_FIELDS['__is_prompt_end'] = __is_prompt_end
25
+ $PROMPT_FIELDS['__is_update_cwd'] = __is_update_cwd
26
+ if $ISTERM_TESTING:
27
+ $PROMPT = "> "
28
+
29
+ $PROMPT = "{__is_prompt_start}{__is_update_cwd}" + $PROMPT + "{__is_prompt_end}"