@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 +42 -3
- package/build/commands/complete.js +1 -0
- package/build/commands/root.js +1 -1
- package/build/index.js +7 -1
- package/build/isterm/commandManager.js +66 -23
- package/build/isterm/pty.js +29 -7
- package/build/runtime/generator.js +7 -5
- package/build/runtime/runtime.js +7 -4
- package/build/runtime/suggestion.js +13 -7
- package/build/runtime/template.js +2 -2
- package/build/ui/suggestionManager.js +25 -11
- package/build/ui/ui-root.js +25 -15
- package/build/ui/ui-uninstall.js +3 -3
- package/build/ui/utils.js +3 -1
- package/build/utils/ansi.js +1 -24
- package/build/utils/config.js +76 -37
- package/build/utils/shell.js +5 -0
- package/package.json +23 -4
- package/shell/shellIntegration-rc.zsh +7 -1
- package/shell/shellIntegration.bash +3 -0
- package/shell/shellIntegration.fish +5 -0
- package/shell/shellIntegration.ps1 +7 -0
- package/shell/shellIntegration.xsh +29 -0
- package/build/ui/input.js +0 -9
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
### Requirements
|
|
10
10
|
|
|
11
|
-
-
|
|
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`);
|
package/build/commands/root.js
CHANGED
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
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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.
|
|
76
|
-
if (inshellisenseConfig
|
|
77
|
-
const
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
const adjustedPrompt = this._adjustPrompt(
|
|
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
|
|
122
|
-
const
|
|
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
|
|
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;
|
package/build/isterm/pty.js
CHANGED
|
@@ -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
|
|
9
|
-
import
|
|
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
|
-
|
|
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
|
|
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(
|
|
30
|
+
suggestions.push(...postProcess(scriptStdout, tokens));
|
|
30
31
|
}
|
|
31
32
|
else if (splitOn) {
|
|
32
|
-
suggestions.push(...
|
|
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
|
-
|
|
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
|
};
|
package/build/runtime/runtime.js
CHANGED
|
@@ -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
|
-
|
|
97
|
-
if (subcommandIdx == null)
|
|
99
|
+
if (!parentCommand.subcommands || parentCommand.subcommands.length === 0)
|
|
98
100
|
return;
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
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:
|
|
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:
|
|
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
|
|
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}
|
|
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(
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
123
|
+
update(keyPress) {
|
|
124
|
+
const { name, shift, ctrl } = keyPress;
|
|
125
|
+
if (!this.#suggestBlob) {
|
|
118
126
|
return false;
|
|
119
|
-
|
|
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 (
|
|
132
|
+
else if (name == prevKey && shift == !!prevShift && ctrl == !!prevCtrl) {
|
|
123
133
|
this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
|
|
124
134
|
}
|
|
125
|
-
else if (
|
|
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 (
|
|
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
|
-
|
|
147
|
+
else {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
log.debug({ msg: "handled keypress", ...keyPress });
|
|
151
|
+
return true;
|
|
138
152
|
}
|
|
139
153
|
}
|
package/build/ui/ui-root.js
CHANGED
|
@@ -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.
|
|
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 <=
|
|
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
|
-
|
|
51
|
-
|
|
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 <
|
|
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 <
|
|
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 <=
|
|
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("
|
|
113
|
-
const
|
|
114
|
-
|
|
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 (
|
|
118
|
-
|
|
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 }) => {
|
package/build/ui/ui-uninstall.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { deleteCacheFolder } from "../utils/config.js";
|
|
5
5
|
export const render = async () => {
|
|
6
|
-
|
|
7
|
-
process.stdout.write(chalk.green("✓") + " successfully deleted the .inshellisense
|
|
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
|
|
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
|
};
|
package/build/utils/ansi.js
CHANGED
|
@@ -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 = "
|
|
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
|
-
};
|
package/build/utils/config.js
CHANGED
|
@@ -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
|
-
|
|
37
|
+
bindings: {
|
|
14
38
|
type: "object",
|
|
39
|
+
nullable: true,
|
|
15
40
|
properties: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
50
|
-
const cachePath = path.join(os.homedir(),
|
|
51
|
-
|
|
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(
|
|
55
|
-
|
|
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(
|
|
84
|
+
program.error(`${configFile} is invalid: ${ajv.errorsText()}`);
|
|
59
85
|
}
|
|
60
|
-
globalConfig =
|
|
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
|
|
64
|
-
const cliConfigPath = path.join(os.homedir(),
|
|
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
|
}
|
package/build/utils/shell.js
CHANGED
|
@@ -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.
|
|
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
|
-
"@
|
|
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
|
-
"
|
|
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
|
-
|
|
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}"
|