@microsoft/inshellisense 0.0.1-rc.1 → 0.0.1-rc.10
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/CODE_OF_CONDUCT.md +9 -9
- package/LICENSE +21 -21
- package/README.md +114 -55
- package/SECURITY.md +41 -41
- package/SUPPORT.md +13 -13
- package/build/commands/complete.js +16 -0
- package/build/commands/root.js +23 -31
- package/build/commands/uninstall.js +11 -0
- package/build/index.js +16 -7
- package/build/isterm/commandManager.js +262 -0
- package/build/isterm/index.js +4 -0
- package/build/isterm/pty.js +270 -0
- package/build/runtime/generator.js +23 -10
- package/build/runtime/parser.js +2 -2
- package/build/runtime/runtime.js +44 -27
- package/build/runtime/suggestion.js +47 -21
- package/build/runtime/template.js +24 -18
- package/build/runtime/utils.js +42 -12
- package/build/ui/suggestionManager.js +153 -0
- package/build/ui/ui-root.js +132 -63
- package/build/ui/ui-uninstall.js +9 -0
- package/build/ui/utils.js +56 -0
- package/build/utils/ansi.js +33 -0
- package/build/utils/config.js +107 -0
- package/build/utils/log.js +30 -0
- package/build/utils/shell.js +98 -0
- package/package.json +86 -59
- package/shell/bash-preexec.sh +380 -0
- package/shell/shellIntegration-env.zsh +9 -0
- package/shell/shellIntegration-login.zsh +4 -0
- package/shell/shellIntegration-profile.zsh +4 -0
- package/shell/shellIntegration-rc.zsh +58 -0
- package/shell/shellIntegration.bash +104 -0
- package/shell/shellIntegration.fish +19 -0
- package/shell/shellIntegration.ps1 +24 -0
- package/shell/shellIntegration.xsh +29 -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 -64
- package/build/utils/bindings.js +0 -144
- 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
package/build/runtime/runtime.js
CHANGED
|
@@ -4,15 +4,19 @@ import speclist, { diffVersionedCompletions as versionedSpeclist,
|
|
|
4
4
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
5
5
|
// @ts-ignore
|
|
6
6
|
} from "@withfig/autocomplete/build/index.js";
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import { parseCommand } from "./parser.js";
|
|
8
9
|
import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion.js";
|
|
9
|
-
import { buildExecuteShellCommand } from "./utils.js";
|
|
10
|
+
import { buildExecuteShellCommand, resolveCwd } from "./utils.js";
|
|
10
11
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recursive type, setting as any
|
|
11
12
|
const specSet = {};
|
|
12
13
|
speclist.forEach((s) => {
|
|
13
14
|
let activeSet = specSet;
|
|
14
15
|
const specRoutes = s.split("/");
|
|
15
16
|
specRoutes.forEach((route, idx) => {
|
|
17
|
+
if (typeof activeSet !== "object") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
16
20
|
if (idx === specRoutes.length - 1) {
|
|
17
21
|
const prefix = versionedSpeclist.includes(s) ? "/index.js" : `.js`;
|
|
18
22
|
activeSet[route] = `@withfig/autocomplete/build/${s}${prefix}`;
|
|
@@ -46,7 +50,7 @@ const lazyLoadSpec = async (key) => {
|
|
|
46
50
|
const lazyLoadSpecLocation = async (location) => {
|
|
47
51
|
return; //TODO: implement spec location loading
|
|
48
52
|
};
|
|
49
|
-
export const getSuggestions = async (cmd) => {
|
|
53
|
+
export const getSuggestions = async (cmd, cwd, shell) => {
|
|
50
54
|
const activeCmd = parseCommand(cmd);
|
|
51
55
|
const rootToken = activeCmd.at(0);
|
|
52
56
|
if (activeCmd.length === 0 || !rootToken?.complete) {
|
|
@@ -58,11 +62,19 @@ export const getSuggestions = async (cmd) => {
|
|
|
58
62
|
const subcommand = getSubcommand(spec);
|
|
59
63
|
if (subcommand == null)
|
|
60
64
|
return;
|
|
61
|
-
const
|
|
65
|
+
const lastCommand = activeCmd.at(-1);
|
|
66
|
+
const { cwd: resolvedCwd, pathy, complete: pathyComplete } = await resolveCwd(lastCommand, cwd, shell);
|
|
67
|
+
if (pathy && lastCommand) {
|
|
68
|
+
lastCommand.isPath = true;
|
|
69
|
+
lastCommand.isPathComplete = pathyComplete;
|
|
70
|
+
}
|
|
71
|
+
const result = await runSubcommand(activeCmd.slice(1), subcommand, resolvedCwd);
|
|
62
72
|
if (result == null)
|
|
63
73
|
return;
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
let charactersToDrop = lastCommand?.complete ? 0 : lastCommand?.token.length ?? 0;
|
|
75
|
+
if (pathy) {
|
|
76
|
+
charactersToDrop = pathyComplete ? 0 : path.basename(lastCommand?.token ?? "").length;
|
|
77
|
+
}
|
|
66
78
|
return { ...result, charactersToDrop };
|
|
67
79
|
};
|
|
68
80
|
const getPersistentOptions = (persistentOptions, options) => {
|
|
@@ -84,12 +96,12 @@ const getSubcommand = (spec) => {
|
|
|
84
96
|
};
|
|
85
97
|
const executeShellCommand = buildExecuteShellCommand(5000);
|
|
86
98
|
const genSubcommand = async (command, parentCommand) => {
|
|
87
|
-
|
|
88
|
-
if (subcommandIdx == null)
|
|
99
|
+
if (!parentCommand.subcommands || parentCommand.subcommands.length === 0)
|
|
89
100
|
return;
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
101
|
+
const subcommandIdx = parentCommand.subcommands.findIndex((s) => (Array.isArray(s.name) ? s.name.includes(command) : s.name === command));
|
|
102
|
+
if (subcommandIdx === -1)
|
|
92
103
|
return;
|
|
104
|
+
const subcommand = parentCommand.subcommands[subcommandIdx];
|
|
93
105
|
// this pulls in the spec from the load spec and overwrites the subcommand in the parent with the loaded spec.
|
|
94
106
|
// then it returns the subcommand and clears the loadSpec field so that it doesn't get called again
|
|
95
107
|
switch (typeof subcommand.loadSpec) {
|
|
@@ -142,7 +154,7 @@ const getPersistentTokens = (tokens) => {
|
|
|
142
154
|
const getArgs = (args) => {
|
|
143
155
|
return args instanceof Array ? args : args != null ? [args] : [];
|
|
144
156
|
};
|
|
145
|
-
const runOption = async (tokens, option, subcommand, persistentOptions, acceptedTokens) => {
|
|
157
|
+
const runOption = async (tokens, option, subcommand, cwd, persistentOptions, acceptedTokens) => {
|
|
146
158
|
if (tokens.length === 0) {
|
|
147
159
|
throw new Error("invalid state reached, option expected but no tokens found");
|
|
148
160
|
}
|
|
@@ -150,37 +162,37 @@ const runOption = async (tokens, option, subcommand, persistentOptions, accepted
|
|
|
150
162
|
const isPersistent = persistentOptions.some((o) => (typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token)));
|
|
151
163
|
if ((option.args instanceof Array && option.args.length > 0) || option.args != null) {
|
|
152
164
|
const args = option.args instanceof Array ? option.args : [option.args];
|
|
153
|
-
return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), true, false);
|
|
165
|
+
return runArg(tokens.slice(1), args, subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken), true, false);
|
|
154
166
|
}
|
|
155
|
-
return runSubcommand(tokens.slice(1), subcommand, persistentOptions, acceptedTokens.concat({ ...activeToken, isPersistent }));
|
|
167
|
+
return runSubcommand(tokens.slice(1), subcommand, cwd, persistentOptions, acceptedTokens.concat({ ...activeToken, isPersistent }));
|
|
156
168
|
};
|
|
157
|
-
const runArg = async (tokens, args, subcommand, persistentOptions, acceptedTokens, fromOption, fromVariadic) => {
|
|
169
|
+
const runArg = async (tokens, args, subcommand, cwd, persistentOptions, acceptedTokens, fromOption, fromVariadic) => {
|
|
158
170
|
if (args.length === 0) {
|
|
159
|
-
return runSubcommand(tokens, subcommand, persistentOptions, acceptedTokens, true, !fromOption);
|
|
171
|
+
return runSubcommand(tokens, subcommand, cwd, persistentOptions, acceptedTokens, true, !fromOption);
|
|
160
172
|
}
|
|
161
173
|
else if (tokens.length === 0) {
|
|
162
|
-
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, fromVariadic);
|
|
174
|
+
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, fromVariadic, cwd);
|
|
163
175
|
}
|
|
164
176
|
else if (!tokens.at(0)?.complete) {
|
|
165
|
-
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0]
|
|
177
|
+
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0], acceptedTokens, fromVariadic, cwd);
|
|
166
178
|
}
|
|
167
179
|
const activeToken = tokens[0];
|
|
168
180
|
if (args.every((a) => a.isOptional)) {
|
|
169
181
|
if (activeToken.isOption) {
|
|
170
182
|
const option = getOption(activeToken, persistentOptions.concat(subcommand.options ?? []));
|
|
171
183
|
if (option != null) {
|
|
172
|
-
return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens);
|
|
184
|
+
return runOption(tokens, option, subcommand, cwd, persistentOptions, acceptedTokens);
|
|
173
185
|
}
|
|
174
186
|
return;
|
|
175
187
|
}
|
|
176
188
|
const nextSubcommand = await genSubcommand(activeToken.token, subcommand);
|
|
177
189
|
if (nextSubcommand != null) {
|
|
178
|
-
return runSubcommand(tokens.slice(1), nextSubcommand, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken)));
|
|
190
|
+
return runSubcommand(tokens.slice(1), nextSubcommand, cwd, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken)));
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
const activeArg = args[0];
|
|
182
194
|
if (activeArg.isVariadic) {
|
|
183
|
-
return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true);
|
|
195
|
+
return runArg(tokens.slice(1), args, subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true);
|
|
184
196
|
}
|
|
185
197
|
else if (activeArg.isCommand) {
|
|
186
198
|
if (tokens.length <= 0) {
|
|
@@ -192,16 +204,16 @@ const runArg = async (tokens, args, subcommand, persistentOptions, acceptedToken
|
|
|
192
204
|
const subcommand = getSubcommand(spec);
|
|
193
205
|
if (subcommand == null)
|
|
194
206
|
return;
|
|
195
|
-
return runSubcommand(tokens.slice(1), subcommand);
|
|
207
|
+
return runSubcommand(tokens.slice(1), subcommand, cwd);
|
|
196
208
|
}
|
|
197
|
-
return runArg(tokens.slice(1), args.slice(1), subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, false);
|
|
209
|
+
return runArg(tokens.slice(1), args.slice(1), subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken), fromOption, false);
|
|
198
210
|
};
|
|
199
|
-
const runSubcommand = async (tokens, subcommand, persistentOptions = [], acceptedTokens = [], argsDepleted = false, argsUsed = false) => {
|
|
211
|
+
const runSubcommand = async (tokens, subcommand, cwd, persistentOptions = [], acceptedTokens = [], argsDepleted = false, argsUsed = false) => {
|
|
200
212
|
if (tokens.length === 0) {
|
|
201
|
-
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens);
|
|
213
|
+
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens, cwd);
|
|
202
214
|
}
|
|
203
215
|
else if (!tokens.at(0)?.complete) {
|
|
204
|
-
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0]
|
|
216
|
+
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0], argsDepleted, argsUsed, acceptedTokens, cwd);
|
|
205
217
|
}
|
|
206
218
|
const activeToken = tokens[0];
|
|
207
219
|
const activeArgsLength = subcommand.args instanceof Array ? subcommand.args.length : 1;
|
|
@@ -209,16 +221,21 @@ const runSubcommand = async (tokens, subcommand, persistentOptions = [], accepte
|
|
|
209
221
|
if (activeToken.isOption) {
|
|
210
222
|
const option = getOption(activeToken, allOptions);
|
|
211
223
|
if (option != null) {
|
|
212
|
-
return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens);
|
|
224
|
+
return runOption(tokens, option, subcommand, cwd, persistentOptions, acceptedTokens);
|
|
213
225
|
}
|
|
214
226
|
return;
|
|
215
227
|
}
|
|
216
228
|
const nextSubcommand = await genSubcommand(activeToken.token, subcommand);
|
|
217
229
|
if (nextSubcommand != null) {
|
|
218
|
-
return runSubcommand(tokens.slice(1), nextSubcommand, getPersistentOptions(persistentOptions, subcommand.options), getPersistentTokens(acceptedTokens.concat(activeToken)));
|
|
230
|
+
return runSubcommand(tokens.slice(1), nextSubcommand, cwd, getPersistentOptions(persistentOptions, subcommand.options), getPersistentTokens(acceptedTokens.concat(activeToken)));
|
|
219
231
|
}
|
|
220
232
|
if (activeArgsLength <= 0) {
|
|
221
233
|
return; // not subcommand or option & no args exist
|
|
222
234
|
}
|
|
223
|
-
|
|
235
|
+
const args = getArgs(subcommand.args);
|
|
236
|
+
if (args.length != 0) {
|
|
237
|
+
return runArg(tokens, args, subcommand, cwd, allOptions, acceptedTokens, false, false);
|
|
238
|
+
}
|
|
239
|
+
// if the subcommand has no args specified, fallback to the subcommand and ignore this item
|
|
240
|
+
return runSubcommand(tokens.slice(1), subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken));
|
|
224
241
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
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
|
var SuggestionIcons;
|
|
@@ -14,7 +15,11 @@ var SuggestionIcons;
|
|
|
14
15
|
SuggestionIcons["Special"] = "\u2B50";
|
|
15
16
|
SuggestionIcons["Default"] = "\uD83D\uDCC0";
|
|
16
17
|
})(SuggestionIcons || (SuggestionIcons = {}));
|
|
17
|
-
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
|
+
// }
|
|
18
23
|
switch (suggestionType) {
|
|
19
24
|
case "arg":
|
|
20
25
|
return SuggestionIcons.Argument;
|
|
@@ -44,7 +49,7 @@ const toSuggestion = (suggestion, name, type) => {
|
|
|
44
49
|
return {
|
|
45
50
|
name: name ?? getLong(suggestion.name),
|
|
46
51
|
description: suggestion.description,
|
|
47
|
-
icon: getIcon(type ?? suggestion.type),
|
|
52
|
+
icon: getIcon(suggestion.icon, type ?? suggestion.type),
|
|
48
53
|
allNames: suggestion.name instanceof Array ? suggestion.name : [suggestion.name],
|
|
49
54
|
priority: suggestion.priority ?? 50,
|
|
50
55
|
insertValue: suggestion.insertValue,
|
|
@@ -65,7 +70,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
65
70
|
? {
|
|
66
71
|
name: matchedName,
|
|
67
72
|
description: s.description,
|
|
68
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
73
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
69
74
|
allNames: s.name,
|
|
70
75
|
priority: s.priority ?? 50,
|
|
71
76
|
insertValue: s.insertValue,
|
|
@@ -76,7 +81,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
76
81
|
? {
|
|
77
82
|
name: s.name,
|
|
78
83
|
description: s.description,
|
|
79
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
84
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
80
85
|
allNames: [s.name],
|
|
81
86
|
priority: s.priority ?? 50,
|
|
82
87
|
insertValue: s.insertValue,
|
|
@@ -95,7 +100,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
95
100
|
? {
|
|
96
101
|
name: matchedName,
|
|
97
102
|
description: s.description,
|
|
98
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
103
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
99
104
|
allNames: s.name,
|
|
100
105
|
insertValue: s.insertValue,
|
|
101
106
|
priority: s.priority ?? 50,
|
|
@@ -106,7 +111,7 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
106
111
|
? {
|
|
107
112
|
name: s.name,
|
|
108
113
|
description: s.description,
|
|
109
|
-
icon: getIcon(s.type ?? suggestionType),
|
|
114
|
+
icon: getIcon(s.icon, s.type ?? suggestionType),
|
|
110
115
|
allNames: [s.name],
|
|
111
116
|
insertValue: s.insertValue,
|
|
112
117
|
priority: s.priority ?? 50,
|
|
@@ -116,14 +121,16 @@ function filter(suggestions, filterStrategy, partialCmd, suggestionType) {
|
|
|
116
121
|
.filter((s) => s != null);
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
|
-
const generatorSuggestions = async (generator, acceptedTokens, filterStrategy, partialCmd) => {
|
|
124
|
+
const generatorSuggestions = async (generator, acceptedTokens, filterStrategy, partialCmd, cwd) => {
|
|
120
125
|
const generators = generator instanceof Array ? generator : generator ? [generator] : [];
|
|
121
126
|
const tokens = acceptedTokens.map((t) => t.token);
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
if (partialCmd)
|
|
128
|
+
tokens.push(partialCmd);
|
|
129
|
+
const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat();
|
|
130
|
+
return filter(suggestions.map((suggestion) => ({ ...suggestion, priority: suggestion.priority ?? 60 })), filterStrategy, partialCmd, undefined);
|
|
124
131
|
};
|
|
125
|
-
const templateSuggestions = async (templates, filterStrategy, partialCmd) => {
|
|
126
|
-
return filter(await runTemplates(templates ?? []), filterStrategy, partialCmd, undefined);
|
|
132
|
+
const templateSuggestions = async (templates, filterStrategy, partialCmd, cwd) => {
|
|
133
|
+
return filter(await runTemplates(templates ?? [], cwd), filterStrategy, partialCmd, undefined);
|
|
127
134
|
};
|
|
128
135
|
const suggestionSuggestions = (suggestions, filterStrategy, partialCmd) => {
|
|
129
136
|
const cleanedSuggestions = suggestions?.map((s) => (typeof s === "string" ? { name: s } : s)) ?? [];
|
|
@@ -137,17 +144,32 @@ const optionSuggestions = (options, acceptedTokens, filterStrategy, partialCmd)
|
|
|
137
144
|
const validOptions = options?.filter((o) => o.exclusiveOn?.every((exclusiveOption) => !usedOptions.has(exclusiveOption)) ?? true);
|
|
138
145
|
return filter(validOptions ?? [], filterStrategy, partialCmd, "option");
|
|
139
146
|
};
|
|
140
|
-
const
|
|
147
|
+
const removeAcceptedSuggestions = (suggestions, acceptedTokens) => {
|
|
141
148
|
const seen = new Set(acceptedTokens.map((t) => t.token));
|
|
142
149
|
return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n)));
|
|
143
150
|
};
|
|
151
|
+
const removeDuplicateSuggestion = (suggestions) => {
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
return suggestions
|
|
154
|
+
.map((s) => {
|
|
155
|
+
if (seen.has(s.name))
|
|
156
|
+
return null;
|
|
157
|
+
seen.add(s.name);
|
|
158
|
+
return s;
|
|
159
|
+
})
|
|
160
|
+
.filter((s) => s != null);
|
|
161
|
+
};
|
|
144
162
|
const removeEmptySuggestion = (suggestions) => {
|
|
145
163
|
return suggestions.filter((s) => s.name.length > 0);
|
|
146
164
|
};
|
|
147
|
-
export const getSubcommandDrivenRecommendation = async (subcommand, persistentOptions,
|
|
165
|
+
export const getSubcommandDrivenRecommendation = async (subcommand, persistentOptions, partialToken, argsDepleted, argsFromSubcommand, acceptedTokens, cwd) => {
|
|
148
166
|
if (argsDepleted && argsFromSubcommand) {
|
|
149
167
|
return;
|
|
150
168
|
}
|
|
169
|
+
let partialCmd = partialToken?.token;
|
|
170
|
+
if (partialToken?.isPath) {
|
|
171
|
+
partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? "");
|
|
172
|
+
}
|
|
151
173
|
const suggestions = [];
|
|
152
174
|
const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0;
|
|
153
175
|
const allOptions = persistentOptions.concat(subcommand.options ?? []);
|
|
@@ -157,28 +179,32 @@ export const getSubcommandDrivenRecommendation = async (subcommand, persistentOp
|
|
|
157
179
|
}
|
|
158
180
|
if (argLength != 0) {
|
|
159
181
|
const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args;
|
|
160
|
-
suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)));
|
|
182
|
+
suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd, cwd)));
|
|
161
183
|
suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd));
|
|
162
|
-
suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd)));
|
|
184
|
+
suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd, cwd)));
|
|
163
185
|
}
|
|
164
186
|
return {
|
|
165
|
-
suggestions: removeEmptySuggestion(
|
|
187
|
+
suggestions: removeDuplicateSuggestion(removeEmptySuggestion(removeAcceptedSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens))),
|
|
166
188
|
};
|
|
167
189
|
};
|
|
168
|
-
export const getArgDrivenRecommendation = async (args, subcommand, persistentOptions,
|
|
190
|
+
export const getArgDrivenRecommendation = async (args, subcommand, persistentOptions, partialToken, acceptedTokens, variadicArgBound, cwd) => {
|
|
191
|
+
let partialCmd = partialToken?.token;
|
|
192
|
+
if (partialToken?.isPath) {
|
|
193
|
+
partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? "");
|
|
194
|
+
}
|
|
169
195
|
const activeArg = args[0];
|
|
170
196
|
const allOptions = persistentOptions.concat(subcommand.options ?? []);
|
|
171
197
|
const suggestions = [
|
|
172
|
-
...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)),
|
|
198
|
+
...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd, cwd)),
|
|
173
199
|
...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd),
|
|
174
|
-
...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd)),
|
|
200
|
+
...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd, cwd)),
|
|
175
201
|
];
|
|
176
|
-
if (
|
|
202
|
+
if (activeArg.isOptional || (activeArg.isVariadic && variadicArgBound)) {
|
|
177
203
|
suggestions.push(...subcommandSuggestions(subcommand.subcommands, activeArg?.filterStrategy, partialCmd));
|
|
178
204
|
suggestions.push(...optionSuggestions(allOptions, acceptedTokens, activeArg?.filterStrategy, partialCmd));
|
|
179
205
|
}
|
|
180
206
|
return {
|
|
181
|
-
suggestions: removeEmptySuggestion(
|
|
207
|
+
suggestions: removeDuplicateSuggestion(removeEmptySuggestion(removeAcceptedSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens))),
|
|
182
208
|
argumentDescription: activeArg.description ?? activeArg.name,
|
|
183
209
|
};
|
|
184
210
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
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.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority:
|
|
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.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 55, context: { templateType: "filepaths" } }));
|
|
8
8
|
};
|
|
9
|
-
const foldersTemplate = async () => {
|
|
10
|
-
const files = await fsAsync.readdir(
|
|
11
|
-
return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority:
|
|
9
|
+
const foldersTemplate = async (cwd) => {
|
|
10
|
+
const files = await fsAsync.readdir(cwd, { withFileTypes: true });
|
|
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 = () => {
|
|
@@ -18,18 +18,24 @@ const historyTemplate = () => {
|
|
|
18
18
|
const helpTemplate = () => {
|
|
19
19
|
return [];
|
|
20
20
|
};
|
|
21
|
-
export const runTemplates = async (template) => {
|
|
21
|
+
export const runTemplates = async (template, cwd) => {
|
|
22
22
|
const templates = template instanceof Array ? template : [template];
|
|
23
23
|
return (await Promise.all(templates.map(async (t) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
try {
|
|
25
|
+
switch (t) {
|
|
26
|
+
case "filepaths":
|
|
27
|
+
return await filepathsTemplate(cwd);
|
|
28
|
+
case "folders":
|
|
29
|
+
return await foldersTemplate(cwd);
|
|
30
|
+
case "history":
|
|
31
|
+
return historyTemplate();
|
|
32
|
+
case "help":
|
|
33
|
+
return helpTemplate();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
log.debug({ msg: "template failed", e, template: t, cwd });
|
|
38
|
+
return [];
|
|
33
39
|
}
|
|
34
40
|
}))).flat();
|
|
35
41
|
};
|
package/build/runtime/utils.js
CHANGED
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
// Copyright (c) Microsoft Corporation.
|
|
2
2
|
// Licensed under the MIT License.
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import fsAsync from "node:fs/promises";
|
|
6
|
+
import { Shell } from "../utils/shell.js";
|
|
7
|
+
import log from "../utils/log.js";
|
|
8
|
+
export const buildExecuteShellCommand = (timeout) => async ({ command, env, args, cwd }) => {
|
|
9
|
+
const child = spawn(command, args, { cwd, env });
|
|
10
|
+
setTimeout(() => child.kill("SIGKILL"), timeout);
|
|
11
|
+
let stdout = "";
|
|
12
|
+
let stderr = "";
|
|
13
|
+
child.stdout.on("data", (data) => (stdout += data));
|
|
14
|
+
child.stderr.on("data", (data) => (stderr += data));
|
|
15
|
+
child.on("error", (err) => {
|
|
16
|
+
log.debug({ msg: "shell command failed", e: err.message });
|
|
11
17
|
});
|
|
12
|
-
};
|
|
13
|
-
export const executeShellCommandTTY = async (shell, command) => {
|
|
14
|
-
const child = spawn(shell, ["-c", command.trim()], { stdio: "inherit" });
|
|
15
18
|
return new Promise((resolve) => {
|
|
16
19
|
child.on("close", (code) => {
|
|
17
20
|
resolve({
|
|
18
|
-
code,
|
|
21
|
+
status: code ?? 0,
|
|
22
|
+
stderr,
|
|
23
|
+
stdout,
|
|
19
24
|
});
|
|
20
25
|
});
|
|
21
26
|
});
|
|
22
27
|
};
|
|
28
|
+
export const resolveCwd = async (cmdToken, cwd, shell) => {
|
|
29
|
+
if (cmdToken == null)
|
|
30
|
+
return { cwd, pathy: false, complete: false };
|
|
31
|
+
const { token } = cmdToken;
|
|
32
|
+
const sep = shell == Shell.Bash ? "/" : path.sep;
|
|
33
|
+
if (!token.includes(sep))
|
|
34
|
+
return { cwd, pathy: false, complete: false };
|
|
35
|
+
const resolvedCwd = path.isAbsolute(token) ? token : path.join(cwd, token);
|
|
36
|
+
try {
|
|
37
|
+
await fsAsync.access(resolvedCwd, fsAsync.constants.R_OK);
|
|
38
|
+
return { cwd: resolvedCwd, pathy: true, complete: token.endsWith(sep) };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// fallback to the parent folder if possible
|
|
42
|
+
const baselessCwd = resolvedCwd.substring(0, resolvedCwd.length - path.basename(resolvedCwd).length);
|
|
43
|
+
try {
|
|
44
|
+
await fsAsync.access(baselessCwd, fsAsync.constants.R_OK);
|
|
45
|
+
return { cwd: baselessCwd, pathy: true, complete: token.endsWith(sep) };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/*empty*/
|
|
49
|
+
}
|
|
50
|
+
return { cwd, pathy: false, complete: false };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
constructor(terminal, shell) {
|
|
23
|
+
this.#term = terminal;
|
|
24
|
+
this.#suggestBlob = { suggestions: [] };
|
|
25
|
+
this.#command = "";
|
|
26
|
+
this.#activeSuggestionIdx = 0;
|
|
27
|
+
this.#shell = shell;
|
|
28
|
+
}
|
|
29
|
+
async _loadSuggestions() {
|
|
30
|
+
const commandText = this.#term.getCommandState().commandText;
|
|
31
|
+
if (!commandText) {
|
|
32
|
+
this.#suggestBlob = undefined;
|
|
33
|
+
this.#activeSuggestionIdx = 0;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (commandText == this.#command) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.#command = commandText;
|
|
40
|
+
const suggestionBlob = await getSuggestions(commandText, this.#term.cwd, this.#shell);
|
|
41
|
+
this.#suggestBlob = suggestionBlob;
|
|
42
|
+
this.#activeSuggestionIdx = 0;
|
|
43
|
+
}
|
|
44
|
+
_renderArgumentDescription(description, x) {
|
|
45
|
+
if (!description)
|
|
46
|
+
return "";
|
|
47
|
+
return renderBox([truncateText(description, descriptionWidth - borderWidth)], descriptionWidth, x);
|
|
48
|
+
}
|
|
49
|
+
_renderDescription(description, x) {
|
|
50
|
+
if (!description)
|
|
51
|
+
return "";
|
|
52
|
+
return renderBox(truncateMultilineText(description, descriptionWidth - borderWidth, descriptionHeight), descriptionWidth, x);
|
|
53
|
+
}
|
|
54
|
+
_descriptionRows(description) {
|
|
55
|
+
if (!description)
|
|
56
|
+
return 0;
|
|
57
|
+
return truncateMultilineText(description, descriptionWidth - borderWidth, descriptionHeight).length;
|
|
58
|
+
}
|
|
59
|
+
_renderSuggestions(suggestions, activeSuggestionIdx, x) {
|
|
60
|
+
return renderBox(suggestions.map((suggestion, idx) => {
|
|
61
|
+
const suggestionText = `${suggestion.icon} ${suggestion.name}`;
|
|
62
|
+
const truncatedSuggestion = truncateText(suggestionText, suggestionWidth - 2);
|
|
63
|
+
return idx == activeSuggestionIdx ? chalk.bgHex(activeSuggestionBackgroundColor)(truncatedSuggestion) : truncatedSuggestion;
|
|
64
|
+
}), suggestionWidth, x);
|
|
65
|
+
}
|
|
66
|
+
validate(suggestion) {
|
|
67
|
+
const commandText = this.#term.getCommandState().commandText;
|
|
68
|
+
return !commandText ? { data: "", rows: 0 } : suggestion;
|
|
69
|
+
}
|
|
70
|
+
async render(remainingLines) {
|
|
71
|
+
await this._loadSuggestions();
|
|
72
|
+
if (!this.#suggestBlob) {
|
|
73
|
+
return { data: "", rows: 0 };
|
|
74
|
+
}
|
|
75
|
+
const { suggestions, argumentDescription } = this.#suggestBlob;
|
|
76
|
+
const page = Math.min(Math.floor(this.#activeSuggestionIdx / maxSuggestions) + 1, Math.floor(suggestions.length / maxSuggestions) + 1);
|
|
77
|
+
const pagedSuggestions = suggestions.filter((_, idx) => idx < page * maxSuggestions && idx >= (page - 1) * maxSuggestions);
|
|
78
|
+
const activePagedSuggestionIndex = this.#activeSuggestionIdx % maxSuggestions;
|
|
79
|
+
const activeDescription = pagedSuggestions.at(activePagedSuggestionIndex)?.description || argumentDescription || "";
|
|
80
|
+
const wrappedPadding = this.#term.getCursorState().cursorX % this.#term.cols;
|
|
81
|
+
const maxPadding = activeDescription.length !== 0 ? this.#term.cols - suggestionWidth - descriptionWidth : this.#term.cols - suggestionWidth;
|
|
82
|
+
const swapDescription = wrappedPadding > maxPadding && activeDescription.length !== 0;
|
|
83
|
+
const swappedPadding = swapDescription ? Math.max(wrappedPadding - descriptionWidth, 0) : wrappedPadding;
|
|
84
|
+
const clampedLeftPadding = Math.min(Math.min(wrappedPadding, swappedPadding), maxPadding);
|
|
85
|
+
if (suggestions.length <= this.#activeSuggestionIdx) {
|
|
86
|
+
this.#activeSuggestionIdx = Math.max(suggestions.length - 1, 0);
|
|
87
|
+
}
|
|
88
|
+
if (pagedSuggestions.length == 0) {
|
|
89
|
+
if (argumentDescription != null) {
|
|
90
|
+
return {
|
|
91
|
+
data: ansi.cursorHide +
|
|
92
|
+
ansi.cursorUp(2) +
|
|
93
|
+
ansi.cursorForward(clampedLeftPadding) +
|
|
94
|
+
this._renderArgumentDescription(argumentDescription, clampedLeftPadding),
|
|
95
|
+
rows: 3,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { data: "", rows: 0 };
|
|
99
|
+
}
|
|
100
|
+
const suggestionRowsUsed = pagedSuggestions.length + borderWidth;
|
|
101
|
+
let descriptionRowsUsed = this._descriptionRows(activeDescription) + borderWidth;
|
|
102
|
+
let rows = Math.max(descriptionRowsUsed, suggestionRowsUsed);
|
|
103
|
+
if (rows <= remainingLines) {
|
|
104
|
+
descriptionRowsUsed = suggestionRowsUsed;
|
|
105
|
+
rows = suggestionRowsUsed;
|
|
106
|
+
}
|
|
107
|
+
const descriptionUI = ansi.cursorUp(descriptionRowsUsed - 1) +
|
|
108
|
+
(swapDescription
|
|
109
|
+
? this._renderDescription(activeDescription, clampedLeftPadding)
|
|
110
|
+
: this._renderDescription(activeDescription, clampedLeftPadding + suggestionWidth)) +
|
|
111
|
+
ansi.cursorDown(descriptionRowsUsed - 1);
|
|
112
|
+
const suggestionUI = ansi.cursorUp(suggestionRowsUsed - 1) +
|
|
113
|
+
(swapDescription
|
|
114
|
+
? this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding + descriptionWidth)
|
|
115
|
+
: this._renderSuggestions(pagedSuggestions, activePagedSuggestionIndex, clampedLeftPadding)) +
|
|
116
|
+
ansi.cursorDown(suggestionRowsUsed - 1);
|
|
117
|
+
const ui = swapDescription ? descriptionUI + suggestionUI : suggestionUI + descriptionUI;
|
|
118
|
+
return {
|
|
119
|
+
data: ansi.cursorHide + ansi.cursorForward(clampedLeftPadding) + ui + ansi.cursorShow,
|
|
120
|
+
rows,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
update(keyPress) {
|
|
124
|
+
const { name, shift, ctrl } = keyPress;
|
|
125
|
+
if (!this.#suggestBlob) {
|
|
126
|
+
return false;
|
|
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) {
|
|
130
|
+
this.#suggestBlob = undefined;
|
|
131
|
+
}
|
|
132
|
+
else if (name == prevKey && shift == !!prevShift && ctrl == !!prevCtrl) {
|
|
133
|
+
this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
|
|
134
|
+
}
|
|
135
|
+
else if (name == nextKey && shift == !!nextShift && ctrl == !!nextCtrl) {
|
|
136
|
+
this.#activeSuggestionIdx = Math.min(this.#activeSuggestionIdx + 1, (this.#suggestBlob?.suggestions.length ?? 1) - 1);
|
|
137
|
+
}
|
|
138
|
+
else if (name == acceptKey && shift == !!acceptShift && ctrl == !!acceptCtrl) {
|
|
139
|
+
const removals = "\u007F".repeat(this.#suggestBlob?.charactersToDrop ?? 0);
|
|
140
|
+
const suggestion = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx);
|
|
141
|
+
const chars = suggestion?.insertValue ?? suggestion?.name + " ";
|
|
142
|
+
if (this.#suggestBlob == null || !chars.trim() || this.#suggestBlob?.suggestions.length == 0) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
this.#term.write(removals + chars);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
log.debug({ msg: "handled keypress", ...keyPress });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|