From 225117abefb118cbcebb80c8b17ce7a3234cea1e Mon Sep 17 00:00:00 2001 From: Chapman Pendery Date: Thu, 5 Mar 2026 15:01:25 -0800 Subject: [PATCH 1/2] fix: some performance issues via abort controller Signed-off-by: Chapman Pendery --- src/runtime/generator.ts | 7 +-- src/runtime/runtime.ts | 98 +++++++++++++++++++++++++------------ src/runtime/suggestion.ts | 29 ++++++++--- src/runtime/template.ts | 12 +++-- src/runtime/utils.ts | 11 +++-- src/ui/suggestionManager.ts | 22 +++++++-- src/ui/ui-root.ts | 13 +---- 7 files changed, 129 insertions(+), 63 deletions(-) diff --git a/src/runtime/generator.ts b/src/runtime/generator.ts index 643128ec..d9d95c49 100644 --- a/src/runtime/generator.ts +++ b/src/runtime/generator.ts @@ -17,11 +17,12 @@ const getGeneratorContext = (cwd: string): Fig.GeneratorContext => { }; // TODO: add support for caching, trigger, & getQueryTerm -export const runGenerator = async (generator: Fig.Generator, tokens: string[], cwd: string): Promise => { +export const runGenerator = async (generator: Fig.Generator, tokens: string[], cwd: string, signal?: AbortSignal): Promise => { // TODO: support trigger + signal?.throwIfAborted(); const { script, postProcess, scriptTimeout, splitOn, custom, template, filterTemplateSuggestions } = generator; - const executeShellCommand = await buildExecuteShellCommand(scriptTimeout ?? 5000); + const executeShellCommand = await buildExecuteShellCommand(scriptTimeout ?? 5000, signal); const suggestions = []; try { if (script) { @@ -43,7 +44,7 @@ export const runGenerator = async (generator: Fig.Generator, tokens: string[], c } if (template != null) { - const templateSuggestions = await runTemplates(template, cwd); + const templateSuggestions = await runTemplates(template, cwd, signal); if (filterTemplateSuggestions) { suggestions.push(...filterTemplateSuggestions(templateSuggestions)); } else { diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 36e2b825..780486a5 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -47,7 +47,7 @@ loadSpecsSet(speclist as string[], versionedSpeclist, specResourcesPath); const loadedSpecs: { [key: string]: Fig.Spec } = {}; -const loadSpec = async (cmd: CommandToken[]): Promise => { +const loadSpec = async (cmd: CommandToken[], signal?: AbortSignal): Promise => { const rootToken = cmd.at(0); if (!rootToken?.complete) { return; @@ -57,6 +57,7 @@ const loadSpec = async (cmd: CommandToken[]): Promise => { return loadedSpecs[rootToken.token]; } if (specSet[rootToken.token]) { + signal?.throwIfAborted(); const specPath = specSet[rootToken.token]; const importPath = path.isAbsolute(specPath) ? pathToFileURL(specPath).href : specPath; const spec = (await import(importPath)).default; @@ -66,14 +67,15 @@ const loadSpec = async (cmd: CommandToken[]): Promise => { }; // this load spec function should only be used for `loadSpec` on the fly as it is cacheless -const lazyLoadSpec = async (key: string): Promise => { +const lazyLoadSpec = async (key: string, signal?: AbortSignal): Promise => { + signal?.throwIfAborted(); const specPath = path.join(specResourcesPath, `${key}.js`); const importPath = path.isAbsolute(specPath) ? pathToFileURL(specPath).href : specPath; return (await import(importPath)).default; }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -- will be implemented in below TODO -const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise => { +const lazyLoadSpecLocation = async (location: Fig.SpecLocation, signal?: AbortSignal): Promise => { return; //TODO: implement spec location loading }; @@ -95,7 +97,7 @@ export const loadLocalSpecsSet = async () => { ); }; -export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Promise => { +export const getSuggestions = async (cmd: string, cwd: string, shell: Shell, signal?: AbortSignal): Promise => { let activeCmd = parseCommand(cmd, shell); const rootToken = activeCmd.at(0); if (activeCmd.length === 0) { @@ -107,18 +109,20 @@ export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Pr activeCmd = aliasExpand(activeCmd); - const spec = await loadSpec(activeCmd); + signal?.throwIfAborted(); + const spec = await loadSpec(activeCmd, signal); if (spec == null) return; const subcommand = getSubcommand(spec); if (subcommand == null) return; + signal?.throwIfAborted(); const lastCommand = activeCmd.at(-1); - const { cwd: resolvedCwd, pathy, complete: pathyComplete } = await resolveCwd(lastCommand, cwd, shell); + const { cwd: resolvedCwd, pathy, complete: pathyComplete } = await resolveCwd(lastCommand, cwd, shell, signal); if (pathy && lastCommand) { lastCommand.isPath = true; lastCommand.isPathComplete = pathyComplete; } - const result = await runSubcommand(activeCmd.slice(1), activeCmd, subcommand, resolvedCwd, shell); + const result = await runSubcommand(activeCmd.slice(1), activeCmd, subcommand, resolvedCwd, shell, undefined, undefined, undefined, undefined, signal); if (result == null) return; if (result.suggestions.length == 0 && !result.argumentDescription) return; @@ -152,9 +156,7 @@ const getSubcommand = (spec?: Fig.Spec): Fig.Subcommand | undefined => { return spec; }; -const executeShellCommand = buildExecuteShellCommand(5000); - -const genSubcommand = async (command: string, parentCommand: Fig.Subcommand): Promise => { +const genSubcommand = async (command: string, parentCommand: Fig.Subcommand, signal?: AbortSignal): Promise => { if (!parentCommand.subcommands || parentCommand.subcommands.length === 0) return; const subcommandIdx = parentCommand.subcommands.findIndex((s) => (Array.isArray(s.name) ? s.name.includes(command) : s.name === command)); @@ -164,11 +166,13 @@ const genSubcommand = async (command: string, parentCommand: Fig.Subcommand): Pr // this pulls in the spec from the load spec and overwrites the subcommand in the parent with the loaded spec. // then it returns the subcommand and clears the loadSpec field so that it doesn't get called again + signal?.throwIfAborted(); + const executeShellCommand = buildExecuteShellCommand(5000, signal); switch (typeof subcommand.loadSpec) { case "function": { const partSpec = await subcommand.loadSpec(command, executeShellCommand); if (partSpec instanceof Array) { - const locationSpecs = (await Promise.all(partSpec.map((s) => lazyLoadSpecLocation(s)))).filter((s) => s != null) as Fig.Spec[]; + const locationSpecs = (await Promise.all(partSpec.map((s) => lazyLoadSpecLocation(s, signal)))).filter((s) => s != null) as Fig.Spec[]; const subcommands = locationSpecs.map((s) => getSubcommand(s)).filter((s) => s != null) as Fig.Subcommand[]; (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { ...subcommand, @@ -177,7 +181,7 @@ const genSubcommand = async (command: string, parentCommand: Fig.Subcommand): Pr }; return (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx]; } else if (Object.prototype.hasOwnProperty.call(partSpec, "type")) { - const locationSingleSpec = await lazyLoadSpecLocation(partSpec as Fig.SpecLocation); + const locationSingleSpec = await lazyLoadSpecLocation(partSpec as Fig.SpecLocation, signal); (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { ...subcommand, ...(getSubcommand(locationSingleSpec) ?? []), @@ -194,7 +198,7 @@ const genSubcommand = async (command: string, parentCommand: Fig.Subcommand): Pr } } case "string": { - const spec = await lazyLoadSpec(subcommand.loadSpec as string); + const spec = await lazyLoadSpec(subcommand.loadSpec as string, signal); (parentCommand.subcommands as Fig.Subcommand[])[subcommandIdx] = { ...subcommand, ...(getSubcommand(spec) ?? []), @@ -237,6 +241,7 @@ const runOption = async ( shell: Shell, persistentOptions: Fig.Option[], acceptedTokens: CommandToken[], + signal?: AbortSignal, ): Promise => { if (tokens.length === 0) { throw new Error("invalid state reached, option expected but no tokens found"); @@ -245,7 +250,7 @@ const runOption = async ( const isPersistent = persistentOptions.some((o) => (typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token))); if ((option.args instanceof Array && option.args.length > 0) || option.args != null) { const args = option.args instanceof Array ? option.args : [option.args]; - return runArg(tokens.slice(1), allTokens, args, subcommand, cwd, shell, persistentOptions, acceptedTokens.concat(activeToken), true, false); + return runArg(tokens.slice(1), allTokens, args, subcommand, cwd, shell, persistentOptions, acceptedTokens.concat(activeToken), true, false, signal); } return runSubcommand( tokens.slice(1), @@ -258,6 +263,9 @@ const runOption = async ( ...activeToken, isPersistent, }), + undefined, + undefined, + signal, ); }; @@ -272,13 +280,15 @@ const runArg = async ( acceptedTokens: CommandToken[], fromOption: boolean, fromVariadic: boolean, + signal?: AbortSignal, ): Promise => { + signal?.throwIfAborted(); if (args.length === 0) { - return runSubcommand(tokens, allTokens, subcommand, cwd, shell, persistentOptions, acceptedTokens, true, !fromOption); + return runSubcommand(tokens, allTokens, subcommand, cwd, shell, persistentOptions, acceptedTokens, true, !fromOption, signal); } else if (tokens.length === 0) { - return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, allTokens, fromVariadic, cwd, shell); + return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, allTokens, fromVariadic, cwd, shell, signal); } else if (!tokens.at(0)?.complete) { - return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0], acceptedTokens, allTokens, fromVariadic, cwd, shell); + return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0], acceptedTokens, allTokens, fromVariadic, cwd, shell, signal); } const activeToken = tokens[0]; @@ -286,31 +296,54 @@ const runArg = async ( if (activeToken.isOption) { const option = getOption(activeToken, persistentOptions.concat(subcommand.options ?? [])); if (option != null) { - return runOption(tokens, allTokens, option, subcommand, cwd, shell, persistentOptions, acceptedTokens); + return runOption(tokens, allTokens, option, subcommand, cwd, shell, persistentOptions, acceptedTokens, signal); } return; } - const nextSubcommand = await genSubcommand(activeToken.token, subcommand); + const nextSubcommand = await genSubcommand(activeToken.token, subcommand, signal); if (nextSubcommand != null) { - return runSubcommand(tokens.slice(1), allTokens, nextSubcommand, cwd, shell, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken))); + return runSubcommand( + tokens.slice(1), + allTokens, + nextSubcommand, + cwd, + shell, + persistentOptions, + getPersistentTokens(acceptedTokens.concat(activeToken)), + undefined, + undefined, + signal, + ); } } const activeArg = args[0]; if (activeArg.isVariadic) { - return runArg(tokens.slice(1), allTokens, args, subcommand, cwd, shell, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true); + return runArg(tokens.slice(1), allTokens, args, subcommand, cwd, shell, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true, signal); } else if (activeArg.isCommand) { if (tokens.length <= 0) { return; } - const spec = await loadSpec(tokens); + const spec = await loadSpec(tokens, signal); if (spec == null) return; const subcommand = getSubcommand(spec); if (subcommand == null) return; - return runSubcommand(tokens.slice(1), allTokens, subcommand, cwd, shell); + return runSubcommand(tokens.slice(1), allTokens, subcommand, cwd, shell, undefined, undefined, undefined, undefined, signal); } - return runArg(tokens.slice(1), allTokens, args.slice(1), subcommand, cwd, shell, persistentOptions, acceptedTokens.concat(activeToken), fromOption, false); + return runArg( + tokens.slice(1), + allTokens, + args.slice(1), + subcommand, + cwd, + shell, + persistentOptions, + acceptedTokens.concat(activeToken), + fromOption, + false, + signal, + ); }; const runSubcommand = async ( @@ -323,11 +356,13 @@ const runSubcommand = async ( acceptedTokens: CommandToken[] = [], argsDepleted = false, argsUsed = false, + signal?: AbortSignal, ): Promise => { + signal?.throwIfAborted(); if (tokens.length === 0) { - return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens, allTokens, cwd, shell); + return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens, allTokens, cwd, shell, signal); } else if (!tokens.at(0)?.complete) { - return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0], argsDepleted, argsUsed, acceptedTokens, allTokens, cwd, shell); + return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0], argsDepleted, argsUsed, acceptedTokens, allTokens, cwd, shell, signal); } const activeToken = tokens[0]; @@ -337,12 +372,12 @@ const runSubcommand = async ( if (activeToken.isOption) { const option = getOption(activeToken, allOptions); if (option != null) { - return runOption(tokens, allTokens, option, subcommand, cwd, shell, persistentOptions, acceptedTokens); + return runOption(tokens, allTokens, option, subcommand, cwd, shell, persistentOptions, acceptedTokens, signal); } return; } - const nextSubcommand = await genSubcommand(activeToken.token, subcommand); + const nextSubcommand = await genSubcommand(activeToken.token, subcommand, signal); if (nextSubcommand != null) { return runSubcommand( tokens.slice(1), @@ -352,6 +387,9 @@ const runSubcommand = async ( shell, getPersistentOptions(persistentOptions, subcommand.options), getPersistentTokens(acceptedTokens.concat(activeToken)), + undefined, + undefined, + signal, ); } @@ -361,10 +399,10 @@ const runSubcommand = async ( const args = getArgs(subcommand.args); if (args.length != 0) { - return runArg(tokens, allTokens, args, subcommand, cwd, shell, allOptions, acceptedTokens, false, false); + return runArg(tokens, allTokens, args, subcommand, cwd, shell, allOptions, acceptedTokens, false, false, signal); } // if the subcommand has no args specified, fallback to the subcommand and ignore this item - return runSubcommand(tokens.slice(1), allTokens, subcommand, cwd, shell, persistentOptions, acceptedTokens.concat(activeToken)); + return runSubcommand(tokens.slice(1), allTokens, subcommand, cwd, shell, persistentOptions, acceptedTokens.concat(activeToken), undefined, undefined, signal); }; const runCommand = async (token: CommandToken): Promise => { diff --git a/src/runtime/suggestion.ts b/src/runtime/suggestion.ts index dd38a421..a8b4934e 100644 --- a/src/runtime/suggestion.ts +++ b/src/runtime/suggestion.ts @@ -108,13 +108,23 @@ const toSuggestion = (suggestion: Fig.Suggestion, name?: string, type?: Fig.Sugg description: suggestion.description, icon: getIcon(suggestion.icon, type ?? suggestion.type), allNames: suggestion.name instanceof Array ? suggestion.name : [suggestion.name], - priority: suggestion.priority ?? 50, + priority: getSuggestionPriority(suggestion), insertValue: suggestion.insertValue, type: suggestion.type, hidden: suggestion.hidden, }; }; +const getSuggestionPriority = (suggestion: Fig.Suggestion): number => { + const isOption = + suggestion.type === "option" || + (suggestion.name instanceof Array + ? suggestion.name.some((n) => n.startsWith("--") || n.startsWith("-")) + : suggestion?.name?.startsWith("--") || suggestion?.name?.startsWith("-")); + if (isOption) return 45; + return suggestion.priority ?? 50; +}; + function filter; type?: Fig.SuggestionType | undefined }>( suggestions: T[], filterStrategy: FilterStrategy | undefined, @@ -201,11 +211,13 @@ const generatorSuggestions = async ( filterStrategy: FilterStrategy | undefined, partialCmd: string | undefined, cwd: string, + signal?: AbortSignal, ): Promise => { const generators = generator instanceof Array ? generator : generator ? [generator] : []; const tokens = allTokens.map((t) => t.token); if (partialCmd) tokens.push(partialCmd); - const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat(); + signal?.throwIfAborted(); + const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd, signal)))).flat(); return filter( suggestions.map((suggestion) => ({ ...suggestion, priority: suggestion.priority ?? 60 })), filterStrategy, @@ -219,8 +231,9 @@ const templateSuggestions = async ( filterStrategy: FilterStrategy | undefined, partialCmd: string | undefined, cwd: string, + signal?: AbortSignal, ): Promise => { - return filter(await runTemplates(templates ?? [], cwd), filterStrategy, partialCmd, undefined); + return filter(await runTemplates(templates ?? [], cwd, signal), filterStrategy, partialCmd, undefined); }; const suggestionSuggestions = ( @@ -310,6 +323,7 @@ export const getSubcommandDrivenRecommendation = async ( allTokens: CommandToken[], cwd: string, shell: Shell, + signal?: AbortSignal, ): Promise => { log.debug({ msg: "suggestion point", subcommand, persistentOptions, partialToken, argsDepleted, argsFromSubcommand, acceptedTokens, cwd }); if (argsDepleted && argsFromSubcommand) { @@ -331,9 +345,9 @@ export const getSubcommandDrivenRecommendation = async ( } if (argLength != 0) { const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args; - suggestions.push(...(await generatorSuggestions(activeArg?.generators, allTokens, activeArg?.filterStrategy, partialCmd, cwd))); + suggestions.push(...(await generatorSuggestions(activeArg?.generators, allTokens, activeArg?.filterStrategy, partialCmd, cwd, signal))); suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd)); - suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd, cwd))); + suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd, cwd, signal))); } return { @@ -369,6 +383,7 @@ export const getArgDrivenRecommendation = async ( variadicArgBound: boolean, cwd: string, shell: Shell, + signal?: AbortSignal, ): Promise => { let partialCmd = partialToken?.token; if (partialToken?.isPath) { @@ -379,9 +394,9 @@ export const getArgDrivenRecommendation = async ( const activeArg = args[0]; const allOptions = persistentOptions.concat(subcommand.options ?? []); const suggestions = [ - ...(await generatorSuggestions(args[0].generators, allTokens, activeArg?.filterStrategy, partialCmd, cwd)), + ...(await generatorSuggestions(args[0].generators, allTokens, activeArg?.filterStrategy, partialCmd, cwd, signal)), ...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd), - ...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd, cwd)), + ...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd, cwd, signal)), ]; if (activeArg.isOptional || (activeArg.isVariadic && variadicArgBound)) { diff --git a/src/runtime/template.ts b/src/runtime/template.ts index 894e751e..ea95e2b3 100644 --- a/src/runtime/template.ts +++ b/src/runtime/template.ts @@ -4,14 +4,16 @@ import fsAsync from "node:fs/promises"; import log from "../utils/log.js"; -const filepathsTemplate = async (cwd: string): Promise => { +const filepathsTemplate = async (cwd: string, signal?: AbortSignal): Promise => { + signal?.throwIfAborted(); const files = await fsAsync.readdir(cwd, { withFileTypes: true }); return files .filter((f) => f.isFile() || f.isDirectory()) .map((f) => ({ name: f.name, priority: 55, context: { templateType: "filepaths" }, type: f.isDirectory() ? "folder" : "file" })); }; -const foldersTemplate = async (cwd: string): Promise => { +const foldersTemplate = async (cwd: string, signal?: AbortSignal): Promise => { + signal?.throwIfAborted(); const files = await fsAsync.readdir(cwd, { withFileTypes: true }); return files .filter((f) => f.isDirectory()) @@ -33,7 +35,7 @@ const helpTemplate = (): Fig.TemplateSuggestion[] => { return []; }; -export const runTemplates = async (template: Fig.TemplateStrings[] | Fig.Template, cwd: string): Promise => { +export const runTemplates = async (template: Fig.TemplateStrings[] | Fig.Template, cwd: string, signal?: AbortSignal): Promise => { const templates = template instanceof Array ? template : [template]; return ( await Promise.all( @@ -41,9 +43,9 @@ export const runTemplates = async (template: Fig.TemplateStrings[] | Fig.Templat try { switch (t) { case "filepaths": - return await filepathsTemplate(cwd); + return await filepathsTemplate(cwd, signal); case "folders": - return await foldersTemplate(cwd); + return await foldersTemplate(cwd, signal); case "history": return historyTemplate(); case "help": diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts index 8e76ea52..dacff16d 100644 --- a/src/runtime/utils.ts +++ b/src/runtime/utils.ts @@ -88,12 +88,13 @@ export const escapePath = (value: string | undefined, shell: Shell): string | un value != null && needsQuoted(value, getShellQuoteChar(shell)) ? quoteString(value, getShellQuoteChar(shell)) : value; export const buildExecuteShellCommand = - (timeout: number): Fig.ExecuteCommandFunction => + (timeout: number, signal?: AbortSignal): Fig.ExecuteCommandFunction => async ({ command, env, args, cwd }: Fig.ExecuteCommandInput): Promise => { + signal?.throwIfAborted(); const executionShell = await getExecutionShell(); const escapedArgs = escapeArgs(executionShell, args); - const child = spawn(command, escapedArgs, { cwd, env: { ...process.env, ...env, ISTERM: "1" }, shell: executionShell }); - setTimeout(() => child.kill("SIGKILL"), timeout); + const child = spawn(command, escapedArgs, { cwd, env: { ...process.env, ...env, ISTERM: "1" }, shell: executionShell, signal }); + const killTimeout = setTimeout(() => child.kill("SIGKILL"), timeout); let stdout = ""; let stderr = ""; child.stdout.on("data", (data) => (stdout += data)); @@ -103,6 +104,7 @@ export const buildExecuteShellCommand = }); return new Promise((resolve) => { child.on("close", (code) => { + clearTimeout(killTimeout); resolve({ status: code ?? 0, stderr, @@ -118,6 +120,7 @@ export const resolveCwd = async ( cmdToken: CommandToken | undefined, cwd: string, shell: Shell, + signal?: AbortSignal, ): Promise<{ cwd: string; pathy: boolean; complete: boolean }> => { if (cmdToken == null || cmdToken.complete) return { cwd, pathy: false, complete: false }; const { token: rawToken, isQuoted } = cmdToken; @@ -131,12 +134,14 @@ export const resolveCwd = async ( const token = trimmedToken; const resolvedCwd = path.isAbsolute(token) ? token : isHomedir(token) ? token.replace("~", homedir()) : path.join(cwd, token); try { + signal?.throwIfAborted(); await fsAsync.access(resolvedCwd, fsAsync.constants.R_OK); return { cwd: resolvedCwd, pathy: true, complete: tokenComplete }; } catch { // fallback to the parent folder if possible const baselessCwd = resolvedCwd.substring(0, resolvedCwd.length - path.basename(resolvedCwd).length); try { + signal?.throwIfAborted(); await fsAsync.access(baselessCwd, fsAsync.constants.R_OK); return { cwd: baselessCwd, pathy: true, complete: tokenComplete }; } catch { diff --git a/src/ui/suggestionManager.ts b/src/ui/suggestionManager.ts index f8e64fc9..ef1d69b1 100644 --- a/src/ui/suggestionManager.ts +++ b/src/ui/suggestionManager.ts @@ -35,6 +35,7 @@ export class SuggestionManager { #suggestBlob?: SuggestionBlob; #shell: Shell; #hideSuggestions: boolean = false; + #abortController?: AbortController; constructor(terminal: ISTerm, shell: Shell) { this.#term = terminal; @@ -45,6 +46,7 @@ export class SuggestionManager { } private async _loadSuggestions(): Promise { + this.#abortController?.abort(); const commandText = this.#term.getCommandState().commandText; if (!commandText) { this.#command = ""; @@ -57,10 +59,19 @@ export class SuggestionManager { if (commandText == this.#command) { return; } - const suggestionBlob = await getSuggestions(commandText, this.#term.cwd, this.#shell); - this.#command = commandText; - this.#suggestBlob = suggestionBlob; - this.#activeSuggestionIdx = 0; + this.#abortController = new AbortController(); + try { + const suggestionBlob = await getSuggestions(commandText, this.#term.cwd, this.#shell, this.#abortController.signal); + this.#command = commandText; + this.#suggestBlob = suggestionBlob; + this.#activeSuggestionIdx = 0; + } catch (e) { + if (e instanceof DOMException && e.name === "AbortError") { + log.debug({ msg: "suggestion generation aborted", commandText, shell: this.#shell }); + return; + } + throw e; + } } private _renderArgumentDescription(description: string | undefined) { @@ -184,6 +195,9 @@ export class SuggestionManager { return false; } this.#term.write(chars); + } else if (name == "return" || (name == "c" && ctrl)) { + this.#term.clearCommand(); + return false; } else { return false; } diff --git a/src/ui/ui-root.ts b/src/ui/ui-root.ts index 8e66676f..0f58453a 100644 --- a/src/ui/ui-root.ts +++ b/src/ui/ui-root.ts @@ -11,7 +11,6 @@ import { getBackspaceSequence, Shell } from "../utils/shell.js"; import { enableWin32InputMode, resetToInitialState } from "../utils/ansi.js"; import { MAX_LINES, type KeyPressEvent, type SuggestionManager } from "./suggestionManager.js"; import type { ISTerm } from "../isterm/pty.js"; -import { v4 as uuidV4 } from "uuid"; export const renderConfirmation = (live: boolean): string => { const statusMessage = live ? chalk.green("live") : chalk.red("not found"); @@ -43,7 +42,7 @@ const _render = (term: ISTerm, suggestionManager: SuggestionManager, data: strin const commandState = term.getCommandState(); const cursorTerminated = handlingBackspace ? true : commandState.cursorTerminated ?? false; - const showSuggestions = hasSuggestion && cursorTerminated && !commandState.hasOutput && !cursorShift; + const showSuggestions = hasSuggestion && cursorTerminated && !commandState.hasOutput && !cursorShift && !!commandState.commandText; const patch = term.getPatch(linesOfInterest, showSuggestions ? suggestion : [], direction); const ansiCursorShow = cursorHidden ? "" : ansi.cursorShow; @@ -81,7 +80,6 @@ export const render = async (program: Command, shell: Shell, underTest: boolean, let hasSuggestion = false; let direction = _direction(term); let handlingBackspace = false; // backspace normally consistent of two data points (move back & delete), so on the first data point, we won't enforce the cursor terminated rule. this will help reduce flicker - let renderId = uuidV4(); const stdinStartedInRawMode = process.stdin.isRaw; if (process.stdin.isTTY) process.stdin.setRawMode(true); readline.emitKeypressEvents(process.stdin); @@ -103,15 +101,8 @@ export const render = async (program: Command, shell: Shell, underTest: boolean, } hasSuggestion = _render(term, suggestionManager, data, handlingBackspace, hasSuggestion); - - const currentRenderId = uuidV4(); - renderId = currentRenderId; await suggestionManager.exec(); - - // handle race conditions where a earlier render might override a later one - if (currentRenderId == renderId) { - hasSuggestion = _render(term, suggestionManager, "", handlingBackspace, hasSuggestion); - } + hasSuggestion = _render(term, suggestionManager, "", handlingBackspace, hasSuggestion); handlingBackspace = false; direction = _direction(term); From ebebf2f77dce2e39000ea032237005950e3c85fd Mon Sep 17 00:00:00 2001 From: Chapman Pendery Date: Thu, 5 Mar 2026 17:12:52 -0800 Subject: [PATCH 2/2] fix: snapshots Signed-off-by: Chapman Pendery --- .../__snapshots__/runtime.test.ts.snap | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/tests/runtime/__snapshots__/runtime.test.ts.snap b/src/tests/runtime/__snapshots__/runtime.test.ts.snap index 70aa1901..c6912cd6 100644 --- a/src/tests/runtime/__snapshots__/runtime.test.ts.snap +++ b/src/tests/runtime/__snapshots__/runtime.test.ts.snap @@ -114,7 +114,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--artifact-server-path", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -126,7 +126,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--artifact-server-port", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -138,7 +138,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--container-architecture", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -150,7 +150,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--container-daemon-socket", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -163,7 +163,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--directory", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -176,7 +176,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--dryrun", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -188,7 +188,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--env-file", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -200,7 +200,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--github-instance", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -212,7 +212,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--insecure-secrets", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -224,7 +224,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--json", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -236,7 +236,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--no-recurse", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -248,7 +248,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--no-skip-checkout", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -261,7 +261,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--quiet", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -273,7 +273,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--secret-file", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -286,7 +286,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--verbose", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -299,7 +299,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--workflows", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -312,7 +312,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--help", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -324,7 +324,7 @@ exports[`parseCommand completedOptionWithArg 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--no-descriptions", - "priority": 50, + "priority": 45, "type": undefined, }, ], @@ -567,7 +567,7 @@ exports[`parseCommand noArgsArgumentGiven 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--analyzer-output", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -579,7 +579,7 @@ exports[`parseCommand noArgsArgumentGiven 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "--analyze", - "priority": 50, + "priority": 45, "type": undefined, }, { @@ -591,7 +591,7 @@ exports[`parseCommand noArgsArgumentGiven 1`] = ` "icon": "🔗", "insertValue": undefined, "name": "-arcmt-migrate-emit-errors", - "priority": 50, + "priority": 45, "type": undefined, }, ],