@microsoft/inshellisense 0.0.1-rc.1 → 0.0.1-rc.11

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.
Files changed (48) hide show
  1. package/CODE_OF_CONDUCT.md +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +114 -55
  4. package/SECURITY.md +41 -41
  5. package/SUPPORT.md +13 -13
  6. package/build/commands/complete.js +16 -0
  7. package/build/commands/root.js +23 -31
  8. package/build/commands/uninstall.js +11 -0
  9. package/build/index.js +16 -7
  10. package/build/isterm/commandManager.js +270 -0
  11. package/build/isterm/index.js +4 -0
  12. package/build/isterm/pty.js +298 -0
  13. package/build/runtime/generator.js +23 -10
  14. package/build/runtime/parser.js +2 -2
  15. package/build/runtime/runtime.js +44 -27
  16. package/build/runtime/suggestion.js +47 -21
  17. package/build/runtime/template.js +33 -18
  18. package/build/runtime/utils.js +42 -12
  19. package/build/ui/suggestionManager.js +153 -0
  20. package/build/ui/ui-root.js +132 -63
  21. package/build/ui/ui-uninstall.js +9 -0
  22. package/build/ui/utils.js +56 -0
  23. package/build/utils/ansi.js +33 -0
  24. package/build/utils/config.js +109 -0
  25. package/build/utils/log.js +30 -0
  26. package/build/utils/shell.js +101 -0
  27. package/package.json +86 -59
  28. package/shell/bash-preexec.sh +380 -0
  29. package/shell/shellIntegration-env.zsh +9 -0
  30. package/shell/shellIntegration-login.zsh +4 -0
  31. package/shell/shellIntegration-profile.zsh +4 -0
  32. package/shell/shellIntegration-rc.zsh +58 -0
  33. package/shell/shellIntegration.bash +94 -0
  34. package/shell/shellIntegration.fish +19 -0
  35. package/shell/shellIntegration.nu +28 -0
  36. package/shell/shellIntegration.ps1 +24 -0
  37. package/shell/shellIntegration.xsh +29 -0
  38. package/build/commands/bind.js +0 -12
  39. package/build/ui/input.js +0 -55
  40. package/build/ui/suggestions.js +0 -84
  41. package/build/ui/ui-bind.js +0 -64
  42. package/build/utils/bindings.js +0 -144
  43. package/build/utils/cache.js +0 -21
  44. package/shell/key-bindings-powershell.ps1 +0 -27
  45. package/shell/key-bindings-pwsh.ps1 +0 -27
  46. package/shell/key-bindings.bash +0 -7
  47. package/shell/key-bindings.fish +0 -8
  48. package/shell/key-bindings.zsh +0 -10
@@ -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 result = await runSubcommand(activeCmd.slice(1), subcommand);
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
- const lastCommand = activeCmd.at(-1);
65
- const charactersToDrop = lastCommand?.complete ? 0 : lastCommand?.token.length ?? 0;
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
- const subcommandIdx = parentCommand.subcommands?.findIndex((s) => s.name === command);
88
- if (subcommandIdx == null)
99
+ if (!parentCommand.subcommands || parentCommand.subcommands.length === 0)
89
100
  return;
90
- const subcommand = parentCommand.subcommands?.at(subcommandIdx);
91
- if (subcommand == null)
101
+ const subcommandIdx = parentCommand.subcommands.findIndex((s) => (Array.isArray(s.name) ? s.name.includes(command) : s.name === command));
102
+ if (subcommandIdx === -1)
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].token, acceptedTokens, fromVariadic);
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].token, argsDepleted, argsUsed, acceptedTokens);
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
- return runArg(tokens, getArgs(subcommand.args), subcommand, allOptions, acceptedTokens, false, false);
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
- const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens)))).flat();
123
- return filter(suggestions, filterStrategy, partialCmd, undefined);
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 removeDuplicateSuggestions = (suggestions, acceptedTokens) => {
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, partialCmd, argsDepleted, argsFromSubcommand, acceptedTokens) => {
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(removeDuplicateSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens)),
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, partialCmd, acceptedTokens, variadicArgBound) => {
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 ((activeArg.isOptional && !activeArg.isVariadic) || (activeArg.isVariadic && activeArg.isOptional && !variadicArgBound)) {
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(removeDuplicateSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens)),
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,23 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import fsAsync from "fs/promises";
4
- import process from "node:process";
5
- const filepathsTemplate = async () => {
6
- const files = await fsAsync.readdir(process.cwd(), { withFileTypes: true });
7
- return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90 }));
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(process.cwd(), { withFileTypes: true });
11
- return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90 }));
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
- switch (t) {
25
- case "filepaths":
26
- return await filepathsTemplate();
27
- case "folders":
28
- return await foldersTemplate();
29
- case "history":
30
- return historyTemplate();
31
- case "help":
32
- return helpTemplate();
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
  };
@@ -1,22 +1,52 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
- import { exec, spawn } from "node:child_process";
4
- export const buildExecuteShellCommand = (timeout) =>
5
- // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: use cwd in the future
6
- async (command, cwd) => {
7
- return new Promise((resolve) => {
8
- exec(command, { timeout }, (_, stdout, stderr) => {
9
- resolve(stdout || stderr);
10
- });
3
+ import path from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import fsAsync from "node:fs/promises";
6
+ import { getPathSeperator } 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 = getPathSeperator(shell);
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
+ }