From 724304cb85978525012de6a42914b779d10293d8 Mon Sep 17 00:00:00 2001 From: Chapman Pendery <35637443+cpendery@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:56:40 -0800 Subject: [PATCH] fix: generators by syncing types/generators & providing path field support (#129) Signed-off-by: Chapman Pendery --- package-lock.json | 43 +++++++------- src/commands/complete.ts | 4 +- src/isterm/pty.ts | 2 +- src/runtime/generator.ts | 27 +++++---- src/runtime/parser.ts | 2 + src/runtime/runtime.ts | 25 +++++--- src/runtime/suggestion.ts | 49 +++++++++++---- src/runtime/template.ts | 14 ++--- src/runtime/utils.ts | 59 ++++++++++++++----- .../__snapshots__/runtime.test.ts.snap | 20 +++---- src/tests/runtime/runtime.test.ts | 3 +- src/ui/suggestionManager.ts | 10 +++- src/ui/ui-root.ts | 2 +- 13 files changed, 171 insertions(+), 89 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1ccdd3e..928d067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -849,9 +849,9 @@ } }, "node_modules/@fig/autocomplete-generators": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@fig/autocomplete-generators/-/autocomplete-generators-2.2.5.tgz", - "integrity": "sha512-FBwrK3U32fMTc0Zij0ixdbqFc4Yt0hkEqhTzsjUGlA0WXkEw+/r8jswmZkwjaj2+W+1yqMWyuk4BaY6FQAdfuA==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@fig/autocomplete-generators/-/autocomplete-generators-2.4.0.tgz", + "integrity": "sha512-fiaaCGmsgnbUJbVbNAcVDmrnCGj/SmfarK6WKt/lfQP9k1hLHkkmZQ836VSMJvPP1vAKFAiXpdJziG6EGyjAYg==" }, "node_modules/@fig/autocomplete-helpers": { "version": "1.0.7", @@ -2089,11 +2089,11 @@ } }, "node_modules/@withfig/autocomplete": { - "version": "2.633.0", - "resolved": "https://registry.npmjs.org/@withfig/autocomplete/-/autocomplete-2.633.0.tgz", - "integrity": "sha512-vNjwa88e6x0TSjUyWWqmQ+q7LlpV46RqnJqJX10tHz5WmO0mDADrlSNG5AneaXWLsGqznPQscsmA9PLWWrvxCA==", + "version": "2.648.2", + "resolved": "https://registry.npmjs.org/@withfig/autocomplete/-/autocomplete-2.648.2.tgz", + "integrity": "sha512-Dw6Irj1YoWEq92zCPRLNDMA4voKjna7jr10DuHHvnrFRbt4xOiYVbBZM+mPCkRMTerUOjrSOcPGQM4j3sA/fyA==", "dependencies": { - "@fig/autocomplete-generators": "^2.2.5", + "@fig/autocomplete-generators": "^2.4.0", "@fig/autocomplete-helpers": "^1.0.7", "@fig/autocomplete-hooks": "^1.0.2", "@withfig/api-bindings": "^0.30.3", @@ -2102,12 +2102,15 @@ "semver": "^7.5.4", "strip-json-comments": "^5.0.1", "yaml": "^2.3.2" + }, + "engines": { + "node": ">=18" } }, "node_modules/@withfig/autocomplete-types": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@withfig/autocomplete-types/-/autocomplete-types-1.28.0.tgz", - "integrity": "sha512-DB5qNZ9r5/giXD/6WY7dPQEWKh6/KEgnwSnRDaNusTfseKoEXUczVM9KLJRzuzNri1EBhzqJWPFqjYwBkr63jA==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@withfig/autocomplete-types/-/autocomplete-types-1.29.0.tgz", + "integrity": "sha512-lg7OR48K408nNPiciZ0HXveJuEgEAaylwk9VEnfPGgoWgNFnEPI32dZdZVTHTEtoE0taLByF8O8uosiYsl2+kQ==", "dev": true }, "node_modules/acorn": { @@ -8557,9 +8560,9 @@ "dev": true }, "@fig/autocomplete-generators": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@fig/autocomplete-generators/-/autocomplete-generators-2.2.5.tgz", - "integrity": "sha512-FBwrK3U32fMTc0Zij0ixdbqFc4Yt0hkEqhTzsjUGlA0WXkEw+/r8jswmZkwjaj2+W+1yqMWyuk4BaY6FQAdfuA==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@fig/autocomplete-generators/-/autocomplete-generators-2.4.0.tgz", + "integrity": "sha512-fiaaCGmsgnbUJbVbNAcVDmrnCGj/SmfarK6WKt/lfQP9k1hLHkkmZQ836VSMJvPP1vAKFAiXpdJziG6EGyjAYg==" }, "@fig/autocomplete-helpers": { "version": "1.0.7", @@ -9518,11 +9521,11 @@ } }, "@withfig/autocomplete": { - "version": "2.633.0", - "resolved": "https://registry.npmjs.org/@withfig/autocomplete/-/autocomplete-2.633.0.tgz", - "integrity": "sha512-vNjwa88e6x0TSjUyWWqmQ+q7LlpV46RqnJqJX10tHz5WmO0mDADrlSNG5AneaXWLsGqznPQscsmA9PLWWrvxCA==", + "version": "2.648.2", + "resolved": "https://registry.npmjs.org/@withfig/autocomplete/-/autocomplete-2.648.2.tgz", + "integrity": "sha512-Dw6Irj1YoWEq92zCPRLNDMA4voKjna7jr10DuHHvnrFRbt4xOiYVbBZM+mPCkRMTerUOjrSOcPGQM4j3sA/fyA==", "requires": { - "@fig/autocomplete-generators": "^2.2.5", + "@fig/autocomplete-generators": "^2.4.0", "@fig/autocomplete-helpers": "^1.0.7", "@fig/autocomplete-hooks": "^1.0.2", "@withfig/api-bindings": "^0.30.3", @@ -9534,9 +9537,9 @@ } }, "@withfig/autocomplete-types": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@withfig/autocomplete-types/-/autocomplete-types-1.28.0.tgz", - "integrity": "sha512-DB5qNZ9r5/giXD/6WY7dPQEWKh6/KEgnwSnRDaNusTfseKoEXUczVM9KLJRzuzNri1EBhzqJWPFqjYwBkr63jA==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@withfig/autocomplete-types/-/autocomplete-types-1.29.0.tgz", + "integrity": "sha512-lg7OR48K408nNPiciZ0HXveJuEgEAaylwk9VEnfPGgoWgNFnEPI32dZdZVTHTEtoE0taLByF8O8uosiYsl2+kQ==", "dev": true }, "acorn": { diff --git a/src/commands/complete.ts b/src/commands/complete.ts index c322468..d014371 100644 --- a/src/commands/complete.ts +++ b/src/commands/complete.ts @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import os from "node:os"; import { Command } from "commander"; import { getSuggestions } from "../runtime/runtime.js"; +import { Shell } from "../utils/shell.js"; const action = async (input: string) => { - const suggestions = await getSuggestions(input, process.cwd()); + const suggestions = await getSuggestions(input, process.cwd(), os.platform() === "win32" ? Shell.Cmd : Shell.Bash); process.stdout.write(JSON.stringify(suggestions)); }; diff --git a/src/isterm/pty.ts b/src/isterm/pty.ts index fdacb8d..b96e6dd 100644 --- a/src/isterm/pty.ts +++ b/src/isterm/pty.ts @@ -112,7 +112,7 @@ export class ISTerm implements IPty { case IstermOscPt.CurrentWorkingDirectory: { const cwd = data.split(";").at(1); if (cwd != null) { - this.cwd = this._sanitizedCwd(this._deserializeIsMessage(cwd)); + this.cwd = path.resolve(this._sanitizedCwd(this._deserializeIsMessage(cwd))); } break; } diff --git a/src/runtime/generator.ts b/src/runtime/generator.ts index e30f3d0..fe422c8 100644 --- a/src/runtime/generator.ts +++ b/src/runtime/generator.ts @@ -18,24 +18,22 @@ 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 => { - const { script, postProcess, scriptTimeout, splitOn, custom, template } = generator; + // TODO: support trigger + const { script, postProcess, scriptTimeout, splitOn, custom, template, filterTemplateSuggestions } = generator; const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000); const suggestions = []; try { if (script) { - const scriptOutput = - typeof script === "function" - ? script(tokens) - : typeof script === "string" - ? await executeShellCommand(script) - : script != null - ? await executeShellCommand((script as string[]).join(" ")) - : ""; + 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); + if (postProcess) { - suggestions.push(...postProcess(scriptOutput, tokens)); + suggestions.push(...postProcess(scriptOutput.stdout, tokens)); } else if (splitOn) { - suggestions.push(...scriptOutput.split(splitOn).map((s) => ({ name: s }))); + suggestions.push(...scriptOutput.stdout.split(splitOn).map((s) => ({ name: s }))); } } @@ -44,7 +42,12 @@ export const runGenerator = async (generator: Fig.Generator, tokens: string[], c } if (template != null) { - suggestions.push(...(await runTemplates(template, cwd))); + const templateSuggestions = await runTemplates(template, cwd); + if (filterTemplateSuggestions) { + suggestions.push(...filterTemplateSuggestions(templateSuggestions)); + } else { + suggestions.push(...templateSuggestions); + } } return suggestions; } catch (e) { diff --git a/src/runtime/parser.ts b/src/runtime/parser.ts index 77acd3c..663aff3 100644 --- a/src/runtime/parser.ts +++ b/src/runtime/parser.ts @@ -6,6 +6,8 @@ export type CommandToken = { complete: boolean; isOption: boolean; isPersistent?: boolean; + isPath?: boolean; + isPathComplete?: boolean; }; const cmdDelim = /(\|\|)|(&&)|(;)|(\|)/; diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 1b757b0..c7eaf5d 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -6,10 +6,12 @@ import speclist, { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore } from "@withfig/autocomplete/build/index.js"; +import path from "node:path"; import { parseCommand, CommandToken } from "./parser.js"; import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion.js"; import { SuggestionBlob } from "./model.js"; -import { buildExecuteShellCommand } from "./utils.js"; +import { buildExecuteShellCommand, resolveCwd } from "./utils.js"; +import { Shell } from "../utils/shell.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- recursive type, setting as any const specSet: any = {}; @@ -55,7 +57,7 @@ const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise => { +export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Promise => { const activeCmd = parseCommand(cmd); const rootToken = activeCmd.at(0); if (activeCmd.length === 0 || !rootToken?.complete) { @@ -67,10 +69,19 @@ export const getSuggestions = async (cmd: string, cwd: string): Promise(validOptions ?? [], filterStrategy, partialCmd, "option"); }; -const removeDuplicateSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => { +const removeAcceptedSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => { const seen = new Set(acceptedTokens.map((t) => t.token)); return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n))); }; +const removeDuplicateSuggestion = (suggestions: Suggestion[]): Suggestion[] => { + const seen = new Set(); + return suggestions + .map((s) => { + if (seen.has(s.name)) return null; + seen.add(s.name); + return s; + }) + .filter((s): s is Suggestion => s != null); +}; + const removeEmptySuggestion = (suggestions: Suggestion[]): Suggestion[] => { return suggestions.filter((s) => s.name.length > 0); }; @@ -190,7 +203,7 @@ const removeEmptySuggestion = (suggestions: Suggestion[]): Suggestion[] => { export const getSubcommandDrivenRecommendation = async ( subcommand: Fig.Subcommand, persistentOptions: Fig.Option[], - partialCmd: string | undefined, + partialToken: CommandToken | undefined, argsDepleted: boolean, argsFromSubcommand: boolean, acceptedTokens: CommandToken[], @@ -199,6 +212,11 @@ export const getSubcommandDrivenRecommendation = async ( if (argsDepleted && argsFromSubcommand) { return; } + let partialCmd = partialToken?.token; + if (partialToken?.isPath) { + partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? ""); + } + const suggestions: Suggestion[] = []; const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0; const allOptions = persistentOptions.concat(subcommand.options ?? []); @@ -215,10 +233,12 @@ export const getSubcommandDrivenRecommendation = async ( } return { - suggestions: removeEmptySuggestion( - removeDuplicateSuggestions( - suggestions.sort((a, b) => b.priority - a.priority), - acceptedTokens, + suggestions: removeDuplicateSuggestion( + removeEmptySuggestion( + removeAcceptedSuggestions( + suggestions.sort((a, b) => b.priority - a.priority), + acceptedTokens, + ), ), ), }; @@ -228,11 +248,16 @@ export const getArgDrivenRecommendation = async ( args: Fig.Arg[], subcommand: Fig.Subcommand, persistentOptions: Fig.Option[], - partialCmd: string | undefined, + partialToken: CommandToken | undefined, acceptedTokens: CommandToken[], variadicArgBound: boolean, cwd: string, ): Promise => { + let partialCmd = partialToken?.token; + if (partialToken?.isPath) { + partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? ""); + } + const activeArg = args[0]; const allOptions = persistentOptions.concat(subcommand.options ?? []); const suggestions = [ @@ -247,10 +272,12 @@ export const getArgDrivenRecommendation = async ( } return { - suggestions: removeEmptySuggestion( - removeDuplicateSuggestions( - suggestions.sort((a, b) => b.priority - a.priority), - acceptedTokens, + suggestions: removeDuplicateSuggestion( + removeEmptySuggestion( + removeAcceptedSuggestions( + suggestions.sort((a, b) => b.priority - a.priority), + acceptedTokens, + ), ), ), argumentDescription: activeArg.description ?? activeArg.name, diff --git a/src/runtime/template.ts b/src/runtime/template.ts index f08d020..02385a7 100644 --- a/src/runtime/template.ts +++ b/src/runtime/template.ts @@ -4,27 +4,27 @@ import fsAsync from "node:fs/promises"; import log from "../utils/log.js"; -const filepathsTemplate = async (cwd: string): Promise => { +const filepathsTemplate = async (cwd: string): Promise => { const files = await fsAsync.readdir(cwd, { withFileTypes: true }); - return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90 })); + return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "filepaths" } })); }; -const foldersTemplate = async (cwd: string): Promise => { +const foldersTemplate = async (cwd: string): Promise => { const files = await fsAsync.readdir(cwd, { withFileTypes: true }); - return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90 })); + return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "folders" } })); }; // TODO: implement history template -const historyTemplate = (): Fig.Suggestion[] => { +const historyTemplate = (): Fig.TemplateSuggestion[] => { return []; }; // TODO: implement help template -const helpTemplate = (): Fig.Suggestion[] => { +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): Promise => { const templates = template instanceof Array ? template : [template]; return ( await Promise.all( diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts index 640f65a..2e0dcee 100644 --- a/src/runtime/utils.ts +++ b/src/runtime/utils.ts @@ -1,30 +1,59 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { exec, spawn } from "node:child_process"; +import path from "node:path"; +import { spawn } from "node:child_process"; +import fsAsync from "node:fs/promises"; + +import { CommandToken } from "./parser.js"; +import { Shell } from "../utils/shell.js"; export type ExecuteShellCommandTTYResult = { code: number | null; }; export const buildExecuteShellCommand = - (timeout: number) => - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- TODO: use cwd in the future - async (command: string, cwd?: string): Promise => { + (timeout: number): Fig.ExecuteCommandFunction => + async ({ command, env, args, cwd }: Fig.ExecuteCommandInput): Promise => { + const child = spawn(command, args, { cwd, env }); + setTimeout(() => child.kill("SIGKILL"), timeout); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (data) => (stdout += data)); + child.stderr.on("data", (data) => (stderr += data)); return new Promise((resolve) => { - exec(command, { timeout, cwd }, (_, stdout, stderr) => { - resolve(stdout || stderr); + child.on("close", (code) => { + resolve({ + status: code ?? 0, + stderr, + stdout, + }); }); }); }; -export const executeShellCommandTTY = async (shell: string, command: string): Promise => { - const child = spawn(shell, ["-c", command.trim()], { stdio: "inherit" }); - return new Promise((resolve) => { - child.on("close", (code) => { - resolve({ - code, - }); - }); - }); +export const resolveCwd = async ( + cmdToken: CommandToken | undefined, + cwd: string, + shell: Shell, +): Promise<{ cwd: string; pathy: boolean; complete: boolean }> => { + if (cmdToken == null) return { cwd, pathy: false, complete: false }; + const { token } = cmdToken; + const sep = shell == Shell.Bash ? "/" : path.sep; + if (!token.includes(sep)) return { cwd, pathy: false, complete: false }; + const resolvedCwd = path.isAbsolute(token) ? token : path.join(cwd, token); + try { + await fsAsync.access(resolvedCwd, fsAsync.constants.R_OK); + return { cwd: resolvedCwd, pathy: true, complete: token.endsWith(sep) }; + } catch { + // fallback to the parent folder if possible + const baselessCwd = resolvedCwd.substring(0, resolvedCwd.length - path.basename(resolvedCwd).length); + try { + await fsAsync.access(baselessCwd, fsAsync.constants.R_OK); + return { cwd: baselessCwd, pathy: true, complete: token.endsWith(sep) }; + } catch { + /*empty*/ + } + return { cwd, pathy: false, complete: false }; + } }; diff --git a/src/tests/runtime/__snapshots__/runtime.test.ts.snap b/src/tests/runtime/__snapshots__/runtime.test.ts.snap index 52782f0..3fda0d1 100644 --- a/src/tests/runtime/__snapshots__/runtime.test.ts.snap +++ b/src/tests/runtime/__snapshots__/runtime.test.ts.snap @@ -482,6 +482,16 @@ exports[`parseCommand optionsSuggestedAfterVariadicArg 1`] = ` "argumentDescription": undefined, "charactersToDrop": 1, "suggestions": [ + { + "allNames": [ + "package-lock.json", + ], + "description": undefined, + "icon": "📀", + "insertValue": undefined, + "name": "package-lock.json", + "priority": 90, + }, { "allNames": [ "-@", @@ -502,16 +512,6 @@ exports[`parseCommand optionsSuggestedAfterVariadicArg 1`] = ` "name": "-1", "priority": 50, }, - { - "allNames": [ - "-A", - ], - "description": "List all entries except for . and ... Always set for the super-user", - "icon": "🔗", - "insertValue": undefined, - "name": "-A", - "priority": 50, - }, ], } `; diff --git a/src/tests/runtime/runtime.test.ts b/src/tests/runtime/runtime.test.ts index 9d4cc85..abf9e58 100644 --- a/src/tests/runtime/runtime.test.ts +++ b/src/tests/runtime/runtime.test.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { getSuggestions } from "../../runtime/runtime.js"; +import { Shell } from "../../utils/shell.js"; const testData = [ { name: "partialPrefixFilter", command: "git sta" }, @@ -26,7 +27,7 @@ describe(`parseCommand`, () => { testData.forEach(({ command, name, skip, maxSuggestions }) => { if (skip) return; test(name, async () => { - const suggestions = await getSuggestions(command, process.cwd()); + const suggestions = await getSuggestions(command, process.cwd(), Shell.Cmd); if (suggestions != null && suggestions.suggestions != null) { suggestions.suggestions = suggestions?.suggestions.slice(0, maxSuggestions); } diff --git a/src/ui/suggestionManager.ts b/src/ui/suggestionManager.ts index e9c8c42..ec07594 100644 --- a/src/ui/suggestionManager.ts +++ b/src/ui/suggestionManager.ts @@ -8,6 +8,7 @@ import { renderBox, truncateText, truncateMultilineText } from "./utils.js"; import ansi from "ansi-escapes"; import chalk from "chalk"; import { parseKeystroke } from "../utils/ansi.js"; +import { Shell } from "../utils/shell.js"; const maxSuggestions = 5; const suggestionWidth = 40; @@ -26,12 +27,14 @@ export class SuggestionManager { #command: string; #activeSuggestionIdx: number; #suggestBlob?: SuggestionBlob; + #shell: Shell; - constructor(terminal: ISTerm) { + constructor(terminal: ISTerm, shell: Shell) { this.#term = terminal; this.#suggestBlob = { suggestions: [] }; this.#command = ""; this.#activeSuggestionIdx = 0; + this.#shell = shell; } private async _loadSuggestions(): Promise { @@ -44,7 +47,7 @@ export class SuggestionManager { return; } this.#command = commandText; - const suggestionBlob = await getSuggestions(commandText, this.#term.cwd); + const suggestionBlob = await getSuggestions(commandText, this.#term.cwd, this.#shell); this.#suggestBlob = suggestionBlob; } @@ -148,7 +151,8 @@ export class SuggestionManager { this.#activeSuggestionIdx = Math.min(this.#activeSuggestionIdx + 1, (this.#suggestBlob?.suggestions.length ?? 1) - 1); } else if (keyStroke == "tab") { const removals = "\u007F".repeat(this.#suggestBlob?.charactersToDrop ?? 0); - const chars = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx)?.name + " "; + const suggestion = this.#suggestBlob?.suggestions.at(this.#activeSuggestionIdx); + const chars = suggestion?.insertValue ?? suggestion?.name + " "; if (this.#suggestBlob == null || !chars.trim() || this.#suggestBlob?.suggestions.length == 0) { return false; } diff --git a/src/ui/ui-root.ts b/src/ui/ui-root.ts index cc78b20..c8574bd 100644 --- a/src/ui/ui-root.ts +++ b/src/ui/ui-root.ts @@ -18,7 +18,7 @@ export const renderConfirmation = (live: boolean): string => { export const render = async (shell: Shell) => { const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns }); - const suggestionManager = new SuggestionManager(term); + const suggestionManager = new SuggestionManager(term, shell); let hasActiveSuggestions = false; let previousSuggestionsRows = 0; process.stdin.setRawMode(true);