@microsoft/inshellisense 0.0.1-rc.2 → 0.0.1-rc.20
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/LICENSE +21 -21
- package/README.md +80 -6
- package/SECURITY.md +41 -41
- package/build/commands/complete.js +16 -0
- package/build/commands/doctor.js +11 -0
- package/build/commands/init.js +24 -0
- package/build/commands/root.js +27 -30
- package/build/commands/specs/list.js +26 -0
- package/build/commands/specs/root.js +8 -0
- package/build/commands/uninstall.js +1 -1
- package/build/index.js +20 -7
- package/build/isterm/commandManager.js +290 -0
- package/build/isterm/index.js +4 -0
- package/build/isterm/pty.js +372 -0
- package/build/runtime/alias.js +61 -0
- package/build/runtime/generator.js +24 -11
- package/build/runtime/parser.js +86 -16
- package/build/runtime/runtime.js +103 -45
- package/build/runtime/suggestion.js +70 -22
- package/build/runtime/template.js +33 -18
- package/build/runtime/utils.js +111 -12
- package/build/ui/suggestionManager.js +162 -0
- package/build/ui/ui-doctor.js +69 -0
- package/build/ui/ui-root.js +130 -64
- package/build/ui/ui-uninstall.js +3 -5
- package/build/ui/utils.js +57 -0
- package/build/utils/ansi.js +37 -0
- package/build/utils/config.js +132 -0
- package/build/utils/log.js +39 -0
- package/build/utils/shell.js +316 -0
- package/package.json +39 -6
- package/scripts/postinstall.js +9 -0
- package/shell/bash-preexec.sh +380 -0
- package/shell/shellIntegration-env.zsh +12 -0
- package/shell/shellIntegration-login.zsh +9 -0
- package/shell/shellIntegration-profile.zsh +9 -0
- package/shell/shellIntegration-rc.zsh +66 -0
- package/shell/shellIntegration.bash +125 -0
- package/shell/shellIntegration.fish +28 -0
- package/shell/shellIntegration.nu +36 -0
- package/shell/shellIntegration.ps1 +27 -0
- package/shell/shellIntegration.xsh +37 -0
- package/build/commands/bind.js +0 -12
- package/build/ui/input.js +0 -55
- package/build/ui/suggestions.js +0 -84
- package/build/ui/ui-bind.js +0 -69
- package/build/utils/bindings.js +0 -216
- package/build/utils/cache.js +0 -21
- package/shell/key-bindings-powershell.ps1 +0 -27
- package/shell/key-bindings-pwsh.ps1 +0 -27
- package/shell/key-bindings.bash +0 -7
- package/shell/key-bindings.fish +0 -8
- package/shell/key-bindings.zsh +0 -10
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { runGenerator } from "./generator.js";
|
|
4
5
|
import { runTemplates } from "./template.js";
|
|
5
|
-
|
|
6
|
+
import log from "../utils/log.js";
|
|
7
|
+
import { escapePath } from "./utils.js";
|
|
8
|
+
import { addPathSeparator, getPathDirname, removePathSeparator } from "../utils/shell.js";
|
|
9
|
+
export var SuggestionIcons;
|
|
6
10
|
(function (SuggestionIcons) {
|
|
7
11
|
SuggestionIcons["File"] = "\uD83D\uDCC4";
|
|
8
12
|
SuggestionIcons["Folder"] = "\uD83D\uDCC1";
|
|
@@ -14,7 +18,11 @@ var SuggestionIcons;
|
|
|
14
18
|
SuggestionIcons["Special"] = "\u2B50";
|
|
15
19
|
SuggestionIcons["Default"] = "\uD83D\uDCC0";
|
|
16
20
|
})(SuggestionIcons || (SuggestionIcons = {}));
|
|
17
|
-
const getIcon = (suggestionType) => {
|
|
21
|
+
const getIcon = (icon, suggestionType) => {
|
|
22
|
+
// eslint-disable-next-line no-control-regex
|
|
23
|
+
if (icon && /[^\u0000-\u00ff]/.test(icon)) {
|
|
24
|
+
return icon;
|
|
25
|
+
}
|
|
18
26
|
switch (suggestionType) {
|
|
19
27
|
case "arg":
|
|
20
28
|
return SuggestionIcons.Argument;
|
|
@@ -38,16 +46,20 @@ const getIcon = (suggestionType) => {
|
|
|
38
46
|
const getLong = (suggestion) => {
|
|
39
47
|
return suggestion instanceof Array ? suggestion.reduce((p, c) => (p.length > c.length ? p : c)) : suggestion;
|
|
40
48
|
};
|
|
49
|
+
const getPathy = (type) => {
|
|
50
|
+
return type === "file" || type === "folder";
|
|
51
|
+
};
|
|
41
52
|
const toSuggestion = (suggestion, name, type) => {
|
|
42
53
|
if (suggestion.name == null)
|
|
43
54
|
return;
|
|
44
55
|
return {
|
|
45
56
|
name: name ?? getLong(suggestion.name),
|
|
46
57
|
description: suggestion.description,
|
|
47
|
-
icon: getIcon(type ?? suggestion.type),
|
|
58
|
+
icon: getIcon(suggestion.icon, type ?? suggestion.type),
|
|
48
59
|
allNames: suggestion.name instanceof Array ? suggestion.name : [suggestion.name],
|
|
49
60
|
priority: suggestion.priority ?? 50,
|
|
50
61
|
insertValue: suggestion.insertValue,
|
|
62
|
+
type: suggestion.type,
|
|
51
63
|
};
|
|
52
64
|
};
|
|
53
65
|
function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
@@ -65,10 +77,11 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
65
77
|
? {
|
|
66
78
|
name: matchedName,
|
|
67
79
|
description: s.description,
|
|
68
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
80
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
69
81
|
allNames: s.name,
|
|
70
82
|
priority: s.priority ?? 50,
|
|
71
83
|
insertValue: s.insertValue,
|
|
84
|
+
type: s.type,
|
|
72
85
|
}
|
|
73
86
|
: undefined;
|
|
74
87
|
}
|
|
@@ -76,10 +89,11 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
76
89
|
? {
|
|
77
90
|
name: s.name,
|
|
78
91
|
description: s.description,
|
|
79
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
92
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
80
93
|
allNames: [s.name],
|
|
81
94
|
priority: s.priority ?? 50,
|
|
82
95
|
insertValue: s.insertValue,
|
|
96
|
+
type: s.type,
|
|
83
97
|
}
|
|
84
98
|
: undefined;
|
|
85
99
|
})
|
|
@@ -95,10 +109,11 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
95
109
|
? {
|
|
96
110
|
name: matchedName,
|
|
97
111
|
description: s.description,
|
|
98
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
112
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
99
113
|
allNames: s.name,
|
|
100
114
|
insertValue: s.insertValue,
|
|
101
115
|
priority: s.priority ?? 50,
|
|
116
|
+
type: s.type,
|
|
102
117
|
}
|
|
103
118
|
: undefined;
|
|
104
119
|
}
|
|
@@ -106,24 +121,27 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
106
121
|
? {
|
|
107
122
|
name: s.name,
|
|
108
123
|
description: s.description,
|
|
109
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
124
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
110
125
|
allNames: [s.name],
|
|
111
126
|
insertValue: s.insertValue,
|
|
112
127
|
priority: s.priority ?? 50,
|
|
128
|
+
type: s.type,
|
|
113
129
|
}
|
|
114
130
|
: undefined;
|
|
115
131
|
})
|
|
116
132
|
.filter((s) => s != null);
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
|
-
const generatorSuggestions = async (generator, acceptedTokens, filterStrategy, partialCmd) => {
|
|
135
|
+
const generatorSuggestions = async (generator, acceptedTokens, filterStrategy, partialCmd, cwd) => {
|
|
120
136
|
const generators = generator instanceof Array ? generator : generator ? [generator] : [];
|
|
121
137
|
const tokens = acceptedTokens.map((t) => t.token);
|
|
122
|
-
|
|
123
|
-
|
|
138
|
+
if (partialCmd)
|
|
139
|
+
tokens.push(partialCmd);
|
|
140
|
+
const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat();
|
|
141
|
+
return filter(suggestions.map((suggestion) => ({ ...suggestion, priority: suggestion.priority ?? 60 })), filterStrategy, partialCmd, undefined);
|
|
124
142
|
};
|
|
125
|
-
const templateSuggestions = async (templates, filterStrategy, partialCmd) => {
|
|
126
|
-
return filter(await runTemplates(templates ?? []), filterStrategy, partialCmd, undefined);
|
|
143
|
+
const templateSuggestions = async (templates, filterStrategy, partialCmd, cwd) => {
|
|
144
|
+
return filter(await runTemplates(templates ?? [], cwd), filterStrategy, partialCmd, undefined);
|
|
127
145
|
};
|
|
128
146
|
const suggestionSuggestions = (suggestions, filterStrategy, partialCmd) => {
|
|
129
147
|
const cleanedSuggestions = suggestions?.map((s) => (typeof s === "string" ? { name: s } : s)) ?? [];
|
|
@@ -137,17 +155,43 @@ const optionSuggestions = (options, acceptedTokens, filterStrategy, partialCmd)
|
|
|
137
155
|
const validOptions = options?.filter((o) => o.exclusiveOn?.every((exclusiveOption) => !usedOptions.has(exclusiveOption)) ?? true);
|
|
138
156
|
return filter(validOptions ?? [], filterStrategy, partialCmd, "option");
|
|
139
157
|
};
|
|
140
|
-
|
|
158
|
+
function adjustPathSuggestions(suggestions, partialToken, shell) {
|
|
159
|
+
return suggestions.map((s) => {
|
|
160
|
+
const pathy = getPathy(s.type);
|
|
161
|
+
const rawInsertValue = removePathSeparator(s.insertValue ?? s.name ?? "");
|
|
162
|
+
const insertValue = s.type == "folder" ? addPathSeparator(rawInsertValue, shell) : rawInsertValue;
|
|
163
|
+
const partialDir = getPathDirname(partialToken?.token ?? "", shell);
|
|
164
|
+
const fullPath = partialToken?.isPath ? `${partialDir}${insertValue}` : insertValue;
|
|
165
|
+
return pathy ? { ...s, insertValue: escapePath(fullPath, shell), name: removePathSeparator(s.name) } : s;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const removeAcceptedSuggestions = (suggestions, acceptedTokens) => {
|
|
141
169
|
const seen = new Set(acceptedTokens.map((t) => t.token));
|
|
142
170
|
return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n)));
|
|
143
171
|
};
|
|
172
|
+
const removeDuplicateSuggestion = (suggestions) => {
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
return suggestions
|
|
175
|
+
.map((s) => {
|
|
176
|
+
if (seen.has(s.name))
|
|
177
|
+
return null;
|
|
178
|
+
seen.add(s.name);
|
|
179
|
+
return s;
|
|
180
|
+
})
|
|
181
|
+
.filter((s) => s != null);
|
|
182
|
+
};
|
|
144
183
|
const removeEmptySuggestion = (suggestions) => {
|
|
145
184
|
return suggestions.filter((s) => s.name.length > 0);
|
|
146
185
|
};
|
|
147
|
-
export const getSubcommandDrivenRecommendation = async (subcommand, persistentOptions,
|
|
186
|
+
export const getSubcommandDrivenRecommendation = async (subcommand, persistentOptions, partialToken, argsDepleted, argsFromSubcommand, acceptedTokens, cwd, shell) => {
|
|
187
|
+
log.debug({ msg: "suggestion point", subcommand, persistentOptions, partialToken, argsDepleted, argsFromSubcommand, acceptedTokens, cwd });
|
|
148
188
|
if (argsDepleted && argsFromSubcommand) {
|
|
149
189
|
return;
|
|
150
190
|
}
|
|
191
|
+
let partialCmd = partialToken?.token;
|
|
192
|
+
if (partialToken?.isPath) {
|
|
193
|
+
partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? "");
|
|
194
|
+
}
|
|
151
195
|
const suggestions = [];
|
|
152
196
|
const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0;
|
|
153
197
|
const allOptions = persistentOptions.concat(subcommand.options ?? []);
|
|
@@ -157,28 +201,32 @@ export const getSubcommandDrivenRecommendation = async (subcommand, persistentOp
|
|
|
157
201
|
}
|
|
158
202
|
if (argLength != 0) {
|
|
159
203
|
const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args;
|
|
160
|
-
suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)));
|
|
204
|
+
suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd, cwd)));
|
|
161
205
|
suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd));
|
|
162
|
-
suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd)));
|
|
206
|
+
suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd, cwd)));
|
|
163
207
|
}
|
|
164
208
|
return {
|
|
165
|
-
suggestions: removeEmptySuggestion(
|
|
209
|
+
suggestions: removeDuplicateSuggestion(removeEmptySuggestion(removeAcceptedSuggestions(adjustPathSuggestions(suggestions.sort((a, b) => b.priority - a.priority), partialToken, shell), acceptedTokens))),
|
|
166
210
|
};
|
|
167
211
|
};
|
|
168
|
-
export const getArgDrivenRecommendation = async (args, subcommand, persistentOptions,
|
|
212
|
+
export const getArgDrivenRecommendation = async (args, subcommand, persistentOptions, partialToken, acceptedTokens, variadicArgBound, cwd, shell) => {
|
|
213
|
+
let partialCmd = partialToken?.token;
|
|
214
|
+
if (partialToken?.isPath) {
|
|
215
|
+
partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? "");
|
|
216
|
+
}
|
|
169
217
|
const activeArg = args[0];
|
|
170
218
|
const allOptions = persistentOptions.concat(subcommand.options ?? []);
|
|
171
219
|
const suggestions = [
|
|
172
|
-
...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)),
|
|
220
|
+
...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd, cwd)),
|
|
173
221
|
...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd),
|
|
174
|
-
...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd)),
|
|
222
|
+
...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd, cwd)),
|
|
175
223
|
];
|
|
176
|
-
if (
|
|
224
|
+
if (activeArg.isOptional || (activeArg.isVariadic && variadicArgBound)) {
|
|
177
225
|
suggestions.push(...subcommandSuggestions(subcommand.subcommands, activeArg?.filterStrategy, partialCmd));
|
|
178
226
|
suggestions.push(...optionSuggestions(allOptions, acceptedTokens, activeArg?.filterStrategy, partialCmd));
|
|
179
227
|
}
|
|
180
228
|
return {
|
|
181
|
-
suggestions: removeEmptySuggestion(
|
|
229
|
+
suggestions: removeDuplicateSuggestion(removeEmptySuggestion(removeAcceptedSuggestions(adjustPathSuggestions(suggestions.sort((a, b) => b.priority - a.priority), partialToken, shell), acceptedTokens))),
|
|
182
230
|
argumentDescription: activeArg.description ?? activeArg.name,
|
|
183
231
|
};
|
|
184
232
|
};
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import fsAsync from "fs/promises";
|
|
4
|
-
import
|
|
5
|
-
const filepathsTemplate = async () => {
|
|
6
|
-
const files = await fsAsync.readdir(
|
|
7
|
-
return files
|
|
3
|
+
import fsAsync from "node:fs/promises";
|
|
4
|
+
import log from "../utils/log.js";
|
|
5
|
+
const filepathsTemplate = async (cwd) => {
|
|
6
|
+
const files = await fsAsync.readdir(cwd, { withFileTypes: true });
|
|
7
|
+
return files
|
|
8
|
+
.filter((f) => f.isFile() || f.isDirectory())
|
|
9
|
+
.map((f) => ({ name: f.name, priority: 55, context: { templateType: "filepaths" }, type: f.isDirectory() ? "folder" : "file" }));
|
|
8
10
|
};
|
|
9
|
-
const foldersTemplate = async () => {
|
|
10
|
-
const files = await fsAsync.readdir(
|
|
11
|
-
return files
|
|
11
|
+
const foldersTemplate = async (cwd) => {
|
|
12
|
+
const files = await fsAsync.readdir(cwd, { withFileTypes: true });
|
|
13
|
+
return files
|
|
14
|
+
.filter((f) => f.isDirectory())
|
|
15
|
+
.map((f) => ({
|
|
16
|
+
name: f.name,
|
|
17
|
+
priority: 55,
|
|
18
|
+
context: { templateType: "folders" },
|
|
19
|
+
type: "folder",
|
|
20
|
+
}));
|
|
12
21
|
};
|
|
13
22
|
// TODO: implement history template
|
|
14
23
|
const historyTemplate = () => {
|
|
@@ -18,18 +27,24 @@ const historyTemplate = () => {
|
|
|
18
27
|
const helpTemplate = () => {
|
|
19
28
|
return [];
|
|
20
29
|
};
|
|
21
|
-
export const runTemplates = async (template) => {
|
|
30
|
+
export const runTemplates = async (template, cwd) => {
|
|
22
31
|
const templates = template instanceof Array ? template : [template];
|
|
23
32
|
return (await Promise.all(templates.map(async (t) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
try {
|
|
34
|
+
switch (t) {
|
|
35
|
+
case "filepaths":
|
|
36
|
+
return await filepathsTemplate(cwd);
|
|
37
|
+
case "folders":
|
|
38
|
+
return await foldersTemplate(cwd);
|
|
39
|
+
case "history":
|
|
40
|
+
return historyTemplate();
|
|
41
|
+
case "help":
|
|
42
|
+
return helpTemplate();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
log.debug({ msg: "template failed", e, template: t, cwd });
|
|
47
|
+
return [];
|
|
33
48
|
}
|
|
34
49
|
}))).flat();
|
|
35
50
|
};
|
package/build/runtime/utils.js
CHANGED
|
@@ -1,22 +1,121 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import fsAsync from "node:fs/promises";
|
|
6
|
+
import { getPathSeparator, gitBashPath, Shell } from "../utils/shell.js";
|
|
7
|
+
import log from "../utils/log.js";
|
|
8
|
+
const getExecutionShell = async () => {
|
|
9
|
+
if (process.platform !== "win32")
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
return await gitBashPath();
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
log.debug({ msg: "failed to load posix shell for windows child_process.spawn, some generators might fail", error: e });
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const bashSpecialCharacters = /[&|<>\s]/g;
|
|
19
|
+
// escape whitespace & special characters in an argument when not quoted
|
|
20
|
+
const shouldEscapeArg = (arg) => {
|
|
21
|
+
const hasSpecialCharacter = bashSpecialCharacters.test(arg);
|
|
22
|
+
const isSingleCharacter = arg.length === 1;
|
|
23
|
+
return hasSpecialCharacter && !isSingleCharacter && !isQuoted(arg, `"`);
|
|
24
|
+
};
|
|
25
|
+
/* based on libuv process.c used by nodejs, only quotes are escaped for shells. if using git bash need to escape whitespace & special characters in an argument */
|
|
26
|
+
const escapeArgs = (shell, args) => {
|
|
27
|
+
// only escape args for git bash
|
|
28
|
+
if (process.platform !== "win32" || shell == undefined)
|
|
29
|
+
return args;
|
|
30
|
+
return args.map((arg) => (shouldEscapeArg(arg) ? `"${arg.replaceAll('"', '\\"')}"` : arg));
|
|
12
31
|
};
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
const isQuoted = (value, quoteChar) => (value?.startsWith(quoteChar) && value?.endsWith(quoteChar)) ?? false;
|
|
33
|
+
const quoteString = (value, quoteChar) => {
|
|
34
|
+
if (isQuoted(value, quoteChar))
|
|
35
|
+
return value;
|
|
36
|
+
const escapedValue = value.replaceAll(`\\${quoteChar}`, quoteChar).replaceAll(quoteChar, `\\${quoteChar}`);
|
|
37
|
+
return `${quoteChar}${escapedValue}${quoteChar}`;
|
|
38
|
+
};
|
|
39
|
+
const needsQuoted = (value, quoteChar) => isQuoted(value, quoteChar) || value.includes(" ");
|
|
40
|
+
const getShellQuoteChar = (shell) => {
|
|
41
|
+
switch (shell) {
|
|
42
|
+
case Shell.Zsh:
|
|
43
|
+
case Shell.Bash:
|
|
44
|
+
case Shell.Fish:
|
|
45
|
+
return `"`;
|
|
46
|
+
case Shell.Xonsh:
|
|
47
|
+
return `'`;
|
|
48
|
+
case Shell.Nushell:
|
|
49
|
+
return "`";
|
|
50
|
+
case Shell.Pwsh:
|
|
51
|
+
case Shell.Powershell:
|
|
52
|
+
return `'`;
|
|
53
|
+
case Shell.Cmd:
|
|
54
|
+
return `"`;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const getShellWhitespaceEscapeChar = (shell) => {
|
|
58
|
+
switch (shell) {
|
|
59
|
+
case Shell.Zsh:
|
|
60
|
+
case Shell.Bash:
|
|
61
|
+
case Shell.Fish:
|
|
62
|
+
case Shell.Xonsh:
|
|
63
|
+
case Shell.Nushell:
|
|
64
|
+
return "\\";
|
|
65
|
+
case Shell.Pwsh:
|
|
66
|
+
case Shell.Powershell:
|
|
67
|
+
return "`";
|
|
68
|
+
case Shell.Cmd:
|
|
69
|
+
return "^";
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
export const escapePath = (value, shell) => value != null && needsQuoted(value, getShellQuoteChar(shell)) ? quoteString(value, getShellQuoteChar(shell)) : value;
|
|
73
|
+
export const buildExecuteShellCommand = async (timeout) => async ({ command, env, args, cwd }) => {
|
|
74
|
+
const executionShell = await getExecutionShell();
|
|
75
|
+
const escapedArgs = escapeArgs(executionShell, args);
|
|
76
|
+
const child = spawn(command, escapedArgs, { cwd, env: { ...process.env, ...env, ISTERM: "1" }, shell: executionShell });
|
|
77
|
+
setTimeout(() => child.kill("SIGKILL"), timeout);
|
|
78
|
+
let stdout = "";
|
|
79
|
+
let stderr = "";
|
|
80
|
+
child.stdout.on("data", (data) => (stdout += data));
|
|
81
|
+
child.stderr.on("data", (data) => (stderr += data));
|
|
82
|
+
child.on("error", (err) => {
|
|
83
|
+
log.debug({ msg: "shell command failed", command, args, e: err.message });
|
|
84
|
+
});
|
|
15
85
|
return new Promise((resolve) => {
|
|
16
86
|
child.on("close", (code) => {
|
|
17
87
|
resolve({
|
|
18
|
-
code,
|
|
88
|
+
status: code ?? 0,
|
|
89
|
+
stderr,
|
|
90
|
+
stdout,
|
|
19
91
|
});
|
|
20
92
|
});
|
|
21
93
|
});
|
|
22
94
|
};
|
|
95
|
+
export const resolveCwd = async (cmdToken, cwd, shell) => {
|
|
96
|
+
if (cmdToken == null)
|
|
97
|
+
return { cwd, pathy: false, complete: false };
|
|
98
|
+
const { token: rawToken, isQuoted } = cmdToken;
|
|
99
|
+
const escapedToken = !isQuoted ? rawToken.replaceAll(" ", "\\ ") : rawToken;
|
|
100
|
+
const token = escapedToken;
|
|
101
|
+
const sep = getPathSeparator(shell);
|
|
102
|
+
if (!token.includes(sep))
|
|
103
|
+
return { cwd, pathy: false, complete: false };
|
|
104
|
+
const resolvedCwd = path.isAbsolute(token) ? token : path.join(cwd, token);
|
|
105
|
+
try {
|
|
106
|
+
await fsAsync.access(resolvedCwd, fsAsync.constants.R_OK);
|
|
107
|
+
return { cwd: resolvedCwd, pathy: true, complete: token.endsWith(sep) };
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// fallback to the parent folder if possible
|
|
111
|
+
const baselessCwd = resolvedCwd.substring(0, resolvedCwd.length - path.basename(resolvedCwd).length);
|
|
112
|
+
try {
|
|
113
|
+
await fsAsync.access(baselessCwd, fsAsync.constants.R_OK);
|
|
114
|
+
return { cwd: baselessCwd, pathy: true, complete: token.endsWith(sep) };
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/*empty*/
|
|
118
|
+
}
|
|
119
|
+
return { cwd, pathy: false, complete: false };
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import { getSuggestions } from "../runtime/runtime.js";
|
|
4
|
+
import { renderBox, truncateText, truncateMultilineText } from "./utils.js";
|
|
5
|
+
import ansi from "ansi-escapes";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import log from "../utils/log.js";
|
|
8
|
+
import { getConfig } from "../utils/config.js";
|
|
9
|
+
const maxSuggestions = 5;
|
|
10
|
+
const suggestionWidth = 40;
|
|
11
|
+
const descriptionWidth = 30;
|
|
12
|
+
const descriptionHeight = 5;
|
|
13
|
+
const borderWidth = 2;
|
|
14
|
+
const activeSuggestionBackgroundColor = "#7D56F4";
|
|
15
|
+
export const MAX_LINES = borderWidth + Math.max(maxSuggestions, descriptionHeight);
|
|
16
|
+
export class SuggestionManager {
|
|
17
|
+
#term;
|
|
18
|
+
#command;
|
|
19
|
+
#activeSuggestionIdx;
|
|
20
|
+
#suggestBlob;
|
|
21
|
+
#shell;
|
|
22
|
+
#hideSuggestions = false;
|
|
23
|
+
constructor(terminal, shell) {
|
|
24
|
+
this.#term = terminal;
|
|
25
|
+
this.#suggestBlob = { suggestions: [] };
|
|
26
|
+
this.#command = "";
|
|
27
|
+
this.#activeSuggestionIdx = 0;
|
|
28
|
+
this.#shell = shell;
|
|
29
|
+
}
|
|
30
|
+
async _loadSuggestions() {
|
|
31
|
+
const commandText = this.#term.getCommandState().commandText;
|
|
32
|
+
if (!commandText || this.#hideSuggestions) {
|
|
33
|
+
this.#suggestBlob = undefined;
|
|
34
|
+
this.#activeSuggestionIdx = 0;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (commandText == this.#command) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.#command = commandText;
|
|
41
|
+
const suggestionBlob = await getSuggestions(commandText, this.#term.cwd, this.#shell);
|
|
42
|
+
this.#suggestBlob = suggestionBlob;
|
|
43
|
+
this.#activeSuggestionIdx = 0;
|
|
44
|
+
}
|
|
45
|
+
_renderArgumentDescription(description, x) {
|
|
46
|
+
if (!description)
|
|
47
|
+
return "";
|
|
48
|
+
return renderBox([truncateText(description, descriptionWidth - borderWidth)], descriptionWidth, x);
|
|
49
|
+
}
|
|
50
|
+
_renderDescription(description, x) {
|
|
51
|
+
if (!description)
|
|
52
|
+
return "";
|
|
53
|
+
return renderBox(truncateMultilineText(description, descriptionWidth - borderWidth, descriptionHeight), descriptionWidth, x);
|
|
54
|
+
}
|
|
55
|
+
_descriptionRows(description) {
|
|
56
|
+
if (!description)
|
|
57
|
+
return 0;
|
|
58
|
+
return truncateMultilineText(description, descriptionWidth - borderWidth, descriptionHeight).length;
|
|
59
|
+
}
|
|
60
|
+
_renderSuggestions(suggestions, activeSuggestionIdx, x) {
|
|
61
|
+
return renderBox(suggestions.map((suggestion, idx) => {
|
|
62
|
+
const suggestionText = `${suggestion.icon} ${suggestion.name}`;
|
|
63
|
+
const truncatedSuggestion = truncateText(suggestionText, suggestionWidth - 2);
|
|
64
|
+
return idx == activeSuggestionIdx ? chalk.bgHex(activeSuggestionBackgroundColor)(truncatedSuggestion) : truncatedSuggestion;
|
|
65
|
+
}), suggestionWidth, x);
|
|
66
|
+
}
|
|
67
|
+
validate(suggestion) {
|
|
68
|
+
const commandText = this.#term.getCommandState().commandText;
|
|
69
|
+
return !commandText ? { data: "", rows: 0 } : suggestion;
|
|
70
|
+
}
|
|
71
|
+
async render(remainingLines) {
|
|
72
|
+
await this._loadSuggestions();
|
|
73
|
+
if (!this.#suggestBlob) {
|
|
74
|
+
return { data: "", rows: 0 };
|
|
75
|
+
}
|
|
76
|
+
const { suggestions, argumentDescription } = this.#suggestBlob;
|
|
77
|
+
const page = Math.min(Math.floor(this.#activeSuggestionIdx / maxSuggestions) + 1, Math.floor(suggestions.length / maxSuggestions) + 1);
|
|
78
|
+
const pagedSuggestions = suggestions.filter((_, idx) => idx < page * maxSuggestions && idx >= (page - 1) * maxSuggestions);
|
|
79
|
+
const activePagedSuggestionIndex = this.#activeSuggestionIdx % maxSuggestions;
|
|
80
|
+
const activeDescription = pagedSuggestions.at(activePagedSuggestionIndex)?.description || argumentDescription || "";
|
|
81
|
+
const wrappedPadding = this.#term.getCursorState().cursorX % this.#term.cols;
|
|
82
|
+
const maxPadding = activeDescription.length !== 0 ? this.#term.cols - suggestionWidth - descriptionWidth : this.#term.cols - suggestionWidth;
|
|
83
|
+
const swapDescription = wrappedPadding > maxPadding && activeDescription.length !== 0;
|
|
84
|
+
const swappedPadding = swapDescription ? Math.max(wrappedPadding - descriptionWidth, 0) : wrappedPadding;
|
|
85
|
+
const clampedLeftPadding = Math.min(Math.min(wrappedPadding, swappedPadding), maxPadding);
|
|
86
|
+
if (suggestions.length <= this.#activeSuggestionIdx) {
|
|
87
|
+
this.#activeSuggestionIdx = Math.max(suggestions.length - 1, 0);
|
|
88
|
+
}
|
|
89
|
+
if (pagedSuggestions.length == 0) {
|
|
90
|
+
if (argumentDescription != null) {
|
|
91
|
+
return {
|
|
92
|
+
data: ansi.cursorHide +
|
|
93
|
+
ansi.cursorUp(2) +
|
|
94
|
+
ansi.cursorForward(clampedLeftPadding) +
|
|
95
|
+
this._renderArgumentDescription(argumentDescription, clampedLeftPadding),
|
|
96
|
+
rows: 3,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { data: "", rows: 0 };
|
|
100
|
+
}
|
|
101
|
+
const suggestionRowsUsed = pagedSuggestions.length + borderWidth;
|
|
102
|
+
let descriptionRowsUsed = this._descriptionRows(activeDescription) + borderWidth;
|
|
103
|
+
let rows = Math.max(descriptionRowsUsed, suggestionRowsUsed);
|
|
104
|
+
if (rows <= remainingLines) {
|
|
105
|
+
descriptionRowsUsed = suggestionRowsUsed;
|
|
106
|
+
rows = suggestionRowsUsed;
|
|
107
|
+
}
|
|
108
|
+
const descriptionUI = ansi.cursorUp(descriptionRowsUsed - 1) +
|
|
109
|
+
(swapDescription
|
|
110
|
+
? this._renderDescription(activeDescription, clampedLeftPadding)
|
|
111
|
+
: this._renderDescription(activeDescription, clampedLeftPadding + suggestionWidth)) +
|
|
112
|
+
ansi.cursorDown(descriptionRowsUsed - 1);
|
|
113
|
+
const suggestionUI = ansi.cursorUp(suggestionRowsUsed - 1) +
|
|
114
|
+
(swapDescription
|
|
115
|
+
? this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding + descriptionWidth)
|
|
116
|
+
: this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding)) +
|
|
117
|
+
ansi.cursorDown(suggestionRowsUsed - 1);
|
|
118
|
+
const ui = swapDescription ? descriptionUI + suggestionUI : suggestionUI + descriptionUI;
|
|
119
|
+
return {
|
|
120
|
+
data: ansi.cursorHide + ansi.cursorForward(clampedLeftPadding) + ui + ansi.cursorShow,
|
|
121
|
+
rows,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
update(keyPress) {
|
|
125
|
+
const { name, shift, ctrl } = keyPress;
|
|
126
|
+
if (name == "return") {
|
|
127
|
+
this.#term.clearCommand(); // clear the current command on enter
|
|
128
|
+
}
|
|
129
|
+
// if suggestions are hidden, keep them hidden until during command navigation
|
|
130
|
+
if (this.#hideSuggestions) {
|
|
131
|
+
this.#hideSuggestions = name == "up" || name == "down";
|
|
132
|
+
}
|
|
133
|
+
if (!this.#suggestBlob) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
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;
|
|
137
|
+
if (name == dismissKey && shift == !!dismissShift && ctrl == !!dismissCtrl) {
|
|
138
|
+
this.#suggestBlob = undefined;
|
|
139
|
+
this.#hideSuggestions = true;
|
|
140
|
+
}
|
|
141
|
+
else if (name == prevKey && shift == !!prevShift && ctrl == !!prevCtrl) {
|
|
142
|
+
this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
|
|
143
|
+
}
|
|
144
|
+
else if (name == nextKey && shift == !!nextShift && ctrl == !!nextCtrl) {
|
|
145
|
+
this.#activeSuggestionIdx = Math.min(this.#activeSuggestionIdx + 1, (this.#suggestBlob?.suggestions.length ?? 1) - 1);
|
|
146
|
+
}
|
|
147
|
+
else if (name == acceptKey && shift == !!acceptShift && ctrl == !!acceptCtrl) {
|
|
148
|
+
const removals = "\u007F".repeat(this.#suggestBlob?.charactersToDrop ?? 0);
|
|
149
|
+
const suggestion = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx);
|
|
150
|
+
const chars = suggestion?.insertValue ?? suggestion?.name + " ";
|
|
151
|
+
if (this.#suggestBlob == null || !chars.trim() || this.#suggestBlob?.suggestions.length == 0) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
this.#term.write(removals + chars);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
log.debug({ msg: "handled keypress", ...keyPress });
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { checkLegacyConfigs, checkShellConfigPlugin, checkShellConfigs } from "../utils/shell.js";
|
|
5
|
+
export const render = async () => {
|
|
6
|
+
let errors = 0;
|
|
7
|
+
errors += await renderLegacyConfigIssues();
|
|
8
|
+
errors += await renderShellPluginIssues();
|
|
9
|
+
errors += renderShellConfigIssues();
|
|
10
|
+
process.exit(errors);
|
|
11
|
+
};
|
|
12
|
+
const renderLegacyConfigIssues = async () => {
|
|
13
|
+
const shellsWithLegacyConfigs = await checkLegacyConfigs();
|
|
14
|
+
if (shellsWithLegacyConfigs.length > 0) {
|
|
15
|
+
process.stderr.write(chalk.red("•") + chalk.bold(" detected legacy configurations\n"));
|
|
16
|
+
process.stderr.write(" the following shells have legacy configurations:\n");
|
|
17
|
+
shellsWithLegacyConfigs.forEach((shell) => {
|
|
18
|
+
process.stderr.write(chalk.red(" - ") + shell + "\n");
|
|
19
|
+
});
|
|
20
|
+
process.stderr.write(chalk.yellow(" remove any inshellisense configurations from your shell profile and re-add them following the instructions in the README\n"));
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
process.stdout.write(chalk.green("✓") + " no legacy configurations found\n");
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
};
|
|
28
|
+
const renderShellConfigIssues = () => {
|
|
29
|
+
const shellsWithoutConfigs = checkShellConfigs();
|
|
30
|
+
if (shellsWithoutConfigs.length > 0) {
|
|
31
|
+
process.stderr.write(chalk.red("•") + " the following shells do not have configurations:\n");
|
|
32
|
+
shellsWithoutConfigs.forEach((shell) => {
|
|
33
|
+
process.stderr.write(chalk.red(" - ") + shell + "\n");
|
|
34
|
+
});
|
|
35
|
+
process.stderr.write(chalk.yellow(" run " + chalk.underline(chalk.cyan("is init --generate-full-configs")) + " to generate new configurations\n"));
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
process.stdout.write(chalk.green("✓") + " all shells have configurations\n");
|
|
40
|
+
}
|
|
41
|
+
return 0;
|
|
42
|
+
};
|
|
43
|
+
const renderShellPluginIssues = async () => {
|
|
44
|
+
const { shellsWithoutPlugin, shellsWithBadPlugin } = await checkShellConfigPlugin();
|
|
45
|
+
if (shellsWithoutPlugin.length == 0) {
|
|
46
|
+
process.stdout.write(chalk.green("✓") + " all shells have plugins\n");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
process.stderr.write(chalk.red("•") + " the following shells do not have the plugin installed:\n");
|
|
50
|
+
shellsWithoutPlugin.forEach((shell) => {
|
|
51
|
+
process.stderr.write(chalk.red(" - ") + shell + "\n");
|
|
52
|
+
});
|
|
53
|
+
process.stderr.write(chalk.yellow(" review the README to generate the missing shell plugins, this warning can be ignored if you prefer manual startup\n"));
|
|
54
|
+
}
|
|
55
|
+
if (shellsWithBadPlugin.length == 0) {
|
|
56
|
+
process.stdout.write(chalk.green("✓") + " all shells have correct plugins\n");
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
process.stderr.write(chalk.red("•") + " the following shells have plugins incorrectly installed:\n");
|
|
60
|
+
shellsWithBadPlugin.forEach((shell) => {
|
|
61
|
+
process.stderr.write(chalk.red(" - ") + shell + "\n");
|
|
62
|
+
});
|
|
63
|
+
process.stderr.write(chalk.yellow(" remove and regenerate the plugins according to the README, only whitespace can be after the shell plugins\n"));
|
|
64
|
+
}
|
|
65
|
+
if (shellsWithoutPlugin.length > 0 || shellsWithBadPlugin.length > 0) {
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
};
|