@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.
Files changed (53) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +80 -6
  3. package/SECURITY.md +41 -41
  4. package/build/commands/complete.js +16 -0
  5. package/build/commands/doctor.js +11 -0
  6. package/build/commands/init.js +24 -0
  7. package/build/commands/root.js +27 -30
  8. package/build/commands/specs/list.js +26 -0
  9. package/build/commands/specs/root.js +8 -0
  10. package/build/commands/uninstall.js +1 -1
  11. package/build/index.js +20 -7
  12. package/build/isterm/commandManager.js +290 -0
  13. package/build/isterm/index.js +4 -0
  14. package/build/isterm/pty.js +372 -0
  15. package/build/runtime/alias.js +61 -0
  16. package/build/runtime/generator.js +24 -11
  17. package/build/runtime/parser.js +86 -16
  18. package/build/runtime/runtime.js +103 -45
  19. package/build/runtime/suggestion.js +70 -22
  20. package/build/runtime/template.js +33 -18
  21. package/build/runtime/utils.js +111 -12
  22. package/build/ui/suggestionManager.js +162 -0
  23. package/build/ui/ui-doctor.js +69 -0
  24. package/build/ui/ui-root.js +130 -64
  25. package/build/ui/ui-uninstall.js +3 -5
  26. package/build/ui/utils.js +57 -0
  27. package/build/utils/ansi.js +37 -0
  28. package/build/utils/config.js +132 -0
  29. package/build/utils/log.js +39 -0
  30. package/build/utils/shell.js +316 -0
  31. package/package.json +39 -6
  32. package/scripts/postinstall.js +9 -0
  33. package/shell/bash-preexec.sh +380 -0
  34. package/shell/shellIntegration-env.zsh +12 -0
  35. package/shell/shellIntegration-login.zsh +9 -0
  36. package/shell/shellIntegration-profile.zsh +9 -0
  37. package/shell/shellIntegration-rc.zsh +66 -0
  38. package/shell/shellIntegration.bash +125 -0
  39. package/shell/shellIntegration.fish +28 -0
  40. package/shell/shellIntegration.nu +36 -0
  41. package/shell/shellIntegration.ps1 +27 -0
  42. package/shell/shellIntegration.xsh +37 -0
  43. package/build/commands/bind.js +0 -12
  44. package/build/ui/input.js +0 -55
  45. package/build/ui/suggestions.js +0 -84
  46. package/build/ui/ui-bind.js +0 -69
  47. package/build/utils/bindings.js +0 -216
  48. package/build/utils/cache.js +0 -21
  49. package/shell/key-bindings-powershell.ps1 +0 -27
  50. package/shell/key-bindings-pwsh.ps1 +0 -27
  51. package/shell/key-bindings.bash +0 -7
  52. package/shell/key-bindings.fish +0 -8
  53. 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
- var SuggestionIcons;
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
- const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens)))).flat();
123
- return filter(suggestions, filterStrategy, partialCmd, undefined);
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
- const removeDuplicateSuggestions = (suggestions, acceptedTokens) => {
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, partialCmd, argsDepleted, argsFromSubcommand, acceptedTokens) => {
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(removeDuplicateSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens)),
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, partialCmd, acceptedTokens, variadicArgBound) => {
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 ((activeArg.isOptional && !activeArg.isVariadic) || (activeArg.isVariadic && activeArg.isOptional && !variadicArgBound)) {
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(removeDuplicateSuggestions(suggestions.sort((a, b) => b.priority - a.priority), acceptedTokens)),
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 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,121 @@
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
- });
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
- export const executeShellCommandTTY = async (shell, command) => {
14
- const child = spawn(shell, ["-c", command.trim()], { stdio: "inherit" });
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
+ };