fix: reduce generator failures, add cwd tracking, and improve failure logging (#193)

* fix: reduce generator failures, add cwd tracking, and improve failure logging

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* fix: update snapshot

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* ci: drop suggestion detection test for now

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* fix: add suggestion item

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

* fix: truncation on generator suggestion

Signed-off-by: Chapman Pendery <cpendery@vt.edu>

---------

Signed-off-by: Chapman Pendery <cpendery@vt.edu>
This commit is contained in:
Chapman Pendery
2024-03-06 16:58:32 -08:00
committed by GitHub
parent a0b557ee25
commit c4ab6cfa8d
10 changed files with 122 additions and 18 deletions
+62
View File
@@ -18,6 +18,7 @@
"chalk": "^5.3.0",
"commander": "^11.0.0",
"find-process": "^1.4.7",
"wcwidth": "^1.0.1",
"which": "^4.0.0",
"wrap-ansi": "^8.1.0",
"xterm-headless": "^5.3.0"
@@ -30,6 +31,7 @@
"@tsconfig/node18": "^18.2.2",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.24",
"@types/wcwidth": "^1.0.2",
"@types/which": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
@@ -2164,6 +2166,12 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
"node_modules/@types/wcwidth": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/wcwidth/-/wcwidth-1.0.2.tgz",
"integrity": "sha512-flVlIrTSL6Z0RS2/IQYP0sUDajwuZ21FQAdOjzitcraVAarBFBwBpixtvxMno56vzFvLAbaGZEhkmMVYmWVUQA==",
"dev": true
},
"node_modules/@types/which": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz",
@@ -3216,6 +3224,14 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"engines": {
"node": ">=0.8"
}
},
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3452,6 +3468,17 @@
"node": ">=0.10.0"
}
},
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"dependencies": {
"clone": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/define-data-property": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz",
@@ -8966,6 +8993,14 @@
"makeerror": "1.0.12"
}
},
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"dependencies": {
"defaults": "^1.0.3"
}
},
"node_modules/which": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
@@ -10901,6 +10936,12 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
"@types/wcwidth": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/wcwidth/-/wcwidth-1.0.2.tgz",
"integrity": "sha512-flVlIrTSL6Z0RS2/IQYP0sUDajwuZ21FQAdOjzitcraVAarBFBwBpixtvxMno56vzFvLAbaGZEhkmMVYmWVUQA==",
"dev": true
},
"@types/which": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz",
@@ -11609,6 +11650,11 @@
}
}
},
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -11785,6 +11831,14 @@
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true
},
"defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"requires": {
"clone": "^1.0.2"
}
},
"define-data-property": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz",
@@ -15695,6 +15749,14 @@
"makeerror": "1.0.12"
}
},
"wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"requires": {
"defaults": "^1.0.3"
}
},
"which": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
+2
View File
@@ -48,6 +48,7 @@
"chalk": "^5.3.0",
"commander": "^11.0.0",
"find-process": "^1.4.7",
"wcwidth": "^1.0.1",
"which": "^4.0.0",
"wrap-ansi": "^8.1.0",
"xterm-headless": "^5.3.0"
@@ -56,6 +57,7 @@
"@tsconfig/node18": "^18.2.2",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.24",
"@types/wcwidth": "^1.0.2",
"@types/which": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
+7 -5
View File
@@ -27,13 +27,14 @@ export const runGenerator = async (generator: Fig.Generator, tokens: string[], c
if (script) {
const shellInput = typeof script === "function" ? script(tokens) : script;
const scriptOutput = Array.isArray(shellInput)
? await executeShellCommand({ command: shellInput.at(0) ?? "", args: shellInput.slice(1) })
: await executeShellCommand(shellInput);
? await executeShellCommand({ command: shellInput.at(0) ?? "", args: shellInput.slice(1), cwd })
: await executeShellCommand({ ...shellInput, cwd });
const scriptStdout = scriptOutput.stdout.trim();
if (postProcess) {
suggestions.push(...postProcess(scriptOutput.stdout, tokens));
suggestions.push(...postProcess(scriptStdout, tokens));
} else if (splitOn) {
suggestions.push(...scriptOutput.stdout.split(splitOn).map((s) => ({ name: s })));
suggestions.push(...scriptStdout.split(splitOn).map((s) => ({ name: s })));
}
}
@@ -51,7 +52,8 @@ export const runGenerator = async (generator: Fig.Generator, tokens: string[], c
}
return suggestions;
} catch (e) {
log.debug({ msg: "generator failed", e, script, splitOn, template });
const err = typeof e === "string" ? e : e instanceof Error ? e.message : e;
log.debug({ msg: "generator failed", err, script, splitOn, template });
}
return suggestions;
};
+17 -7
View File
@@ -20,7 +20,11 @@ enum SuggestionIcons {
Default = "📀",
}
const getIcon = (suggestionType?: Fig.SuggestionType | undefined): string => {
const getIcon = (icon: string | undefined, suggestionType: Fig.SuggestionType | undefined): string => {
// TODO: enable fig icons once spacing is better
// if (icon && /[^\u0000-\u00ff]/.test(icon)) {
// return icon;
// }
switch (suggestionType) {
case "arg":
return SuggestionIcons.Argument;
@@ -51,7 +55,7 @@ const toSuggestion = (suggestion: Fig.Suggestion, name?: string, type?: Fig.Sugg
return {
name: name ?? getLong(suggestion.name),
description: suggestion.description,
icon: getIcon(type ?? suggestion.type),
icon: getIcon(suggestion.icon, type ?? suggestion.type),
allNames: suggestion.name instanceof Array ? suggestion.name : [suggestion.name],
priority: suggestion.priority ?? 50,
insertValue: suggestion.insertValue,
@@ -77,7 +81,7 @@ function filter<T extends Fig.BaseSuggestion & { name?: Fig.SingleOrArray<string
? {
name: matchedName,
description: s.description,
icon: getIcon(s.type ?? suggestionType),
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: s.name,
priority: s.priority ?? 50,
insertValue: s.insertValue,
@@ -88,7 +92,7 @@ function filter<T extends Fig.BaseSuggestion & { name?: Fig.SingleOrArray<string
? {
name: s.name,
description: s.description,
icon: getIcon(s.type ?? suggestionType),
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: [s.name],
priority: s.priority ?? 50,
insertValue: s.insertValue,
@@ -106,7 +110,7 @@ function filter<T extends Fig.BaseSuggestion & { name?: Fig.SingleOrArray<string
? {
name: matchedName,
description: s.description,
icon: getIcon(s.type ?? suggestionType),
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: s.name,
insertValue: s.insertValue,
priority: s.priority ?? 50,
@@ -117,7 +121,7 @@ function filter<T extends Fig.BaseSuggestion & { name?: Fig.SingleOrArray<string
? {
name: s.name,
description: s.description,
icon: getIcon(s.type ?? suggestionType),
icon: getIcon(s.icon, s.type ?? suggestionType),
allNames: [s.name],
insertValue: s.insertValue,
priority: s.priority ?? 50,
@@ -139,8 +143,14 @@ const generatorSuggestions = async (
): Promise<Suggestion[]> => {
const generators = generator instanceof Array ? generator : generator ? [generator] : [];
const tokens = acceptedTokens.map((t) => t.token);
if (partialCmd) tokens.push(partialCmd);
const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat();
return filter<Fig.Suggestion>(suggestions, filterStrategy, partialCmd, undefined);
return filter<Fig.Suggestion>(
suggestions.map((suggestion) => ({ ...suggestion, priority: suggestion.priority ?? 60 })),
filterStrategy,
partialCmd,
undefined,
);
};
const templateSuggestions = async (
+2 -2
View File
@@ -6,12 +6,12 @@ import log from "../utils/log.js";
const filepathsTemplate = async (cwd: string): Promise<Fig.TemplateSuggestion[]> => {
const files = await fsAsync.readdir(cwd, { withFileTypes: true });
return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "filepaths" } }));
return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 55, context: { templateType: "filepaths" } }));
};
const foldersTemplate = async (cwd: string): Promise<Fig.TemplateSuggestion[]> => {
const files = await fsAsync.readdir(cwd, { withFileTypes: true });
return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "folders" } }));
return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 55, context: { templateType: "folders" } }));
};
// TODO: implement history template
@@ -414,6 +414,24 @@ exports[`parseCommand fullyTypedSuggestion 1`] = `
}
`;
exports[`parseCommand generatorUsingPartialInput 1`] = `
{
"charactersToDrop": 21,
"suggestions": [
{
"allNames": [
"Microsoft.Azure.WebJobs",
],
"description": "This package contains the runtime assemblies for Microsoft.Azure.WebJobs.Host. It also adds rich diagnostics capabilities which makes it easier to monitor the WebJobs in the dashboard. For more information, please visit http://go.microsoft.com/fwlink/?LinkID=320971",
"icon": "📀",
"insertValue": "Microsoft.Azure.WebJobs",
"name": "Microsoft.Azure.WebJobs",
"priority": 60,
},
],
}
`;
exports[`parseCommand loadSpec 1`] = `
{
"charactersToDrop": 3,
@@ -490,7 +508,7 @@ exports[`parseCommand optionsSuggestedAfterVariadicArg 1`] = `
"icon": "📀",
"insertValue": undefined,
"name": "package-lock.json",
"priority": 90,
"priority": 55,
},
{
"allNames": [
+1
View File
@@ -21,6 +21,7 @@ const testData = [
{ name: "nestedNonCommands", command: "az az ", skip: true }, // TODO: fix skipped test
{ name: "loadSpec", command: "aws acm add" },
{ name: "noArgsArgumentGiven", command: "gcc lab ", maxSuggestions: 3 },
{ name: "generatorUsingPartialInput", command: "dotnet add package Microsoft.Azure.WebJo", maxSuggestions: 1 },
];
describe(`parseCommand`, () => {
+8 -1
View File
@@ -162,7 +162,14 @@ shells.map((activeShell) => {
await expect(terminal.getByText("clear")).toBeVisible();
});
test.when([Shell.Zsh, Shell.Powershell].includes(activeShell), "command detection with suggestions", async ({ terminal }) => {
test("proper overflow truncation in command", async ({ terminal }) => {
await expect(terminal.getByText("> ")).toBeVisible();
terminal.write("dotnet add package Holoon.Newtonsoft");
await expect(terminal.getByText("CanBeUndefi…│")).toBeVisible();
});
test.skip("command detection with suggestions", async ({ terminal }) => {
await expect(terminal.getByText("> ")).toBeVisible();
terminal.write(`dotnet add item\r`);
+1 -1
View File
@@ -78,7 +78,7 @@ export class SuggestionManager {
private _renderSuggestions(suggestions: Suggestion[], activeSuggestionIdx: number, x: number) {
return renderBox(
suggestions.map((suggestion, idx) => {
const suggestionText = `${suggestion.icon} ${suggestion.name}`.padEnd(suggestionWidth - borderWidth, " ");
const suggestionText = `${suggestion.icon} ${suggestion.name}`;
const truncatedSuggestion = truncateText(suggestionText, suggestionWidth - 2);
return idx == activeSuggestionIdx ? chalk.bgHex(activeSuggestionBackgroundColor)(truncatedSuggestion) : truncatedSuggestion;
}),
+3 -1
View File
@@ -4,6 +4,7 @@
import ansi from "ansi-escapes";
import wrapAnsi from "wrap-ansi";
import chalk from "chalk";
import wcwdith from "wcwidth";
/**
* Renders a box around the given rows
@@ -40,6 +41,7 @@ export const truncateMultilineText = (description: string, width: number, maxHei
*/
export const truncateText = (text: string, width: number) => {
const textPoints = [...text];
const slicedText = textPoints.slice(0, width - 1);
const wcOffset = Math.max(wcwdith(text) - textPoints.length, 0);
const slicedText = textPoints.slice(0, width - 1 - wcOffset);
return slicedText.length == textPoints.length ? text.padEnd(width) : (slicedText.join("") + "…").padEnd(width);
};