@microsoft/inshellisense 0.0.1-rc.7 → 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
- - `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`);
@@ -28,5 +28,5 @@ export const action = (program) => async (options) => {
28
28
  else if (shell == Shell.Bash) {
29
29
  await setupBashPreExec();
30
30
  }
31
- await render(shell);
31
+ await render(shell, options.test ?? false);
32
32
  };
package/build/index.js CHANGED
@@ -2,12 +2,17 @@
2
2
  // Copyright (c) Microsoft Corporation.
3
3
  // Licensed under the MIT License.
4
4
  /* eslint-disable header/header */
5
- import { Command } from "commander";
5
+ import { Command, Option } from "commander";
6
6
  import complete from "./commands/complete.js";
7
7
  import uninstall from "./commands/uninstall.js";
8
8
  import { action, supportedShells } from "./commands/root.js";
9
9
  import { getVersion } from "./utils/version.js";
10
10
  const program = new Command();
11
+ const hiddenOption = (flags, description) => {
12
+ const option = new Option(flags, description);
13
+ option.hidden = true;
14
+ return option;
15
+ };
11
16
  program
12
17
  .name("inshellisense")
13
18
  .description("IDE style command line auto complete")
@@ -15,6 +20,7 @@ program
15
20
  .action(action(program))
16
21
  .option("-s, --shell <shell>", `shell to use for command execution, supported shells: ${supportedShells}`)
17
22
  .option("-c, --check", `check if shell is in an inshellisense session`)
23
+ .addOption(hiddenOption("-T, --test", "used to make e2e tests reproducible across machines"))
18
24
  .option("-V, --verbose", `enable verbose logging`)
19
25
  .showHelpAfterError("(add --help for additional information)");
20
26
  program.addCommand(complete);
@@ -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, ">");
@@ -96,7 +128,7 @@ export class CommandManager {
96
128
  }
97
129
  }
98
130
  if (this.#shell == Shell.Cmd) {
99
- return lineText.match(/^(?<prompt>(\(.+\)\s)?(?:[A-Z]:\\.*>))/)?.groups?.prompt;
131
+ return lineText.match(/^(?<prompt>(\(.+\)\s)?(?:[A-Z]:\\.*>)|(> ))/)?.groups?.prompt;
100
132
  }
101
133
  // Custom prompts like starship end in the common \u276f character
102
134
  const customPrompt = lineText.match(/.*\u276f(?=[^\u276f]*$)/g)?.[0];
@@ -117,14 +149,25 @@ 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();
122
- const dullColor = color == 8 || (color ?? 0) > 235;
161
+ const color = this._getFgPaletteColor(cell);
162
+ const dim = (cell?.isDim() ?? 0) > 0;
163
+ const italic = (cell?.isItalic() ?? 0) > 0;
164
+ const dullColor = color == 8 || color == 7 || (color ?? 0) > 235 || (color == 15 && dim);
165
+ const dullItalic = (color ?? 0) > 235 || (dullColor && italic);
123
166
  if (this.#shell == Shell.Powershell) {
124
167
  return false;
125
168
  }
126
169
  else if (this.#shell == Shell.Pwsh) {
127
- return (color ?? 0) > 235;
170
+ return dullItalic;
128
171
  }
129
172
  return dullColor;
130
173
  }
@@ -141,16 +184,15 @@ export class CommandManager {
141
184
  if (this.#activeCommand.promptEndMarker == null || this.#activeCommand.promptStartMarker == null) {
142
185
  return;
143
186
  }
144
- const promptEndMarker = this.#activeCommand.promptEndMarker;
145
- const promptStartMarker = this.#activeCommand.promptStartMarker;
146
187
  const globalCursorPosition = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
147
188
  const withinPollDistance = globalCursorPosition < this.#activeCommand.promptEndMarker.line + 5;
148
- if (globalCursorPosition < promptStartMarker.line) {
189
+ if (globalCursorPosition < this.#activeCommand.promptStartMarker.line) {
149
190
  this.handleClear();
191
+ this.#activeCommand.promptEndMarker = this.#terminal.registerMarker(0);
150
192
  }
151
193
  // if we haven't fond the prompt yet, poll over the next 5 lines searching for it
152
194
  if (this.#activeCommand.promptText == null && withinPollDistance) {
153
- for (let i = globalCursorPosition; i < promptEndMarker.line + maxPromptPollDistance; i++) {
195
+ for (let i = globalCursorPosition; i < this.#activeCommand.promptEndMarker.line + maxPromptPollDistance; i++) {
154
196
  if (this.#previousCommandLines.has(i))
155
197
  continue;
156
198
  const promptResult = this._getWindowsPrompt(i);
@@ -159,17 +201,18 @@ export class CommandManager {
159
201
  this.#activeCommand.promptEndX = promptResult.length;
160
202
  this.#activeCommand.promptText = promptResult;
161
203
  this.#previousCommandLines.add(i);
204
+ break;
162
205
  }
163
206
  }
164
207
  }
165
208
  // if the prompt is set, now parse out the values from the terminal
166
209
  if (this.#activeCommand.promptText != null) {
167
- let lineY = promptEndMarker.line;
168
- let line = this.#terminal.buffer.active.getLine(promptEndMarker.line);
210
+ let lineY = this.#activeCommand.promptEndMarker.line;
211
+ let line = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker.line);
169
212
  let command = "";
170
213
  let suggestions = "";
171
214
  for (;;) {
172
- for (let i = lineY == promptEndMarker.line ? this.#activeCommand.promptText.length : 0; i < this.#terminal.cols; i++) {
215
+ for (let i = lineY == this.#activeCommand.promptEndMarker.line ? this.#activeCommand.promptText.length : 0; i < this.#terminal.cols; i++) {
173
216
  const cell = line?.getCell(i);
174
217
  if (cell == null)
175
218
  continue;
@@ -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 pty from "node-pty";
9
- import { Shell, userZdotdir, zdotdir } from "../utils/shell.js";
8
+ import fs from "node:fs";
9
+ import pty from "@homebridge/node-pty-prebuilt-multiarch";
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";
@@ -30,13 +31,13 @@ export class ISTerm {
30
31
  #term;
31
32
  #commandManager;
32
33
  #shell;
33
- constructor({ shell, cols, rows, env, shellTarget, shellArgs }) {
34
+ constructor({ shell, cols, rows, env, shellTarget, shellArgs, underTest }) {
34
35
  this.#pty = pty.spawn(shellTarget, shellArgs ?? [], {
35
36
  name: "xterm-256color",
36
37
  cols,
37
38
  rows,
38
39
  cwd: process.cwd(),
39
- env: { ...convertToPtyEnv(shell), ...env },
40
+ env: { ...convertToPtyEnv(shell, underTest), ...env },
40
41
  });
41
42
  this.pid = this.#pty.pid;
42
43
  this.cols = this.#pty.cols;
@@ -62,6 +63,10 @@ export class ISTerm {
62
63
  };
63
64
  this.onExit = this.#pty.onExit;
64
65
  }
66
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
67
+ on(_event, _listener) {
68
+ throw new Error("Method not implemented as deprecated in node-pty.");
69
+ }
65
70
  _deserializeIsMessage(message) {
66
71
  return message.replaceAll(/\\(\\|x([0-9a-f]{2}))/gi, (_match, op, hex) => (hex ? String.fromCharCode(parseInt(hex, 16)) : op));
67
72
  }
@@ -214,29 +219,46 @@ export const spawn = async (options) => {
214
219
  };
215
220
  const convertToPtyTarget = async (shell) => {
216
221
  const platform = os.platform();
217
- 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;
218
223
  const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");
219
224
  let shellArgs = [];
220
225
  switch (shell) {
221
226
  case Shell.Bash:
222
227
  shellArgs = ["--init-file", path.join(shellFolderPath, "shellIntegration.bash")];
223
228
  break;
224
- case (Shell.Powershell, Shell.Pwsh):
229
+ case Shell.Powershell:
230
+ case Shell.Pwsh:
225
231
  shellArgs = ["-noexit", "-command", `try { . "${path.join(shellFolderPath, "shellIntegration.ps1")}" } catch {}`];
226
232
  break;
227
233
  case Shell.Fish:
228
234
  shellArgs = ["--init-command", `. ${path.join(shellFolderPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`];
229
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
+ }
230
248
  }
231
249
  return { shellTarget, shellArgs };
232
250
  };
233
- const convertToPtyEnv = (shell) => {
251
+ const convertToPtyEnv = (shell, underTest) => {
234
252
  const env = {
235
253
  ...process.env,
236
254
  ISTERM: "1",
255
+ ISTERM_TESTING: underTest ? "1" : undefined,
237
256
  };
238
257
  switch (shell) {
239
258
  case Shell.Cmd: {
259
+ if (underTest) {
260
+ return { ...env, PROMPT: `${IstermPromptStart}$G ${IstermPromptEnd}` };
261
+ }
240
262
  const prompt = process.env.PROMPT ? process.env.PROMPT : "$P$G";
241
263
  return { ...env, PROMPT: `${IstermPromptStart}${prompt}${IstermPromptEnd}` };
242
264
  }
@@ -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
  };
@@ -14,6 +14,9 @@ speclist.forEach((s) => {
14
14
  let activeSet = specSet;
15
15
  const specRoutes = s.split("/");
16
16
  specRoutes.forEach((route, idx) => {
17
+ if (typeof activeSet !== "object") {
18
+ return;
19
+ }
17
20
  if (idx === specRoutes.length - 1) {
18
21
  const prefix = versionedSpeclist.includes(s) ? "/index.js" : `.js`;
19
22
  activeSet[route] = `@withfig/autocomplete/build/${s}${prefix}`;
@@ -93,12 +96,12 @@ const getSubcommand = (spec) => {
93
96
  };
94
97
  const executeShellCommand = buildExecuteShellCommand(5000);
95
98
  const genSubcommand = async (command, parentCommand) => {
96
- const subcommandIdx = parentCommand.subcommands?.findIndex((s) => s.name === command);
97
- if (subcommandIdx == null)
99
+ if (!parentCommand.subcommands || parentCommand.subcommands.length === 0)
98
100
  return;
99
- const subcommand = parentCommand.subcommands?.at(subcommandIdx);
100
- if (subcommand == null)
101
+ const subcommandIdx = parentCommand.subcommands.findIndex((s) => (Array.isArray(s.name) ? s.name.includes(command) : s.name === command));
102
+ if (subcommandIdx === -1)
101
103
  return;
104
+ const subcommand = parentCommand.subcommands[subcommandIdx];
102
105
  // this pulls in the spec from the load spec and overwrites the subcommand in the parent with the loaded spec.
103
106
  // then it returns the subcommand and clears the loadSpec field so that it doesn't get called again
104
107
  switch (typeof subcommand.loadSpec) {
@@ -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 = () => {
@@ -4,7 +4,8 @@ import { getSuggestions } from "../runtime/runtime.js";
4
4
  import { renderBox, truncateText, truncateMultilineText } from "./utils.js";
5
5
  import ansi from "ansi-escapes";
6
6
  import chalk from "chalk";
7
- import { parseKeystroke } from "../utils/ansi.js";
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;
@@ -29,6 +30,7 @@ export class SuggestionManager {
29
30
  const commandText = this.#term.getCommandState().commandText;
30
31
  if (!commandText) {
31
32
  this.#suggestBlob = undefined;
33
+ this.#activeSuggestionIdx = 0;
32
34
  return;
33
35
  }
34
36
  if (commandText == this.#command) {
@@ -37,6 +39,7 @@ export class SuggestionManager {
37
39
  this.#command = commandText;
38
40
  const suggestionBlob = await getSuggestions(commandText, this.#term.cwd, this.#shell);
39
41
  this.#suggestBlob = suggestionBlob;
42
+ this.#activeSuggestionIdx = 0;
40
43
  }
41
44
  _renderArgumentDescription(description, x) {
42
45
  if (!description)
@@ -55,15 +58,20 @@ export class SuggestionManager {
55
58
  }
56
59
  _renderSuggestions(suggestions, activeSuggestionIdx, x) {
57
60
  return renderBox(suggestions.map((suggestion, idx) => {
58
- const suggestionText = `${suggestion.icon} ${suggestion.name}`.padEnd(suggestionWidth - borderWidth, " ");
61
+ const suggestionText = `${suggestion.icon} ${suggestion.name}`;
59
62
  const truncatedSuggestion = truncateText(suggestionText, suggestionWidth - 2);
60
63
  return idx == activeSuggestionIdx ? chalk.bgHex(activeSuggestionBackgroundColor)(truncatedSuggestion) : truncatedSuggestion;
61
64
  }), suggestionWidth, x);
62
65
  }
66
+ validate(suggestion) {
67
+ const commandText = this.#term.getCommandState().commandText;
68
+ return !commandText ? { data: "", rows: 0 } : suggestion;
69
+ }
63
70
  async render(remainingLines) {
64
71
  await this._loadSuggestions();
65
- if (!this.#suggestBlob)
72
+ if (!this.#suggestBlob) {
66
73
  return { data: "", rows: 0 };
74
+ }
67
75
  const { suggestions, argumentDescription } = this.#suggestBlob;
68
76
  const page = Math.min(Math.floor(this.#activeSuggestionIdx / maxSuggestions) + 1, Math.floor(suggestions.length / maxSuggestions) + 1);
69
77
  const pagedSuggestions = suggestions.filter((_, idx) => idx < page * maxSuggestions && idx >= (page - 1) * maxSuggestions);
@@ -112,20 +120,22 @@ export class SuggestionManager {
112
120
  rows,
113
121
  };
114
122
  }
115
- update(input) {
116
- const keyStroke = parseKeystroke(input);
117
- if (keyStroke == null)
123
+ update(keyPress) {
124
+ const { name, shift, ctrl } = keyPress;
125
+ if (!this.#suggestBlob) {
118
126
  return false;
119
- if (keyStroke == "esc") {
127
+ }
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) {
120
130
  this.#suggestBlob = undefined;
121
131
  }
122
- else if (keyStroke == "up") {
132
+ else if (name == prevKey && shift == !!prevShift && ctrl == !!prevCtrl) {
123
133
  this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
124
134
  }
125
- else if (keyStroke == "down") {
135
+ else if (name == nextKey && shift == !!nextShift && ctrl == !!nextCtrl) {
126
136
  this.#activeSuggestionIdx = Math.min(this.#activeSuggestionIdx + 1, (this.#suggestBlob?.suggestions.length ?? 1) - 1);
127
137
  }
128
- else if (keyStroke == "tab") {
138
+ else if (name == acceptKey && shift == !!acceptShift && ctrl == !!acceptCtrl) {
129
139
  const removals = "\u007F".repeat(this.#suggestBlob?.charactersToDrop ?? 0);
130
140
  const suggestion = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx);
131
141
  const chars = suggestion?.insertValue ?? suggestion?.name + " ";
@@ -134,6 +144,10 @@ export class SuggestionManager {
134
144
  }
135
145
  this.#term.write(removals + chars);
136
146
  }
137
- return "handled";
147
+ else {
148
+ return false;
149
+ }
150
+ log.debug({ msg: "handled keypress", ...keyPress });
151
+ return true;
138
152
  }
139
153
  }
@@ -1,9 +1,10 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import readline from "node:readline";
3
4
  import ansi from "ansi-escapes";
4
5
  import chalk from "chalk";
5
- import { inputModifier } from "./input.js";
6
6
  import log from "../utils/log.js";
7
+ import { Shell } from "../utils/shell.js";
7
8
  import isterm from "../isterm/index.js";
8
9
  import { eraseLinesBelow } from "../utils/ansi.js";
9
10
  import { SuggestionManager, MAX_LINES } from "./suggestionManager.js";
@@ -11,12 +12,14 @@ export const renderConfirmation = (live) => {
11
12
  const statusMessage = live ? chalk.green("live") : chalk.red("not found");
12
13
  return `inshellisense session [${statusMessage}]\n`;
13
14
  };
14
- export const render = async (shell) => {
15
- const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns });
15
+ export const render = async (shell, underTest) => {
16
+ const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns, underTest });
16
17
  const suggestionManager = new SuggestionManager(term, shell);
17
18
  let hasActiveSuggestions = false;
18
19
  let previousSuggestionsRows = 0;
19
- process.stdin.setRawMode(true);
20
+ if (process.stdin.isTTY)
21
+ process.stdin.setRawMode(true);
22
+ readline.emitKeypressEvents(process.stdin);
20
23
  const writeOutput = (data) => {
21
24
  log.debug({ msg: "writing data", data });
22
25
  process.stdout.write(data);
@@ -27,7 +30,7 @@ export const render = async (shell) => {
27
30
  // Considers when data includes newlines which have shifted the cursor position downwards
28
31
  const newlines = Math.max((data.match(/\r/g) || []).length, (data.match(/\n/g) || []).length);
29
32
  const linesOfInterest = MAX_LINES + newlines;
30
- if (term.getCursorState().remainingLines <= previousSuggestionsRows) {
33
+ if (term.getCursorState().remainingLines <= MAX_LINES) {
31
34
  // handles when suggestions get loaded before shell output so you need to always clear below output as a precaution
32
35
  if (term.getCursorState().remainingLines != 0) {
33
36
  writeOutput(ansi.cursorHide + ansi.cursorSavePosition + eraseLinesBelow(linesOfInterest + 1) + ansi.cursorRestorePosition);
@@ -47,12 +50,13 @@ export const render = async (shell) => {
47
50
  else {
48
51
  writeOutput(data);
49
52
  }
50
- setImmediate(async () => {
51
- const suggestion = await suggestionManager.render(term.getCursorState().remainingLines);
53
+ process.nextTick(async () => {
54
+ // validate result to prevent stale suggestion being provided
55
+ const suggestion = suggestionManager.validate(await suggestionManager.render(term.getCursorState().remainingLines));
52
56
  const commandState = term.getCommandState();
53
57
  if (suggestion.data != "" && commandState.cursorTerminated && !commandState.hasOutput) {
54
58
  if (hasActiveSuggestions) {
55
- if (term.getCursorState().remainingLines < suggestion.rows) {
59
+ if (term.getCursorState().remainingLines < MAX_LINES) {
56
60
  writeOutput(ansi.cursorHide +
57
61
  ansi.cursorSavePosition +
58
62
  ansi.cursorPrevLine.repeat(MAX_LINES) +
@@ -76,7 +80,7 @@ export const render = async (shell) => {
76
80
  }
77
81
  }
78
82
  else {
79
- if (term.getCursorState().remainingLines < suggestion.rows) {
83
+ if (term.getCursorState().remainingLines < MAX_LINES) {
80
84
  writeOutput(ansi.cursorHide + ansi.cursorSavePosition + ansi.cursorUp() + suggestion.data + ansi.cursorRestorePosition + ansi.cursorShow);
81
85
  }
82
86
  else {
@@ -92,7 +96,7 @@ export const render = async (shell) => {
92
96
  }
93
97
  else {
94
98
  if (hasActiveSuggestions) {
95
- if (term.getCursorState().remainingLines <= previousSuggestionsRows) {
99
+ if (term.getCursorState().remainingLines <= MAX_LINES) {
96
100
  writeOutput(ansi.cursorHide +
97
101
  ansi.cursorSavePosition +
98
102
  ansi.cursorPrevLine.repeat(MAX_LINES) +
@@ -109,13 +113,19 @@ export const render = async (shell) => {
109
113
  previousSuggestionsRows = suggestion.rows;
110
114
  });
111
115
  });
112
- process.stdin.on("data", (d) => {
113
- const suggestionResult = suggestionManager.update(d);
114
- if (previousSuggestionsRows > 0 && suggestionResult == "handled") {
116
+ process.stdin.on("keypress", (...keyPress) => {
117
+ const press = keyPress[1];
118
+ const inputHandled = suggestionManager.update(press);
119
+ if (previousSuggestionsRows > 0 && inputHandled) {
115
120
  term.noop();
116
121
  }
117
- else if (suggestionResult != "fully-handled") {
118
- term.write(inputModifier(d));
122
+ else if (!inputHandled) {
123
+ if (press.name == "backspace" && (shell === Shell.Pwsh || shell === Shell.Powershell || shell === Shell.Cmd)) {
124
+ term.write("\u007F");
125
+ }
126
+ else {
127
+ term.write(press.sequence);
128
+ }
119
129
  }
120
130
  });
121
131
  term.onExit(({ exitCode }) => {
@@ -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
  };
@@ -1,10 +1,9 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  const ESC = "\u001B";
4
- const CSI = "\u001B[";
4
+ const CSI = ESC + "[";
5
5
  const OSC = "\u001B]";
6
6
  const BEL = "\u0007";
7
- const SS3 = "\u001BO";
8
7
  export const IsTermOscPs = 6973;
9
8
  const IS_OSC = OSC + IsTermOscPs + ";";
10
9
  export var IstermOscPt;
@@ -32,25 +31,3 @@ export const scrollDown = (count = 1) => CSI + count + "T";
32
31
  export const eraseLinesBelow = (count = 1) => {
33
32
  return [...Array(count).keys()].map(() => cursorNextLine + eraseLine).join("");
34
33
  };
35
- export const parseKeystroke = (b) => {
36
- let s;
37
- if (b[0] > 127 && b[1] === undefined) {
38
- b[0] -= 128;
39
- s = "\u001B" + String(b);
40
- }
41
- else {
42
- s = String(b);
43
- }
44
- if (s == ESC) {
45
- return "esc";
46
- }
47
- else if (s == CSI + "A" || s == SS3 + "A") {
48
- return "up";
49
- }
50
- else if (s == CSI + "B" || s == SS3 + "B") {
51
- return "down";
52
- }
53
- else if (s == "\t") {
54
- return "tab";
55
- }
56
- };
@@ -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,8 +1,11 @@
1
1
  {
2
2
  "name": "@microsoft/inshellisense",
3
- "version": "0.0.1-rc.7",
3
+ "version": "0.0.1-rc.9",
4
4
  "description": "IDE style command line auto complete",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=16.6.0 <21.0.0"
8
+ },
6
9
  "bin": {
7
10
  "inshellisense": "./build/index.js",
8
11
  "is": "./build/index.js"
@@ -17,9 +20,11 @@
17
20
  "build": "tsc",
18
21
  "dev": "node --loader ts-node/esm src/index.ts -V",
19
22
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
23
+ "test:e2e": "tui-test",
20
24
  "lint": "eslint src/ --ext .ts,.tsx && prettier src/ --check",
21
25
  "lint:fix": "eslint src/ --ext .ts,.tsx --fix && prettier src/ --write",
22
- "debug": "node --inspect --loader ts-node/esm src/index.ts -V"
26
+ "debug": "node --inspect --loader ts-node/esm src/index.ts -V",
27
+ "pre-commit": "lint-staged"
23
28
  },
24
29
  "repository": {
25
30
  "type": "git",
@@ -34,22 +39,28 @@
34
39
  },
35
40
  "homepage": "https://github.com/microsoft/inshellisense#readme",
36
41
  "dependencies": {
37
- "@withfig/autocomplete": "2.648.2",
42
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.11.12",
43
+ "@withfig/autocomplete": "2.651.0",
38
44
  "ajv": "^8.12.0",
39
45
  "ansi-escapes": "^6.2.0",
40
46
  "ansi-styles": "^6.2.1",
41
47
  "chalk": "^5.3.0",
48
+ "color-convert": "^2.0.1",
42
49
  "commander": "^11.0.0",
43
50
  "find-process": "^1.4.7",
44
- "node-pty": "^1.0.0",
51
+ "toml": "^3.0.0",
52
+ "wcwidth": "^1.0.1",
45
53
  "which": "^4.0.0",
46
54
  "wrap-ansi": "^8.1.0",
47
55
  "xterm-headless": "^5.3.0"
48
56
  },
49
57
  "devDependencies": {
58
+ "@microsoft/tui-test": "^0.0.1-rc.3",
50
59
  "@tsconfig/node18": "^18.2.2",
60
+ "@types/color-convert": "^2.0.3",
51
61
  "@types/jest": "^29.5.5",
52
62
  "@types/react": "^18.2.24",
63
+ "@types/wcwidth": "^1.0.2",
53
64
  "@types/which": "^3.0.3",
54
65
  "@typescript-eslint/eslint-plugin": "^6.7.4",
55
66
  "@typescript-eslint/parser": "^6.7.4",
@@ -58,10 +69,18 @@
58
69
  "eslint-config-prettier": "^9.0.0",
59
70
  "eslint-plugin-header": "^3.1.1",
60
71
  "eslint-plugin-react": "^7.33.2",
72
+ "husky": "^9.0.11",
61
73
  "jest": "^29.7.0",
74
+ "lint-staged": "^15.2.2",
62
75
  "prettier": "3.0.3",
63
76
  "ts-jest": "^29.1.1",
64
77
  "ts-node": "^10.9.2",
65
78
  "typescript": "^5.2.2"
79
+ },
80
+ "lint-staged": {
81
+ "{,src/**/}*.{ts,tsx}": [
82
+ "eslint --fix",
83
+ "prettier --write"
84
+ ]
66
85
  }
67
86
  }
@@ -41,10 +41,16 @@ __is_update_cwd() {
41
41
 
42
42
  __is_update_prompt() {
43
43
  __is_prior_prompt="$PS1"
44
- PS1="%{$(__is_prompt_start)%}$PS1%{$(__is_prompt_end)%}"
44
+ if [[ $ISTERM_TESTING == "1" ]]; then
45
+ __is_prior_prompt="> "
46
+ fi
47
+ PS1="%{$(__is_prompt_start)%}$__is_prior_prompt%{$(__is_prompt_end)%}"
45
48
  }
46
49
 
47
50
  __is_precmd() {
51
+ if [[ $PS1 != *"$(__is_prompt_start)"* ]]; then
52
+ __is_update_prompt
53
+ fi
48
54
  __is_update_cwd
49
55
  }
50
56
 
@@ -57,6 +57,9 @@ fi
57
57
  __is_update_prompt() {
58
58
  if [[ "$__is_custom_PS1" == "" || "$__is_custom_PS1" != "$PS1" ]]; then
59
59
  __is_original_PS1=$PS1
60
+ if [ $ISTERM_TESTING == "1" ]; then
61
+ __is_original_PS1="> "
62
+ fi
60
63
  __is_custom_PS1="\[$(__is_prompt_start)\]$__is_original_PS1\[$(__is_prompt_end)\]"
61
64
  export PS1="$__is_custom_PS1"
62
65
  fi
@@ -11,4 +11,9 @@ end
11
11
  function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;$__is_cwd\a"; end
12
12
 
13
13
  __is_copy_function fish_prompt is_user_prompt
14
+
15
+ if [ "$ISTERM_TESTING" = "1" ]
16
+ function is_user_prompt; printf '> '; end
17
+ end
18
+
14
19
  function fish_prompt; printf (__is_prompt_start); printf (is_user_prompt); printf (__is_prompt_end); end
@@ -1,5 +1,12 @@
1
1
  $Global:__IsOriginalPrompt = $function:Prompt
2
2
 
3
+ function Global:__IsTestingPrompt() {
4
+ return "PS > "
5
+ }
6
+ if ($env:ISTERM_TESTING -eq "1") {
7
+ $Global:__IsOriginalPrompt = $function:__IsTestingPrompt
8
+ }
9
+
3
10
  function Global:__IS-Escape-Value([string]$value) {
4
11
  [regex]::Replace($value, '[\\\n;]', { param($match)
5
12
  -Join (
@@ -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}"
package/build/ui/input.js DELETED
@@ -1,9 +0,0 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT License.
3
- export const inputModifier = (input) => {
4
- switch (input.toString()) {
5
- case "\b":
6
- return "\u007F"; // DEL
7
- }
8
- return input.toString();
9
- };