From 90aea63d3f1263df514989bce69a4bb33b6826a1 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 4 Feb 2025 16:28:27 -0600 Subject: [PATCH 01/51] add fig modules --- extensions/terminal-suggest/package.json | 3 + .../fig/autocomplete-parser/src/constants.ts | 37 + .../src/fig/autocomplete-parser/src/index.ts | 9 + .../autocomplete-parser/src/loadHelpers.ts | 252 ++++ .../fig/autocomplete-parser/src/loadSpec.ts | 248 ++++ .../autocomplete-parser/src/parseArguments.ts | 1140 +++++++++++++++++ .../src/tryResolveSpecToSubcommand.ts | 47 + .../src/fig/shell-parser/src/command.ts | 239 ++++ .../src/fig/shell-parser/src/index.ts | 6 + .../src/fig/shell-parser/src/parser.ts | 735 +++++++++++ 10 files changed, 2716 insertions(+) create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts create mode 100644 extensions/terminal-suggest/src/fig/shell-parser/src/command.ts create mode 100644 extensions/terminal-suggest/src/fig/shell-parser/src/index.ts create mode 100644 extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index 82e488dd9f527..d7f995cee24a4 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -30,5 +30,8 @@ "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" + }, + "dependencies": { + "@withfig/autocomplete-helpers": "^0.1.0" } } diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts new file mode 100644 index 0000000000000..6353e39a452e9 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const AWS_SPECS = ['aws', 'q']; +const UNIX_SPECS = [ + 'cd', + 'git', + 'rm', + 'ls', + 'cat', + 'mv', + 'ssh', + 'cp', + 'chmod', + 'source', + 'curl', + 'make', + 'mkdir', + 'man', + 'ln', + 'grep', + 'kill', +]; +const EDITOR_SPECS = ['code', 'nano', 'vi', 'vim', 'nvim']; +const JS_SPECS = ['node', 'npm', 'npx', 'yarn']; +const MACOS_SPECS = ['brew', 'open']; +const OTHER_SPECS = ['docker', 'python']; + +export const MOST_USED_SPECS = [ + ...AWS_SPECS, + ...UNIX_SPECS, + ...EDITOR_SPECS, + ...JS_SPECS, + ...MACOS_SPECS, + ...OTHER_SPECS, +]; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts new file mode 100644 index 0000000000000..adde6f9136db4 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export * from './constants.js'; +export * from './loadHelpers.js'; +export * from './loadSpec.js'; +export * from './parseArguments.js'; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts new file mode 100644 index 0000000000000..3b0917bbfdfac --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts @@ -0,0 +1,252 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import logger, { Logger } from 'loglevel'; +import * as semver from 'semver'; +import { + ensureTrailingSlash, + withTimeout, + exponentialBackoff, +} from '@aws/amazon-q-developer-cli-shared/utils'; +import { + executeCommand, + fread, + isInDevMode, +} from '@aws/amazon-q-developer-cli-api-bindings-wrappers'; +import z from 'zod'; +import { MOST_USED_SPECS } from './constants.js'; + +export type SpecFileImport = + | { + default: Fig.Spec; + getVersionCommand?: Fig.GetVersionCommand; + } + | { + default: Fig.Subcommand; + versions: Fig.VersionDiffMap; + }; + +const makeCdnUrlFactory = + (baseUrl: string) => + (specName: string, ext: string = "js") => + `${baseUrl}${specName}.${ext}`; + +const cdnUrlFactory = makeCdnUrlFactory( + "https://specs.q.us-east-1.amazonaws.com/", +); + +const stringImportCache = new Map(); + +export const importString = async (str: string) => { + if (stringImportCache.has(str)) { + return stringImportCache.get(str); + } + const result = await import( + /* @vite-ignore */ + URL.createObjectURL(new Blob([str], { type: "text/javascript" })) + ); + + stringImportCache.set(str, result); + return result; +}; + +/* + * Deprecated: eventually will just use importLocalSpec above + * Load a spec import("{path}/{name}") + */ +export async function importSpecFromFile( + name: string, + path: string, + localLogger: Logger = logger, +): Promise { + const importFromPath = async (fullPath: string) => { + localLogger.info(`Loading spec from ${fullPath}`); + const contents = await fread(fullPath); + if (!contents) { + throw new Error(`Failed to read file: ${fullPath}`); + } + return contents; + }; + + let result: string; + const joinedPath = `${ensureTrailingSlash(path)}${name}`; + try { + result = await importFromPath(`${joinedPath}.js`); + } catch (_) { + result = await importFromPath(`${joinedPath}/index.js`); + } + + return importString(result); +} + +/** + * Specs can only be loaded from non "secure" contexts, so we can't load from https + */ +// export const canLoadSpecProtocol = () => getActiveWindow().location.protocol !== "https:"; + +// TODO: this is a problem for diff-versioned specs +export async function importFromPublicCDN( + name: string, +): Promise { + //TODO@meganrogge + // if (canLoadSpecProtocol()) { + return withTimeout( + 20000, + import( + /* @vite-ignore */ + `spec://localhost/${name}.js` + ), + ); + // } + + // Total of retries in the worst case should be close to previous timeout value + // 500ms * 2^5 + 5 * 1000ms + 5 * 100ms = 21500ms, before the timeout was 20000ms + try { + return await exponentialBackoff( + { + attemptTimeout: 1000, + baseDelay: 500, + maxRetries: 5, + jitter: 100, + }, + + () => import(/* @vite-ignore */ cdnUrlFactory(name)), + ); + } catch { + /**/ + } + + throw new Error("Unable to load from a CDN"); +} + +async function jsonFromPublicCDN(path: string): Promise { + // if (canLoadSpecProtocol()) { + //TODO@meganrogge + return fetch(`spec://localhost/${path}.json`).then((res) => res.json()); + // } + + return exponentialBackoff( + { + attemptTimeout: 1000, + baseDelay: 500, + maxRetries: 5, + jitter: 100, + }, + () => fetch(cdnUrlFactory(path, "json")).then((res) => res.json()), + ); +} + +// TODO: this is a problem for diff-versioned specs +export async function importFromLocalhost( + name: string, + port: number | string, +): Promise { + return withTimeout( + 20000, + import( + /* @vite-ignore */ + `http://localhost:${port}/${name}.js` + ), + ); +} + +const cachedCLIVersions: Record = {}; + +export const getCachedCLIVersion = (key: string) => + cachedCLIVersions[key] ?? null; + +export async function getVersionFromFullFile( + specData: SpecFileImport, + name: string, +) { + // if the default export is a function it is a versioned spec + if (typeof specData.default === "function") { + try { + const storageKey = `cliVersion-${name}`; + const version = getCachedCLIVersion(storageKey); + if (!isInDevMode() && version !== null) { + return version; + } + + if ("getVersionCommand" in specData && specData.getVersionCommand) { + const newVersion = await specData.getVersionCommand(executeCommand); + cachedCLIVersions[storageKey] = newVersion; + return newVersion; + } + + const newVersion = semver.clean( + ( + await executeCommand({ + command: name, + args: ["--version"], + }) + ).stdout, + ); + if (newVersion) { + cachedCLIVersions[storageKey] = newVersion; + return newVersion; + } + } catch { + /**/ + } + } + return undefined; +} + +// TODO: cache this request using SWR strategy +let publicSpecsRequest: + | Promise<{ + completions: Set; + diffVersionedSpecs: Set; + }> + | undefined; + +export function clearSpecIndex() { + publicSpecsRequest = undefined; +} + +const INDEX_ZOD = z.object({ + completions: z.array(z.string()), + diffVersionedCompletions: z.array(z.string()), +}); + +const createPublicSpecsRequest = async () => { + if (publicSpecsRequest === undefined) { + publicSpecsRequest = jsonFromPublicCDN("index") + .then(INDEX_ZOD.parse) + .then((index) => ({ + completions: new Set(index.completions), + diffVersionedSpecs: new Set(index.diffVersionedCompletions), + })) + .catch(() => { + publicSpecsRequest = undefined; + return { completions: new Set(), diffVersionedSpecs: new Set() }; + }); + } + return publicSpecsRequest; +}; + +export async function publicSpecExists(name: string): Promise { + const { completions } = await createPublicSpecsRequest(); + return completions.has(name); +} + +export async function isDiffVersionedSpec(name: string): Promise { + const { diffVersionedSpecs } = await createPublicSpecsRequest(); + return diffVersionedSpecs.has(name); +} + +export async function preloadSpecs(): Promise { + return Promise.all( + MOST_USED_SPECS.map(async (name) => { + // TODO: refactor everything to allow the correct diff-versioned specs to be loaded + // too, now we are only loading the index + if (await isDiffVersionedSpec(name)) { + return importFromPublicCDN(`${name}/index`); + } + return importFromPublicCDN(name); + }).map((promise) => promise.catch((e) => e)), + ); +} diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts new file mode 100644 index 0000000000000..79cdb8c0a5fcd --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import logger, { Logger } from 'loglevel'; +import { Settings } from '@aws/amazon-q-developer-cli-api-bindings'; +import { convertSubcommand, initializeDefault } from '@fig/autocomplete-shared'; +import { + withTimeout, + SpecLocationSource, + splitPath, + ensureTrailingSlash, +} from '@aws/amazon-q-developer-cli-shared/utils'; +import { + Subcommand, + SpecLocation, +} from '@aws/amazon-q-developer-cli-shared/internal'; +import { + SETTINGS, + getSetting, + executeCommand, + isInDevMode, +} from '@aws/amazon-q-developer-cli-api-bindings-wrappers'; +import { + importFromPublicCDN, + publicSpecExists, + SpecFileImport, + importSpecFromFile, + isDiffVersionedSpec, + importFromLocalhost, +} from './loadHelpers.js'; +import { tryResolveSpecToSubcommand } from './tryResolveSpecToSubcommand.js'; + +/** + * This searches for the first directory containing a .fig/ folder in the parent directories + */ +const searchFigFolder = async (currentDirectory: string) => { + try { + return ensureTrailingSlash( + ( + await executeCommand({ + command: 'bash', + args: [ + '-c', + `until [[ -f .fig/autocomplete/build/_shortcuts.js ]] || [[ $PWD = $HOME ]] || [[ $PWD = "/" ]]; do cd ..; done; echo $PWD`, + ], + cwd: currentDirectory, + }) + ).stdout, + ); + } catch { + return ensureTrailingSlash(currentDirectory); + } +}; + +export const serializeSpecLocation = (location: SpecLocation): string => { + if (location.type === SpecLocationSource.GLOBAL) { + return `global://name=${location.name}`; + } + return `local://path=${location.path ?? ""}&name=${location.name}`; +}; + +export const getSpecPath = async ( + name: string, + cwd: string, + isScript?: boolean, +): Promise => { + if (name === "?") { + // If the user is searching for _shortcuts.js by using "?" + const path = await searchFigFolder(cwd); + return { name: "_shortcuts", type: SpecLocationSource.LOCAL, path }; + } + + const personalShortcutsToken = + getSetting(SETTINGS.PERSONAL_SHORTCUTS_TOKEN) || "+"; + if (name === personalShortcutsToken) { + return { name: "+", type: SpecLocationSource.LOCAL, path: "~/" }; + } + + const [path, basename] = splitPath(name); + + if (!isScript) { + const type = SpecLocationSource.GLOBAL; + + // If `isScript` is undefined, we are parsing the first token, and + // any path with a / is a script. + if (isScript === undefined) { + // special-case: Symfony has "bin/console" which can be invoked directly + // and should not require a user to create script completions for it + if (name === "bin/console" || name.endsWith("/bin/console")) { + return { name: "php/bin-console", type }; + } + if (!path.includes("/")) { + return { name, type }; + } + } else if (["/", "./", "~/"].every((prefix) => !path.startsWith(prefix))) { + return { name, type }; + } + } + + const type = SpecLocationSource.LOCAL; + if (path.startsWith("/") || path.startsWith("~/")) { + return { name: basename, type, path }; + } + + const relative = path.startsWith("./") ? path.slice(2) : path; + return { name: basename, type, path: `${cwd}/${relative}` }; +}; + +type ResolvedSpecLocation = + | { type: "public"; name: string } + | { type: "private"; namespace: string; name: string }; + +export const importSpecFromLocation = async ( + specLocation: SpecLocation, + localLogger: Logger = logger, +): Promise<{ + specFile: SpecFileImport; + resolvedLocation?: ResolvedSpecLocation; +}> => { + // Try loading spec from `devCompletionsFolder` first. + const devPath = isInDevMode() + ? (getSetting(SETTINGS.DEV_COMPLETIONS_FOLDER) as string) + : undefined; + + const devPort = isInDevMode() + ? getSetting(SETTINGS.DEV_COMPLETIONS_SERVER_PORT) + : undefined; + + let specFile: SpecFileImport | undefined; + let resolvedLocation: ResolvedSpecLocation | undefined; + + if (typeof devPort === "string" || typeof devPort === "number") { + const { diffVersionedFile, name } = specLocation; + specFile = await importFromLocalhost( + diffVersionedFile ? `${name}/${diffVersionedFile}` : name, + devPort, + ); + } + + if (!specFile && devPath) { + try { + const { diffVersionedFile, name } = specLocation; + const spec = await importSpecFromFile( + diffVersionedFile ? `${name}/${diffVersionedFile}` : name, + devPath, + localLogger, + ); + specFile = spec; + } catch { + // fallback to loading other specs in dev mode. + } + } + + if (!specFile && specLocation.type === SpecLocationSource.LOCAL) { + // If we couldn't successfully load a dev spec try loading from specPath. + const { name, path } = specLocation; + const [dirname, basename] = splitPath(`${path || "~/"}${name}`); + + specFile = await importSpecFromFile( + basename, + `${dirname}.fig/autocomplete/build/`, + localLogger, + ); + } else if (!specFile) { + const { name, diffVersionedFile: versionFileName } = specLocation; + + if (await publicSpecExists(name)) { + // If we're here, importing was successful. + try { + const result = await importFromPublicCDN( + versionFileName ? `${name}/${versionFileName}` : name, + ); + + specFile = result; + resolvedLocation = { type: "public", name }; + } catch (err) { + localLogger.error("Unable to load from CDN", err); + throw err; + } + } else { + try { + specFile = await importSpecFromFile( + name, + `~/.fig/autocomplete/build/`, + localLogger, + ); + } catch (_err) { + /* empty */ + } + } + } + + if (!specFile) { + throw new Error("No spec found"); + } + + return { specFile, resolvedLocation }; +}; + +export const loadFigSubcommand = async ( + specLocation: SpecLocation, + _context?: Fig.ShellContext, + localLogger: Logger = logger, +): Promise => { + const { name } = specLocation; + const location = (await isDiffVersionedSpec(name)) + ? { ...specLocation, diffVersionedFile: "index" } + : specLocation; + const { specFile } = await importSpecFromLocation(location, localLogger); + const subcommand = await tryResolveSpecToSubcommand(specFile, specLocation); + return subcommand; +}; + +export const loadSubcommandCached = async ( + specLocation: SpecLocation, + context?: Fig.ShellContext, + localLogger: Logger = logger, +): Promise => { + const { name } = specLocation; + // const path = + // specLocation.type === SpecLocationSource.LOCAL ? specLocation.path : ""; + + // Do not load completion spec for commands that are 'disabled' by user + const disabledSpecs = + getSetting(SETTINGS.DISABLE_FOR_COMMANDS) || []; + if (disabledSpecs.includes(name)) { + localLogger.info(`Not getting path for disabled spec ${name}`); + throw new Error("Command requested disabled completion spec"); + } + + // const key = [source, path || "", name].join(","); + if (getSetting(SETTINGS.DEV_MODE_NPM_INVALIDATE_CACHE)) { + // specCache.clear(); + Settings.set(SETTINGS.DEV_MODE_NPM_INVALIDATE_CACHE, false); + // } else if (!getSetting(SETTINGS.DEV_MODE_NPM) && specCache.has(key)) { + // return specCache.get(key) as Subcommand; + } + + const subcommand = await withTimeout( + 5000, + loadFigSubcommand(specLocation, context, localLogger), + ); + const converted = convertSubcommand(subcommand, initializeDefault); + // specCache.set(key, converted); + return converted; +}; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts new file mode 100644 index 0000000000000..f0b02499894c2 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts @@ -0,0 +1,1140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import logger from 'loglevel'; +import { convertSubcommand, initializeDefault } from '@fig/autocomplete-shared'; +import { filepaths, folders } from '@fig/autocomplete-generators'; +import * as Internal from "@aws/amazon-q-developer-cli-shared/internal"; +import { + firstMatchingToken, + makeArray, + SpecLocationSource, + SuggestionFlag, + SuggestionFlags, + withTimeout, +} from '@aws/amazon-q-developer-cli-shared/utils'; +import { + executeCommand, + executeLoginShell, + getSetting, + SETTINGS, +} from '@aws/amazon-q-developer-cli-api-bindings-wrappers'; +import { + Command, + substituteAlias, +} from '@aws/amazon-q-developer-cli-shell-parser'; +import { + getSpecPath, + loadSubcommandCached, +} from './loadSpec.js'; + +type ArgArrayState = { + args: Array | null; + index: number; + variadicCount?: number; +}; + +export enum TokenType { + None = "none", + Subcommand = "subcommand", + Option = "option", + OptionArg = "option_arg", + SubcommandArg = "subcommand_arg", + + // Option chain or option passed with arg in a single token. + Composite = "composite", +} + +export type BasicAnnotation = + | { + text: string; + type: Exclude; + + // Same as text, unless in CompositeAnnotation, where, e.g. in ls -lah + // the "a" token has text: "a" but tokenName: -a + tokenName?: string; + } + | { + text: string; + type: TokenType.Subcommand; + spec: Internal.Subcommand; + specLocation: Internal.SpecLocation; + }; + +type CompositeAnnotation = { + text: string; + type: TokenType.Composite; + subtokens: BasicAnnotation[]; +}; + +export type Annotation = BasicAnnotation | CompositeAnnotation; + +export type ArgumentParserState = { + completionObj: Internal.Subcommand; + + optionArgState: ArgArrayState; + subcommandArgState: ArgArrayState; + annotations: Annotation[]; + passedOptions: Internal.Option[]; + + commandIndex: number; + // Used to exclude subcommand suggestions after user has entered a subcommand arg. + haveEnteredSubcommandArgs: boolean; + isEndOfOptions: boolean; +}; + +// Result with derived completionObj/currentArg from cached state. +export type ArgumentParserResult = { + completionObj: Internal.Subcommand; + currentArg: Internal.Arg | null; + passedOptions: Internal.Option[]; + searchTerm: string; + commandIndex: number; + suggestionFlags: SuggestionFlags; + annotations: Annotation[]; +}; + +export const createArgState = (args?: Internal.Arg[]): ArgArrayState => { + const updatedArgs: Internal.Arg[] = []; + + for (const arg of args ?? []) { + const updatedGenerators = new Set(); + for (let i = 0; i < arg.generators.length; i += 1) { + const generator = arg.generators[i]; + const templateArray = makeArray(generator.template ?? []); + + let updatedGenerator: Fig.Generator | undefined; + if (templateArray.includes("filepaths")) { + updatedGenerator = filepaths; + } else if (templateArray.includes("folders")) { + updatedGenerator = folders; + } + + if (updatedGenerator && generator.filterTemplateSuggestions) { + updatedGenerator.filterTemplateSuggestions = + generator.filterTemplateSuggestions; + } + updatedGenerators.add(updatedGenerator ?? generator); + } + + updatedArgs.push({ + ...arg, + generators: [...updatedGenerators], + }); + } + return { + args: updatedArgs.length > 0 ? updatedArgs : null, + index: 0, + }; +}; + +export const flattenAnnotations = ( + annotations: Annotation[], +): BasicAnnotation[] => { + const result: BasicAnnotation[] = []; + for (let i = 0; i < annotations.length; i += 1) { + const annotation = annotations[i]; + if (annotation.type === TokenType.Composite) { + result.push(...annotation.subtokens); + } else { + result.push(annotation); + } + } + return result; +}; + +export const optionsAreEqual = (a: Internal.Option, b: Internal.Option) => + a.name.some((name: any) => b.name.includes(name)); + +export const countEqualOptions = ( + option: Internal.Option, + options: Internal.Option[], +) => + options.reduce( + (count, opt) => (optionsAreEqual(option, opt) ? count + 1 : count), + 0, + ); + +export const updateArgState = (argState: ArgArrayState): ArgArrayState => { + // Consume an argument and update the arg state accordingly. + const { args, index, variadicCount } = argState; + + if (args && args[index] && args[index].isVariadic) { + return { args, index, variadicCount: (variadicCount || 0) + 1 }; + } + + if (args && args[index] && index < args.length - 1) { + return { args, index: index + 1 }; + } + + return { args: null, index: 0 }; +}; + +export const getCurrentArg = (argState: ArgArrayState): Internal.Arg | null => + (argState.args && argState.args[argState.index]) || null; + +export const isMandatoryOrVariadic = (arg: Internal.Arg | null): boolean => + !!arg && (arg.isVariadic || !arg.isOptional); + +const preferOptionArg = (state: ArgumentParserState): boolean => + isMandatoryOrVariadic(getCurrentArg(state.optionArgState)) || + !getCurrentArg(state.subcommandArgState); + +const getArgState = (state: ArgumentParserState): ArgArrayState => + preferOptionArg(state) ? state.optionArgState : state.subcommandArgState; + +const canConsumeOptions = (state: ArgumentParserState): boolean => { + const { + subcommandArgState, + optionArgState, + isEndOfOptions, + haveEnteredSubcommandArgs, + completionObj, + } = state; + + if ( + haveEnteredSubcommandArgs && + completionObj.parserDirectives?.optionsMustPrecedeArguments === true + ) { + return false; + } + + if (isEndOfOptions) { + return false; + } + const subcommandArg = getCurrentArg(subcommandArgState); + const optionArg = getCurrentArg(optionArgState); + + if (isMandatoryOrVariadic(getCurrentArg(optionArgState))) { + // If option arg is mandatory or variadic, we may still be able to consume + // an option if options can break and we have already passed at least one + // variadic option arg. + if ( + optionArg?.isVariadic && + optionArgState.variadicCount && + optionArg.optionsCanBreakVariadicArg !== false + ) { + return true; + } + return false; + } + + if ( + subcommandArg && + subcommandArgState.variadicCount && + subcommandArg?.optionsCanBreakVariadicArg === false + ) { + // If we are in the middle of a variadic subcommand arg, we cannot consume the + // next token as an option if optionsCanBreakVariadicArg is false + return false; + } + + return true; +}; + +export const findOption = ( + spec: Internal.Subcommand, + token: string, +): Internal.Option => { + const option = spec.options[token] || spec.persistentOptions[token]; + if (!option) { + throw new Error(`Option not found: ${token}`); + } + return option; +}; + +export const findSubcommand = ( + spec: Internal.Subcommand, + token: string, +): Internal.Subcommand => { + const subcommand = spec.subcommands[token]; + if (!subcommand) { + throw new Error("Subcommand not found"); + } + return subcommand; +}; + +const updateStateForSubcommand = ( + state: ArgumentParserState, + token: string, + isFinalToken = false, +): ArgumentParserState => { + const { completionObj, haveEnteredSubcommandArgs } = state; + if (!completionObj.subcommands) { + throw new Error("No subcommands"); + } + + if (haveEnteredSubcommandArgs) { + throw new Error("Already entered subcommand args"); + } + + const newCompletionObj = findSubcommand(state.completionObj, token); + + const annotations: Annotation[] = [ + ...state.annotations, + { text: token, type: TokenType.Subcommand }, + ]; + + if (isFinalToken) { + return { ...state, annotations }; + } + + // Mutate for parser directives and persistent options: these are carried + // down deterministically. + if (!newCompletionObj.parserDirectives && completionObj.parserDirectives) { + newCompletionObj.parserDirectives = completionObj.parserDirectives; + } + + Object.assign( + newCompletionObj.persistentOptions, + completionObj.persistentOptions, + ); + + return { + ...state, + annotations, + // Inherit parserDirectives if not specified. + completionObj: newCompletionObj, + passedOptions: [], + optionArgState: createArgState(), + subcommandArgState: createArgState(newCompletionObj.args), + }; +}; + +const updateStateForOption = ( + state: ArgumentParserState, + token: string, + isFinalToken = false, +): ArgumentParserState => { + const option = findOption(state.completionObj, token); + let { isRepeatable } = option; + if (isRepeatable === false) { + isRepeatable = 1; + } + if (isRepeatable !== true && isRepeatable !== undefined) { + const currentRepetitions = countEqualOptions(option, state.passedOptions); + if (currentRepetitions >= isRepeatable) { + throw new Error( + `Cannot pass option again, already passed ${currentRepetitions} times, ` + + `and can only be passed ${isRepeatable} times`, + ); + } + } + + const annotations: Annotation[] = [ + ...state.annotations, + { text: token, type: TokenType.Option }, + ]; + + if (isFinalToken) { + return { ...state, annotations }; + } + + return { + ...state, + annotations, + passedOptions: [...state.passedOptions, option], + optionArgState: createArgState(option.args), + }; +}; + +const updateStateForOptionArg = ( + state: ArgumentParserState, + token: string, + isFinalToken = false, +): ArgumentParserState => { + if (!getCurrentArg(state.optionArgState)) { + throw new Error("Cannot consume option arg."); + } + + const annotations: Annotation[] = [ + ...state.annotations, + { text: token, type: TokenType.OptionArg }, + ]; + + if (isFinalToken) { + return { ...state, annotations }; + } + + return { + ...state, + annotations, + optionArgState: updateArgState(state.optionArgState), + }; +}; + +const updateStateForSubcommandArg = ( + state: ArgumentParserState, + token: string, + isFinalToken = false, +): ArgumentParserState => { + // Consume token as subcommand arg if possible. + if (!getCurrentArg(state.subcommandArgState)) { + throw new Error("Cannot consume subcommand arg."); + } + + const annotations: Annotation[] = [ + ...state.annotations, + { text: token, type: TokenType.SubcommandArg }, + ]; + + if (isFinalToken) { + return { ...state, annotations }; + } + + return { + ...state, + annotations, + subcommandArgState: updateArgState(state.subcommandArgState), + haveEnteredSubcommandArgs: true, + }; +}; + +const updateStateForChainedOptionToken = ( + state: ArgumentParserState, + token: string, + isFinalToken = false, +): ArgumentParserState => { + // Handle composite option tokens, accounting for different types of inputs. + // https://en.wikipedia.org/wiki/Command-line_interface#Option_conventions_in_Unix-like_systems + // See https://stackoverflow.com/a/10818697 + // Handle -- as special option flag. + if (isFinalToken && ["-", "--"].includes(token)) { + throw new Error("Final token, not consuming as option"); + } + + if (token === "--") { + return { + ...state, + isEndOfOptions: true, + annotations: [ + ...state.annotations, + { text: token, type: TokenType.Option }, + ], + optionArgState: { args: null, index: 0 }, + }; + } + + const { parserDirectives } = state.completionObj; + const isLongOption = + parserDirectives?.flagsArePosixNoncompliant || + token.startsWith("--") || + !token.startsWith("-"); + + if (isLongOption) { + const optionSeparators = new Set( + parserDirectives?.optionArgSeparators || "=", + ); + const separatorMatches = firstMatchingToken(token, optionSeparators); + + if (separatorMatches) { + // Handle long option with equals --num=10, -pnf=10, opt=10. + const matchedSeparator = separatorMatches[0]; + const [flag, ...optionArgParts] = token.split(matchedSeparator); + const optionArg = optionArgParts.join(matchedSeparator); + const optionState = updateStateForOption(state, flag); + + if ((optionState.optionArgState.args?.length ?? 0) > 1) { + throw new Error( + "Cannot pass argument with separator: option takes multiple args", + ); + } + + const finalState = updateStateForOptionArg( + optionState, + optionArg, + isFinalToken, + ); + + return { + ...finalState, + annotations: [ + ...state.annotations, + { + type: TokenType.Composite, + text: token, + subtokens: [ + { + type: TokenType.Option, + text: `${flag}${matchedSeparator}`, + tokenName: flag, + }, + { type: TokenType.OptionArg, text: optionArg }, + ], + }, + ], + }; + } + + // Normal long option + const finalState = updateStateForOption(state, token, isFinalToken); + const option = findOption(state.completionObj, token); + return option.requiresEquals || option.requiresSeparator + ? { ...finalState, optionArgState: { args: null, index: 0 } } + : finalState; + } + + let optionState = state; + let optionArg = ""; + const subtokens: BasicAnnotation[] = []; + let { passedOptions } = state; + + for (let i = 1; i < token.length; i += 1) { + const [optionFlag, remaining] = [`-${token[i]}`, token.slice(i + 1)]; + passedOptions = optionState.passedOptions; + try { + optionState = updateStateForOption(optionState, optionFlag); + } catch (err) { + if (i > 1) { + optionArg = token.slice(i); + break; + } + throw err; + } + + subtokens.push({ + type: TokenType.Option, + text: i === 1 ? optionFlag : token[i], + tokenName: optionFlag, + }); + + if (isMandatoryOrVariadic(getCurrentArg(optionState.optionArgState))) { + optionArg = remaining; + break; + } + } + + if (optionArg) { + if ((optionState.optionArgState.args?.length ?? 0) > 1) { + throw new Error( + "Cannot chain option argument: option takes multiple args", + ); + } + + optionState = updateStateForOptionArg(optionState, optionArg, isFinalToken); + passedOptions = optionState.passedOptions; + subtokens.push({ type: TokenType.OptionArg, text: optionArg }); + } + + return { + ...optionState, + annotations: [ + ...state.annotations, + { + type: TokenType.Composite, + text: token, + subtokens, + }, + ], + passedOptions: isFinalToken ? passedOptions : optionState.passedOptions, + }; +}; + +const canConsumeSubcommands = (state: ArgumentParserState): boolean => + !isMandatoryOrVariadic(getCurrentArg(state.optionArgState)) && + !state.haveEnteredSubcommandArgs; + +// State machine for argument parser. +function updateState( + state: ArgumentParserState, + token: string, + isFinalToken = false, +): ArgumentParserState { + if (canConsumeSubcommands(state)) { + try { + return updateStateForSubcommand(state, token, isFinalToken); + } catch (_err) { + // Continue to other token types if we can't consume subcommand. + } + } + + if (canConsumeOptions(state)) { + try { + return updateStateForChainedOptionToken(state, token, isFinalToken); + } catch (_err) { + // Continue to other token types if we can't consume option. + } + } + + if (preferOptionArg(state)) { + try { + return updateStateForOptionArg(state, token, isFinalToken); + } catch (_err) { + // Continue to other token types if we can't consume option arg. + } + } + + return updateStateForSubcommandArg(state, token, isFinalToken); +} + +const getInitialState = ( + spec: Internal.Subcommand, + text?: string, + specLocation?: Internal.SpecLocation, +): ArgumentParserState => ({ + completionObj: spec, + passedOptions: [], + + annotations: + text && specLocation + ? [{ text, type: TokenType.Subcommand, spec, specLocation }] + : [], + commandIndex: 0, + + optionArgState: createArgState(), + subcommandArgState: createArgState(spec.args), + + haveEnteredSubcommandArgs: false, + isEndOfOptions: false, +}); + +const historyExecuteShellCommand: Fig.ExecuteCommandFunction = async () => { + throw new Error( + "Cannot run shell command while parsing history", + ); +}; + +const getExecuteShellCommandFunction = (isParsingHistory = false) => + isParsingHistory ? historyExecuteShellCommand : executeCommand; + +// const getGenerateSpecCacheKey = ( +// completionObj: Internal.Subcommand, +// tokenArray: string[], +// ): string | undefined => { +// let cacheKey: string | undefined; + +// const generateSpecCacheKey = completionObj?.generateSpecCacheKey; +// if (generateSpecCacheKey) { +// if (typeof generateSpecCacheKey === "string") { +// cacheKey = generateSpecCacheKey; +// } else if (typeof generateSpecCacheKey === "function") { +// cacheKey = generateSpecCacheKey({ +// tokens: tokenArray, +// }); +// } else { +// logger.error( +// "generateSpecCacheKey must be a string or function", +// generateSpecCacheKey, +// ); +// } +// } + +// // Return this late to ensure any generateSpecCacheKey side effects still happen +// if (isInDevMode()) { +// return undefined; +// } +// if (typeof cacheKey === "string") { +// // Prepend the spec name to the cacheKey to avoid collisions between specs. +// return `${tokenArray[0]}:${cacheKey}`; +// } +// return undefined; +// }; + +const generateSpecForState = async ( + state: ArgumentParserState, + tokenArray: string[], + isParsingHistory = false, + localLogger: logger.Logger = logger, +): Promise => { + localLogger.debug("generateSpec", { state, tokenArray }); + const { completionObj } = state; + const { generateSpec } = completionObj; + if (!generateSpec) { + return state; + } + + try { + // const cacheKey = getGenerateSpecCacheKey(completionObj, tokenArray); + let newSpec; + // if (cacheKey && generateSpecCache.has(cacheKey)) { + // newSpec = generateSpecCache.get(cacheKey)!; + // } else { + const exec = getExecuteShellCommandFunction(isParsingHistory); + newSpec = convertSubcommand( + await generateSpec(tokenArray, exec), + initializeDefault, + ); + // if (cacheKey) generateSpecCache.set(cacheKey, newSpec); + // } + + const keepArgs = completionObj.args.length > 0; + + return { + ...state, + completionObj: { + ...completionObj, + subcommands: { ...completionObj.subcommands, ...newSpec.subcommands }, + options: { ...completionObj.options, ...newSpec.options }, + persistentOptions: { + ...completionObj.persistentOptions, + ...newSpec.persistentOptions, + }, + args: keepArgs ? completionObj.args : newSpec.args, + }, + subcommandArgState: keepArgs + ? state.subcommandArgState + : createArgState(newSpec.args), + }; + } catch (err) { + if (!(err instanceof Error)) { + localLogger.error( + `There was an error with spec (generator owner: ${completionObj.name + }, tokens: ${tokenArray.join(", ")}) generateSpec function`, + err, + ); + } + } + return state; +}; + +export const getResultFromState = ( + state: ArgumentParserState, +): ArgumentParserResult => { + const { completionObj, passedOptions, commandIndex, annotations } = state; + + const lastAnnotation: Annotation | undefined = + annotations[annotations.length - 1]; + let argState = getArgState(state); + let searchTerm = lastAnnotation?.text ?? ""; + + let onlySuggestArgs = state.isEndOfOptions; + + if (lastAnnotation?.type === TokenType.Composite) { + argState = state.optionArgState; + + const lastSubtoken = + lastAnnotation.subtokens[lastAnnotation.subtokens.length - 1]; + if (lastSubtoken.type === TokenType.OptionArg) { + searchTerm = lastSubtoken.text; + onlySuggestArgs = true; + } + } + + const currentArg = getCurrentArg(argState); + + // Determine what to suggest from final state, always suggest args. + let suggestionFlags: SuggestionFlags = SuggestionFlag.Args; + + // Selectively enable options or subcommand suggestions if it makes sense. + if (!onlySuggestArgs) { + if (canConsumeSubcommands(state)) { + suggestionFlags |= SuggestionFlag.Subcommands; + } + if (canConsumeOptions(state)) { + suggestionFlags |= SuggestionFlag.Options; + } + } + + return { + completionObj, + passedOptions, + commandIndex, + annotations, + + currentArg, + searchTerm, + suggestionFlags, + }; +}; + +export const initialParserState = getResultFromState( + getInitialState({ + name: [""], + subcommands: {}, + options: {}, + persistentOptions: {}, + parserDirectives: {}, + args: [], + }), +); + +// const parseArgumentsCache = createCache(); +// const parseArgumentsGenerateSpecCache = createCache(); +// const figCaches = new Set(); +// export const clearFigCaches = () => { +// for (const cache of figCaches) { +// parseArgumentsGenerateSpecCache.delete(cache); +// } +// return { unsubscribe: false }; +// }; + +// const getCacheKey = ( +// tokenArray: string[], +// context: Fig.ShellContext, +// specLocation: Internal.SpecLocation, +// ): string => +// [ +// tokenArray.slice(0, -1).join(" "), +// serializeSpecLocation(specLocation), +// context.currentWorkingDirectory, +// context.currentProcess, +// ].join(","); + +// Parse all arguments in tokenArray. +const parseArgumentsCached = async ( + command: Command, + context: Fig.ShellContext, + // authClient: AuthClient, + specLocations?: Internal.SpecLocation[], + isParsingHistory?: boolean, + startIndex = 0, + localLogger: logger.Logger = logger, +): Promise => { + const exec = getExecuteShellCommandFunction(isParsingHistory); + + let currentCommand = command; + let tokens = currentCommand.tokens.slice(startIndex); + const tokenText = tokens.map((token: any) => token.text); + + const locations = specLocations || [ + await getSpecPath(tokenText[0], context.currentWorkingDirectory), + ]; + localLogger.debug({ locations }); + + // let cacheKey = ""; + // for (let i = 0; i < locations.length; i += 1) { + // cacheKey = getCacheKey(tokenText, context, locations[i]); + // if ( + // !isInDevMode() && + // (parseArgumentsCache.has(cacheKey) || + // parseArgumentsGenerateSpecCache.has(cacheKey)) + // ) { + // return ( + // (parseArgumentsGenerateSpecCache.get( + // cacheKey, + // ) as ArgumentParserState) || + // (parseArgumentsCache.get(cacheKey) as ArgumentParserState) + // ); + // } + // } + + let spec: Internal.Subcommand | undefined; + let specPath: Internal.SpecLocation | undefined; + for (let i = 0; i < locations.length; i += 1) { + specPath = locations[i]; + if (isParsingHistory && specPath.type === SpecLocationSource.LOCAL) { + continue; + } + + spec = await withTimeout( + 5000, + loadSubcommandCached(specPath, context, localLogger), + ); + + if (!spec) { + const path = + specPath.type === SpecLocationSource.LOCAL ? specPath.path : ""; + localLogger.warn( + `Failed to load spec ${specPath.name} from ${specPath.type} ${path}`, + ); + } else { + // cacheKey = getCacheKey(tokenText, context, specPath); + break; + } + } + + if (!spec || !specPath) { + throw new Error("Failed loading spec"); + } + + let state: ArgumentParserState = getInitialState( + spec, + tokens[0].text, + specPath, + ); + + // let generatedSpec = false; + + const substitutedAliases = new Set(); + let aliasError: Error | undefined; + + // Returns true if we should return state immediately after calling. + const updateStateForLoadSpec = async ( + loadSpec: typeof state.completionObj.loadSpec, + index: number, + token?: string, + ) => { + const loadSpecResult = + typeof loadSpec === "function" + ? token !== undefined + ? await loadSpec(token, exec) + : undefined + : loadSpec; + + if (Array.isArray(loadSpecResult)) { + state = await parseArgumentsCached( + currentCommand, + context, + // authClient, + loadSpecResult, + isParsingHistory, + startIndex + index, + ); + state = { ...state, commandIndex: state.commandIndex + index }; + return true; + } + + if (loadSpecResult) { + state = { + ...state, + completionObj: { + ...loadSpecResult, + parserDirectives: { + ...state.completionObj.parserDirectives, + ...loadSpecResult.parserDirectives, + }, + }, + optionArgState: createArgState(), + passedOptions: [], + subcommandArgState: createArgState(loadSpecResult.args), + haveEnteredSubcommandArgs: false, + }; + } + + return false; + }; + + if (await updateStateForLoadSpec(state.completionObj.loadSpec, 0)) { + return state; + } + + for (let i = 1; i < tokens.length; i += 1) { + if (state.completionObj.generateSpec) { + state = await generateSpecForState( + state, + tokens.map((token: any) => token.text), + isParsingHistory, + localLogger, + ); + // generatedSpec = true; + } + + if (i === tokens.length - 1) { + // Don't update state for last token. + break; + } + + const token = tokens[i].text; + + const lastArgObject = getCurrentArg(getArgState(state)); + const lastArgType = preferOptionArg(state) + ? TokenType.OptionArg + : TokenType.SubcommandArg; + + const lastState = state; + + state = updateState(state, token); + localLogger.debug("Parser state update", { state }); + + const { annotations } = state; + const lastAnnotation = annotations[annotations.length - 1]; + const lastType = + lastAnnotation.type === TokenType.Composite + ? lastAnnotation.subtokens[lastAnnotation.subtokens.length - 1].type + : lastAnnotation.type; + + if ( + lastType === lastArgType && + lastArgObject?.parserDirectives?.alias && + !substitutedAliases.has(token) + ) { + const { alias } = lastArgObject.parserDirectives; + try { + const aliasValue = + typeof alias === "string" ? alias : await alias(token, exec); + try { + currentCommand = substituteAlias(command, tokens[i], aliasValue); + // tokens[...i] should be the same, but tokens[i+1...] may be different. + substitutedAliases.add(token); + tokens = currentCommand.tokens.slice(startIndex); + state = lastState; + i -= 1; + continue; + } catch (err) { + localLogger.error("Error substituting alias:", err); + throw err; + } + } catch (err) { + if (substitutedAliases.size === 0) { + throw err; + } + aliasError = err as Error; + } + } + + let loadSpec = + lastType === TokenType.Subcommand + ? state.completionObj.loadSpec + : undefined; + + // Recurse for load spec or special arg + if (lastType === lastArgType && lastArgObject) { + const { + isCommand, + isModule, + isScript, + loadSpec: argLoadSpec, + } = lastArgObject; + if (argLoadSpec) { + loadSpec = argLoadSpec; + } else if (isCommand || isScript) { + const specLocation = await getSpecPath( + token, + context.currentWorkingDirectory, + Boolean(isScript), + ); + loadSpec = [specLocation]; + } else if (isModule) { + loadSpec = [ + { + name: `${isModule}${token}`, + type: SpecLocationSource.GLOBAL, + }, + ]; + } + } + + if (await updateStateForLoadSpec(loadSpec, i, token)) { + return state; + } + + // If error with alias and corresponding arg was not used in a loadSpec, + // throw the error. + if (aliasError) { + throw aliasError; + } + + substitutedAliases.clear(); + } + + // if (generatedSpec) { + // if (tokenText[0] === "fig") figCaches.add(cacheKey); + // parseArgumentsGenerateSpecCache.set(cacheKey, state); + // } else { + // parseArgumentsCache.set(cacheKey, state); + // } + + return state; +}; + +const firstTokenSpec: Internal.Subcommand = { + name: ["firstTokenSpec"], + subcommands: {}, + options: {}, + persistentOptions: {}, + loadSpec: undefined, + args: [ + { + name: "command", + generators: [ + { + custom: async (_tokens: any, _exec: any, context: { currentProcess: string | string[]; }) => { + let result: Fig.Suggestion[] = []; + if (context?.currentProcess.includes("fish")) { + const commands = await executeLoginShell({ + command: 'complete -C ""', + executable: context.currentProcess, + }); + result = commands.split("\n").map((commandString: string | string[]) => { + const splitIndex = commandString.indexOf("\t"); + const name = commandString.slice(0, splitIndex + 1); + const description = commandString.slice(splitIndex + 1); + return { name, description, type: "subcommand" }; + }); + } else if (context?.currentProcess.includes("bash")) { + const commands = await executeLoginShell({ + command: "compgen -c", + executable: context.currentProcess, + }); + result = commands + .split("\n") + .map((name: any) => ({ name, type: "subcommand" })); + } else if (context?.currentProcess.includes("zsh")) { + const commands = await executeLoginShell({ + command: `for key in \${(k)commands}; do echo $key; done && alias +r`, + executable: context.currentProcess, + }); + result = commands + .split("\n") + .map((name: any) => ({ name, type: "subcommand" })); + } + + const names = new Set(); + return result.filter((suggestion) => { + if (names.has(suggestion.name)) { + return false; + } + names.add(suggestion.name); + return true; + }); + }, + cache: { + strategy: "stale-while-revalidate", + ttl: 10 * 1000, // 10s + }, + }, + ], + }, + ], + parserDirectives: {}, +}; + +export const parseArguments = async ( + command: Command | null, + context: Fig.ShellContext, + // authClient: AuthClient, + isParsingHistory = false, + localLogger: logger.Logger = logger, +): Promise => { + const tokens = command?.tokens ?? []; + if (!command || tokens.length === 0) { + throw new Error("Invalid token array"); + } + + if (tokens.length === 1) { + const showFirstCommandCompletion = getSetting( + SETTINGS.FIRST_COMMAND_COMPLETION, + ); + let spec = showFirstCommandCompletion + ? firstTokenSpec + : { ...firstTokenSpec, args: [] }; + let specPath = { name: "firstTokenSpec", type: SpecLocationSource.GLOBAL }; + if (tokens[0].text.includes("/")) { + // special-case: Symfony has "bin/console" which can be invoked directly + // and should not require a user to create script completions for it + if (tokens[0].text === "bin/console") { + specPath = { name: "php/bin-console", type: SpecLocationSource.GLOBAL }; + } else { + specPath = { name: "dotslash", type: SpecLocationSource.GLOBAL }; + } + spec = await loadSubcommandCached(specPath, context, localLogger); + } + return getResultFromState(getInitialState(spec, tokens[0].text, specPath)); + } + + let state = await parseArgumentsCached( + command, + context, + // authClient, + undefined, + isParsingHistory, + 0, + localLogger, + ); + + const finalToken = tokens[tokens.length - 1].text; + try { + state = updateState(state, finalToken, true); + } catch (_err) { + state = { + ...state, + annotations: [ + ...state.annotations, + { type: TokenType.None, text: finalToken }, + ], + }; + } + return getResultFromState(state); +}; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts new file mode 100644 index 0000000000000..6c77d1e2a12c8 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getVersionFromVersionedSpec } from '@fig/autocomplete-helpers'; +import { splitPath } from "@aws/amazon-q-developer-cli-shared/utils"; +import { SpecLocation } from "@aws/amazon-q-developer-cli-shared/internal"; +import { SpecFileImport, getVersionFromFullFile } from "./loadHelpers.js"; +import { importSpecFromLocation } from './loadSpec.js'; + +export const tryResolveSpecToSubcommand = async ( + spec: SpecFileImport, + location: SpecLocation, +): Promise => { + if (typeof spec.default === "function") { + // Handle versioned specs, either simple versioned or diff versioned. + const cliVersion = await getVersionFromFullFile(spec, location.name); + const subcommandOrDiffVersionInfo = await spec.default(cliVersion); + + if ("versionedSpecPath" in subcommandOrDiffVersionInfo) { + // Handle diff versioned specs. + const { versionedSpecPath, version } = subcommandOrDiffVersionInfo; + const [dirname, basename] = splitPath(versionedSpecPath); + const { specFile } = await importSpecFromLocation({ + ...location, + name: dirname.slice(0, -1), + diffVersionedFile: basename, + }); + + if ("versions" in specFile) { + const result = getVersionFromVersionedSpec( + specFile.default, + specFile.versions, + version, + ); + return result.spec; + } + + throw new Error("Invalid versioned specs file"); + } + + return subcommandOrDiffVersionInfo; + } + + return spec.default; +}; diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts new file mode 100644 index 0000000000000..40d0e7fd894c5 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts @@ -0,0 +1,239 @@ + + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { NodeType, BaseNode, createTextNode, parse } from './parser.js'; + +export type Token = { + text: string; + node: BaseNode; + originalNode: BaseNode; +}; + +export type Command = { + tokens: Token[]; + tree: BaseNode; + + originalTree: BaseNode; +}; + +export type AliasMap = Record; + +const descendantAtIndex = ( + node: BaseNode, + index: number, + type?: NodeType, +): BaseNode | null => { + if (node.startIndex <= index && index <= node.endIndex) { + const descendant = node.children + .map((child) => descendantAtIndex(child, index, type)) + .find(Boolean); + if (descendant) { + return descendant; + } + return !type || node.type === type ? node : null; + } + return null; +}; + +export const createTextToken = ( + command: Command, + index: number, + text: string, + originalNode?: BaseNode, +): Token => { + const { tree, originalTree, tokens } = command; + + let indexDiff = 0; + const tokenIndex = tokens.findIndex( + (token) => index < token.originalNode.startIndex, + ); + const token = tokens[tokenIndex]; + if (tokenIndex === 0) { + indexDiff = token.node.startIndex - token.originalNode.startIndex; + } else if (tokenIndex === -1) { + indexDiff = tree.text.length - originalTree.text.length; + } else { + indexDiff = token.node.endIndex - token.originalNode.endIndex; + } + + return { + originalNode: + originalNode || createTextNode(originalTree.text, index, text), + node: createTextNode(text, index + indexDiff, text), + text, + }; +}; + +const convertCommandNodeToCommand = (tree: BaseNode): Command => { + if (tree.type !== NodeType.Command) { + throw new Error("Cannot get tokens from non-command node"); + } + + const command = { + originalTree: tree, + tree, + tokens: tree.children.map((child) => ({ + originalNode: child, + node: child, + text: child.innerText, + })), + }; + + const { children, endIndex, text } = tree; + if ( + +(children.length === 0 || children[children.length - 1].endIndex) < + endIndex && + text.endsWith(" ") + ) { + command.tokens.push(createTextToken(command, endIndex, "")); + } + return command; +}; + +const shiftByAmount = (node: BaseNode, shift: number): BaseNode => ({ + ...node, + startIndex: node.startIndex + shift, + endIndex: node.endIndex + shift, + children: node.children.map((child) => shiftByAmount(child, shift)), +}); + +export const substituteAlias = ( + command: Command, + token: Token, + alias: string, +): Command => { + if (command.tokens.find((t) => t === token) === undefined) { + throw new Error("Token not in command"); + } + const { tree } = command; + + const preAliasChars = token.node.startIndex - tree.startIndex; + const postAliasChars = token.node.endIndex - tree.endIndex; + + const preAliasText = `${tree.text.slice(0, preAliasChars)}`; + const postAliasText = postAliasChars + ? `${tree.text.slice(postAliasChars)}` + : ""; + + const commandBuffer = `${preAliasText}${alias}${postAliasText}`; + + // Parse command and shift indices to align with original command. + const parseTree = shiftByAmount(parse(commandBuffer), tree.startIndex); + + if (parseTree.children.length !== 1) { + throw new Error("Invalid alias"); + } + + const newCommand = convertCommandNodeToCommand(parseTree.children[0]); + + const [aliasStart, aliasEnd] = [ + token.node.startIndex, + token.node.startIndex + alias.length, + ]; + + let tokenIndexDiff = 0; + let lastTokenInAlias = false; + // Map tokens from new command back to old command to attributing the correct original nodes. + const tokens = newCommand.tokens.map((newToken, index) => { + const tokenInAlias = + aliasStart < newToken.node.endIndex && + newToken.node.startIndex < aliasEnd; + tokenIndexDiff += tokenInAlias && lastTokenInAlias ? 1 : 0; + const { originalNode } = command.tokens[index - tokenIndexDiff]; + lastTokenInAlias = tokenInAlias; + return { ...newToken, originalNode }; + }); + + if (newCommand.tokens.length - command.tokens.length !== tokenIndexDiff) { + throw new Error("Error substituting alias"); + } + + return { + originalTree: command.originalTree, + tree: newCommand.tree, + tokens, + }; +}; + +export const expandCommand = ( + command: Command, + _cursorIndex: number, + aliases: AliasMap, +): Command => { + let expanded = command; + const usedAliases = new Set(); + + // Check for aliases + let [name] = expanded.tokens; + while ( + expanded.tokens.length > 1 && + name && + aliases[name.text] && + !usedAliases.has(name.text) + ) { + // Remove quotes + const aliasValue = aliases[name.text].replace(/^'(.*)'$/g, "$1"); + try { + expanded = substituteAlias(expanded, name, aliasValue); + } catch (_err) { + // TODO(refactoring): add logger again + // console.error("Error substituting alias"); + } + usedAliases.add(name.text); + [name] = expanded.tokens; + } + + return expanded; +}; + +export const getCommand = ( + buffer: string, + aliases: AliasMap, + cursorIndex?: number, +): Command | null => { + const index = cursorIndex === undefined ? buffer.length : cursorIndex; + const parseTree = parse(buffer); + const commandNode = descendantAtIndex(parseTree, index, NodeType.Command); + if (commandNode === null) { + return null; + } + const command = convertCommandNodeToCommand(commandNode); + return expandCommand(command, index, aliases); +}; + +const statements = [ + NodeType.Program, + NodeType.CompoundStatement, + NodeType.Subshell, + NodeType.Pipeline, + NodeType.List, + NodeType.Command, +]; + +export const getTopLevelCommands = (parseTree: BaseNode): Command[] => { + if (parseTree.type === NodeType.Command) { + return [convertCommandNodeToCommand(parseTree)]; + } + if (!statements.includes(parseTree.type)) { + return []; + } + const commands: Command[] = []; + for (let i = 0; i < parseTree.children.length; i += 1) { + commands.push(...getTopLevelCommands(parseTree.children[i])); + } + return commands; +}; + +export const getAllCommandsWithAlias = ( + buffer: string, + aliases: AliasMap, +): Command[] => { + const parseTree = parse(buffer); + const commands = getTopLevelCommands(parseTree); + return commands.map((command) => + expandCommand(command, command.tree.text.length, aliases), + ); +}; diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts new file mode 100644 index 0000000000000..d528abb50217d --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export * from './parser.js'; +export * from './command.js'; diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts new file mode 100644 index 0000000000000..b7c5594a3f7db --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts @@ -0,0 +1,735 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Loosely follows the following grammar: +// terminator = ";" | "&" | "&;" +// literal = string | ansi_c_string | raw_string | expansion | simple_expansion | word +// concatenation = literal literal +// command = (concatenation | literal)+ +// +// variable_name = word +// subscript = variable_name"["literal"]" +// assignment = (word | subscript)("=" | "+=")literal +// assignment_list = assignment+ command? +// +// statement = +// | "{" (statement terminator)+ "}" +// | "(" statements ")" +// | statement "||" statement +// | statement "&&" statement +// | statement "|" statement +// | statement "|&" statement +// | command +// | assignment_list +// +// statements = (statement terminator)* statement terminator? +// program = statements + +export enum NodeType { + Program = "program", + + AssignmentList = "assignment_list", + Assignment = "assignment", + VariableName = "variable_name", + Subscript = "subscript", + + CompoundStatement = "compound_statement", + Subshell = "subshell", + Command = "command", + Pipeline = "pipeline", + List = "list", + + // TODO: implement <(commands) + ProcessSubstitution = "process_substitution", + + // Primary expressions + Concatenation = "concatenation", + Word = "word", + String = "string", + Expansion = "expansion", + CommandSubstitution = "command_substitution", + + // Leaf Nodes + RawString = "raw_string", + AnsiCString = "ansi_c_string", + SimpleExpansion = "simple_expansion", + SpecialExpansion = "special_expansion", + ArithmeticExpansion = "arithmetic_expansion", +} + +export type LiteralNode = + | BaseNode + | BaseNode + | BaseNode + | BaseNode + | BaseNode + | BaseNode + | BaseNode + | BaseNode + | BaseNode + | BaseNode; + +export interface BaseNode { + text: string; + // Unquoted text in node. + innerText: string; + + startIndex: number; + endIndex: number; + + complete: boolean; + + type: Type; + children: BaseNode[]; +} + +export interface ListNode extends BaseNode { + type: NodeType.List; + operator: "||" | "&&" | "|" | "|&"; +} + +export interface AssignmentListNode extends BaseNode { + type: NodeType.AssignmentList; + children: + | [...AssignmentNode[], BaseNode] + | AssignmentNode[]; + hasCommand: boolean; +} + +export interface AssignmentNode extends BaseNode { + type: NodeType.Assignment; + operator: "=" | "+="; + name: BaseNode | SubscriptNode; + children: LiteralNode[]; +} + +export interface SubscriptNode extends BaseNode { + type: NodeType.Subscript; + name: BaseNode; + index: LiteralNode; +} + +const operators = [";", "&", "&;", "|", "|&", "&&", "||"] as const; + +type Operator = (typeof operators)[number]; + +const parseOperator = (str: string, index: number): Operator | null => { + const c = str.charAt(index); + if (["&", ";", "|"].includes(c)) { + const op = str.slice(index, index + 2); + return operators.includes(op as unknown as Operator) + ? (op as Operator) + : (c as Operator); + } + return null; +}; + +const getInnerText = (node: BaseNode): string => { + const { children, type, complete, text } = node; + if (type === NodeType.Concatenation) { + return children.reduce((current, child) => current + child.innerText, ""); + } + + const terminalChars = ( + { + [NodeType.String]: ['"', '"'], + [NodeType.RawString]: ["'", "'"], + [NodeType.AnsiCString]: ["$'", "'"], + } as Record + )[type] || ["", ""]; + + const startChars = terminalChars[0]; + const endChars = !complete ? "" : terminalChars[1]; + + let innerText = ""; + for (let i = startChars.length; i < text.length - endChars.length; i += 1) { + const c = text.charAt(i); + const isWordEscape = c === "\\" && type === NodeType.Word; + const isStringEscape = + c === "\\" && + type === NodeType.String && + '$`"\\\n'.includes(text.charAt(i + 1)); + + if (isWordEscape || isStringEscape) { + i += 1; + } + + innerText += text.charAt(i); + } + return innerText; +}; + +const createNode = ( + str: string, + partial: Partial, +): T => { + const node = { + startIndex: 0, + type: NodeType.Word, + endIndex: str.length, + text: "", + innerText: "", + complete: true, + children: [], + ...partial, + } as BaseNode as T; + const text = str.slice(node.startIndex, node.endIndex); + const innerText = getInnerText({ ...node, text }); + return { ...node, text, innerText }; +}; + +export const createTextNode = ( + str: string, + startIndex: number, + text: string, +): BaseNode => + createNode(str, { startIndex, text, endIndex: startIndex + text.length }); + +const nextWordIndex = (str: string, index: number) => { + const firstChar = str.slice(index).search(/\S/); + if (firstChar === -1) { + return -1; + } + return index + firstChar; +}; + +// Parse simple variable expansion ($foo or $$) +const parseSimpleExpansion = ( + str: string, + index: number, + terminalChars: string[], +): + | BaseNode + | BaseNode + | null => { + const node: Partial> = { + startIndex: index, + type: NodeType.SimpleExpansion, + }; + if (str.length > index + 1 && "*@?-$0_".includes(str.charAt(index + 1))) { + return createNode>(str, { + ...node, + type: NodeType.SpecialExpansion, + endIndex: index + 2, + }); + } + const terminalSymbols = ["\t", " ", "\n", "$", "\\", ...terminalChars]; + let i = index + 1; + for (; i < str.length; i += 1) { + if (terminalSymbols.includes(str.charAt(i))) { + // Parse a literal $ if last token + return i === index + 1 + ? null + : createNode>(str, { + ...node, + endIndex: i, + }); + } + } + return createNode>(str, { + ...node, + endIndex: i, + }); +}; + +// Parse command substitution $(foo) or `foo` +function parseCommandSubstitution( + str: string, + startIndex: number, + terminalChar: string, +): BaseNode { + const index = + str.charAt(startIndex) === "`" ? startIndex + 1 : startIndex + 2; + const { statements: children, terminatorIndex } = parseStatements( + str, + index, + terminalChar, + ); + const terminated = terminatorIndex !== -1; + return createNode>(str, { + startIndex, + type: NodeType.CommandSubstitution, + complete: terminated && children.length !== 0, + endIndex: terminated ? terminatorIndex + 1 : str.length, + children, + }); +} + +const parseString = parseLiteral(NodeType.String, '"', '"'); +const parseRawString = parseLiteral( + NodeType.RawString, + "'", + "'", +); +const parseExpansion = parseLiteral( + NodeType.Expansion, + "${", + "}", +); +const parseAnsiCString = parseLiteral( + NodeType.AnsiCString, + "$'", + "'", +); +const parseArithmeticExpansion = parseLiteral( + NodeType.ArithmeticExpansion, + "$((", + "))", +); + +function childAtIndex( + str: string, + index: number, + inString: boolean, + terminators: string[], +): LiteralNode | null { + const lookahead = [ + str.charAt(index), + str.charAt(index + 1), + str.charAt(index + 2), + ]; + switch (lookahead[0]) { + case "$": + if (lookahead[1] === "(") { + return lookahead[2] === "(" + ? parseArithmeticExpansion(str, index) + : parseCommandSubstitution(str, index, ")"); + } + if (lookahead[1] === "{") { + return parseExpansion(str, index); + } + if (!inString && lookahead[1] === "'") { + return parseAnsiCString(str, index); + } + return parseSimpleExpansion(str, index, terminators); + case "`": + return parseCommandSubstitution(str, index, "`"); + case "'": + return inString ? null : parseRawString(str, index); + case '"': + return inString ? null : parseString(str, index); + default: + return null; + } +} + +function parseLiteral( + type: T, + startChars: string, + endChars: string, +) { + const canHaveChildren = + type === NodeType.Expansion || type === NodeType.String; + const isString = type === NodeType.String; + return (str: string, startIndex: number): BaseNode => { + const children = []; + for (let i = startIndex + startChars.length; i < str.length; i += 1) { + const child = canHaveChildren + ? childAtIndex(str, i, isString, [endChars]) + : null; + if (child !== null) { + children.push(child); + i = child.endIndex - 1; + } else if (str.charAt(i) === "\\" && type !== NodeType.RawString) { + i += 1; + } else if (str.slice(i, i + endChars.length) === endChars) { + return createNode>(str, { + startIndex, + type, + children, + endIndex: i + endChars.length, + }); + } + } + return createNode>(str, { + startIndex, + type, + children, + complete: false, + }); + }; +} + +function parseStatements( + str: string, + index: number, + terminalChar: string, + mustTerminate = false, +): { + statements: BaseNode[]; + terminatorIndex: number; +} { + const statements = []; + + let i = index; + while (i < str.length) { + // Will only exit on EOF, terminalChar or terminator symbol (;, &, &;) + let statement = parseStatement(str, i, mustTerminate ? "" : terminalChar); + + const opIndex = nextWordIndex(str, statement.endIndex); + const reachedEnd = opIndex === -1; + if (!mustTerminate && !reachedEnd && terminalChar === str.charAt(opIndex)) { + statements.push(statement); + return { statements, terminatorIndex: opIndex }; + } + + if (reachedEnd) { + statements.push(statement); + break; + } + + const op = !reachedEnd && parseOperator(str, opIndex); + if (op) { + // Terminator symbol, ; | & | &; + i = opIndex + op.length; + const nextIndex = nextWordIndex(str, i); + statements.push(statement); + if (nextIndex !== -1 && str.charAt(nextIndex) === terminalChar) { + return { statements, terminatorIndex: nextIndex }; + } + } else { + // Missing terminator but still have tokens left. + // assignments do not require terminators + statement = createNode(str, { + ...statement, + complete: + statement.type === NodeType.AssignmentList + ? statement.complete + : false, + }); + statements.push(statement); + i = opIndex; + } + } + return { statements, terminatorIndex: -1 }; +} + +const parseConcatenationOrLiteralNode = ( + str: string, + startIndex: number, + terminalChar: string, +): { children: LiteralNode[]; endIndex: number } => { + const children: LiteralNode[] = []; + + let argumentChildren: LiteralNode[] = []; + let wordStart = -1; + + const endWord = (endIndex: number) => { + if (wordStart !== -1) { + const word = createNode>(str, { + startIndex: wordStart, + endIndex, + }); + argumentChildren.push(word); + } + wordStart = -1; + }; + + const endArgument = (endIndex: number) => { + endWord(endIndex); + let [argument] = argumentChildren; + if (argumentChildren.length > 1) { + const finalPart = argumentChildren[argumentChildren.length - 1]; + argument = createNode>(str, { + startIndex: argumentChildren[0].startIndex, + type: NodeType.Concatenation, + endIndex: finalPart.endIndex, + complete: finalPart.complete, + children: argumentChildren, + }); + } + if (argument) { + children.push(argument); + } + argumentChildren = []; + }; + + const terminators = ["&", "|", ";", "\n", "'", '"', "`"]; + if (terminalChar) { + terminators.push(terminalChar); + } + + let i = startIndex; + for (; i < str.length; i += 1) { + const c = str.charAt(i); + const op = parseOperator(str, i); + if (op !== null || c === terminalChar) { + // TODO: handle terminator like ; as first token. + break; + } + const childNode = childAtIndex(str, i, false, terminators); + if (childNode !== null) { + endWord(i); + argumentChildren.push(childNode); + i = childNode.endIndex - 1; + } else if ([" ", "\t"].includes(c)) { + endArgument(i); + } else { + if (c === "\\") { + i += 1; + } + if (wordStart === -1) { + wordStart = i; + } + } + } + + endArgument(i); + + return { children, endIndex: i }; +}; + +function parseCommand( + str: string, + idx: number, + terminalChar: string, +): BaseNode { + const startIndex = Math.max(nextWordIndex(str, idx), idx); + const { children, endIndex } = parseConcatenationOrLiteralNode( + str, + startIndex, + terminalChar, + ); + + return createNode>(str, { + startIndex, + type: NodeType.Command, + complete: children.length > 0, + // Extend command up to separator. + endIndex: children.length > 0 ? endIndex : str.length, + children, + }); +} + +const parseAssignmentNode = ( + str: string, + startIndex: number, +): AssignmentNode => { + const equalsIndex = str.indexOf("=", startIndex); + const operator = str.charAt(equalsIndex - 1) === "+" ? "+=" : "="; + const firstOperatorCharIndex = + operator === "=" ? equalsIndex : equalsIndex - 1; + const firstSquareBracketIndex = str + .slice(startIndex, firstOperatorCharIndex) + .indexOf("["); + let nameNode: SubscriptNode | BaseNode; + + const variableName = createNode>(str, { + type: NodeType.VariableName, + startIndex, + endIndex: + firstSquareBracketIndex !== -1 + ? firstSquareBracketIndex + : firstOperatorCharIndex, + }); + + if (firstSquareBracketIndex !== -1) { + const index = createNode>(str, { + type: NodeType.Word, + startIndex: firstSquareBracketIndex + 1, + endIndex: firstOperatorCharIndex - 1, + }); + nameNode = createNode(str, { + type: NodeType.Subscript, + name: variableName, + startIndex, + endIndex: index.endIndex + 1, + children: [index], + }); + } else { + nameNode = variableName; + } + + const { children, endIndex } = parseConcatenationOrLiteralNode( + str, + equalsIndex + 1, + " ", + ); + return createNode(str, { + name: nameNode, + startIndex, + endIndex, + type: NodeType.Assignment, + operator, + children, + complete: children[children.length - 1].complete, + }); +}; + +const parseAssignments = (str: string, index: number): AssignmentNode[] => { + const variables: AssignmentNode[] = []; + let lastVariableEnd = index; + while (lastVariableEnd < str.length) { + const nextTokenStart = nextWordIndex(str, lastVariableEnd); + if (/^[\w[\]]+\+?=.*/.test(str.slice(nextTokenStart))) { + const assignmentNode = parseAssignmentNode(str, nextTokenStart); + variables.push(assignmentNode); + lastVariableEnd = assignmentNode.endIndex; + } else { + return variables; + } + } + return variables; +}; + +const parseAssignmentListNodeOrCommandNode = ( + str: string, + startIndex: number, + terminalChar: string, +): AssignmentListNode | BaseNode => { + const assignments = parseAssignments(str, startIndex); + if (assignments.length > 0) { + const lastAssignment = assignments[assignments.length - 1]; + const operator = parseOperator( + str, + nextWordIndex(str, lastAssignment.endIndex), + ); + let command: BaseNode | undefined; + if ( + !operator && + lastAssignment.complete && + lastAssignment.endIndex !== str.length + ) { + command = parseCommand(str, lastAssignment.endIndex, terminalChar); + } + // if it makes sense to parse a command here do it else return the list + return createNode(str, { + type: NodeType.AssignmentList, + startIndex, + endIndex: command ? command.endIndex : lastAssignment.endIndex, + hasCommand: !!command, + children: command ? [...assignments, command] : assignments, + }); + } + return parseCommand(str, startIndex, terminalChar); +}; + +const reduceStatements = ( + str: string, + lhs: BaseNode, + rhs: BaseNode, + type: NodeType, +): BaseNode => + createNode(str, { + type, + startIndex: lhs.startIndex, + children: rhs.type === type ? [lhs, ...rhs.children] : [lhs, rhs], + endIndex: rhs.endIndex, + complete: lhs.complete && rhs.complete, + }); + +function parseStatement( + str: string, + index: number, + terminalChar: string, +): BaseNode { + let i = nextWordIndex(str, index); + i = i === -1 ? index : i; + let statement = null; + if (["{", "("].includes(str.charAt(i))) { + // Parse compound statement or subshell + const isCompound = str.charAt(i) === "{"; + const endChar = isCompound ? "}" : ")"; + + const { statements: children, terminatorIndex } = parseStatements( + str, + i + 1, + endChar, + isCompound, + ); + const hasChildren = children.length > 0; + const terminated = terminatorIndex !== -1; + let endIndex = terminatorIndex + 1; + if (!terminated) { + endIndex = hasChildren + ? children[children.length - 1].endIndex + : str.length; + } + statement = createNode(str, { + startIndex: i, + type: isCompound ? NodeType.CompoundStatement : NodeType.Subshell, + endIndex, + complete: terminated && hasChildren, + children, + }); + } else { + // statement = parseAssignmentListNodeOrCommandNode(str, i, terminalChar) + statement = parseAssignmentListNodeOrCommandNode(str, i, terminalChar); + } + + i = statement.endIndex; + const opIndex = nextWordIndex(str, i); + const op = opIndex !== -1 && parseOperator(str, opIndex); + if ( + !op || + op === ";" || + op === "&" || + op === "&;" || + (opIndex !== -1 && terminalChar && str.charAt(opIndex) === terminalChar) + ) { + return statement; + } + + // Recursively parse rightHandStatement if theres an operator. + const rightHandStatement = parseStatement( + str, + opIndex + op.length, + terminalChar, + ); + if (op === "&&" || op === "||") { + return reduceStatements(str, statement, rightHandStatement, NodeType.List); + } + + if (op === "|" || op === "|&") { + if (rightHandStatement.type === NodeType.List) { + const [oldFirstChild, ...otherChildren] = rightHandStatement.children; + const newFirstChild = reduceStatements( + str, + statement, + oldFirstChild, + NodeType.Pipeline, + ); + return createNode(str, { + type: NodeType.List, + startIndex: newFirstChild.startIndex, + children: [newFirstChild, ...otherChildren], + endIndex: rightHandStatement.endIndex, + complete: newFirstChild.complete && rightHandStatement.complete, + }); + } + return reduceStatements( + str, + statement, + rightHandStatement, + NodeType.Pipeline, + ); + } + return statement; +} + +export const printTree = (root: BaseNode) => { + const getNodeText = (node: BaseNode, level = 0) => { + const indent = " ".repeat(level); + let nodeText = `${indent}${node.type} [${node.startIndex}, ${node.endIndex}] - ${node.text}`; + const childrenText = node.children + .map((child) => getNodeText(child, level + 1)) + .join("\n"); + if (childrenText) { + nodeText += `\n${childrenText}`; + } + if (!node.complete) { + nodeText += `\n${indent}INCOMPLETE`; + } + return nodeText; + }; + console.log(getNodeText(root)); +}; + +export const parse = (str: string): BaseNode => + createNode>(str, { + startIndex: 0, + type: NodeType.Program, + children: parseStatements(str, 0, "").statements, + }); From 1b39277c5002588b5a1405239b49db3503c29d0a Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 4 Feb 2025 16:33:06 -0600 Subject: [PATCH 02/51] change tsconfig --- extensions/terminal-suggest/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index f3d3aa73975cf..67e85415712b6 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -6,8 +6,8 @@ "node" ], "target": "es2020", - "module": "CommonJS", - "moduleResolution": "node", + "module": "ES2020", + "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, From 8fe538221b3cf4fa3b4aed21fded827186ecb9d9 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 4 Feb 2025 16:36:05 -0600 Subject: [PATCH 03/51] fix errors --- .../autocomplete-parser/src/parseArguments.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts index f0b02499894c2..ba6df71f0db66 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts @@ -652,13 +652,18 @@ const generateSpecForState = async ( // newSpec = generateSpecCache.get(cacheKey)!; // } else { const exec = getExecuteShellCommandFunction(isParsingHistory); - newSpec = convertSubcommand( - await generateSpec(tokenArray, exec), - initializeDefault, - ); + const spec = await generateSpec(tokenArray, exec); + if (spec) { + newSpec = convertSubcommand( + spec, + initializeDefault, + ); + } // if (cacheKey) generateSpecCache.set(cacheKey, newSpec); // } - + if (!newSpec) { + throw new Error("Failed to generate spec"); + } const keepArgs = completionObj.args.length > 0; return { @@ -1032,21 +1037,22 @@ const firstTokenSpec: Internal.Subcommand = { { custom: async (_tokens: any, _exec: any, context: { currentProcess: string | string[]; }) => { let result: Fig.Suggestion[] = []; - if (context?.currentProcess.includes("fish")) { + + if (context?.currentProcess.includes("fish") && typeof context.currentProcess === 'string') { const commands = await executeLoginShell({ command: 'complete -C ""', - executable: context.currentProcess, + executable: Array.isArray(context.currentProcess) ? context.currentProcess[0] : context.currentProcess, }); - result = commands.split("\n").map((commandString: string | string[]) => { + result = commands.split("\n").map((commandString: string) => { const splitIndex = commandString.indexOf("\t"); const name = commandString.slice(0, splitIndex + 1); const description = commandString.slice(splitIndex + 1); - return { name, description, type: "subcommand" }; + return { name, description: description as string, type: "subcommand" }; }); } else if (context?.currentProcess.includes("bash")) { const commands = await executeLoginShell({ command: "compgen -c", - executable: context.currentProcess, + executable: Array.isArray(context.currentProcess) ? context.currentProcess[0] : context.currentProcess, }); result = commands .split("\n") @@ -1054,7 +1060,7 @@ const firstTokenSpec: Internal.Subcommand = { } else if (context?.currentProcess.includes("zsh")) { const commands = await executeLoginShell({ command: `for key in \${(k)commands}; do echo $key; done && alias +r`, - executable: context.currentProcess, + executable: Array.isArray(context.currentProcess) ? context.currentProcess[0] : context.currentProcess, }); result = commands .split("\n") From 05599251cae1e2c33e7acd6cb858ad9a73398964 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 4 Feb 2025 16:39:24 -0600 Subject: [PATCH 04/51] add log statement --- .../terminal-suggest/src/terminalSuggestMain.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 7c375d4ba7062..78673131e8fcd 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -19,6 +19,8 @@ import { getPwshGlobals } from './shell/pwsh'; import { getTokenType, TokenType } from './tokens'; import { PathExecutableCache } from './env/pathExecutableCache'; import { getFriendlyResourcePath } from './helpers/uri'; +import { parseArguments } from './fig/autocomplete-parser/src'; +import { getCommand } from './fig/shell-parser/src/command'; // TODO: remove once API is finalized export const enum TerminalShellType { @@ -102,7 +104,16 @@ export async function activate(context: vscode.ExtensionContext) { return; } const commands = [...commandsInPath.completionResources, ...shellGlobals]; - + const env: Record = {}; + if (terminal.shellIntegration?.env) { + for (const [key, value] of Object.entries(terminal.shellIntegration.env)) { + if (typeof value === 'string') { + env[key] = value; + } + } + } + const parsedArguments = await parseArguments(getCommand(terminalContext.commandLine, {}, terminalContext.cursorPosition), { environmentVariables: env, currentWorkingDirectory: terminal.shellIntegration!.cwd!.fsPath, sshPrefix: '', currentProcess: terminal.name }); + console.log(parsedArguments); const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; const tokenType = getTokenType(terminalContext, shellType); From 5c975e3df221b12bfb19d843503b417fbe1a57f2 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 4 Feb 2025 16:40:05 -0600 Subject: [PATCH 05/51] eslint, filters --- .eslint-ignore | 1 + build/filters.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/.eslint-ignore b/.eslint-ignore index 6fbdf94696e66..f93a73afa7517 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -8,6 +8,7 @@ **/extensions/html-language-features/server/src/test/pathCompletionFixtures/** **/extensions/ipynb/notebook-out/** **/extensions/markdown-language-features/media/** +**/extensions/terminal-suggest/src/fig/** **/extensions/markdown-language-features/notebook-out/** **/extensions/markdown-math/notebook-out/** **/extensions/notebook-renderers/renderer-out/index.js diff --git a/build/filters.js b/build/filters.js index 17e74c3871a55..37ea941b90e77 100644 --- a/build/filters.js +++ b/build/filters.js @@ -50,6 +50,7 @@ module.exports.unicodeFilter = [ '!extensions/notebook-renderers/renderer-out/**', '!extensions/php-language-features/src/features/phpGlobalFunctions.ts', '!extensions/terminal-suggest/src/completions/upstream/**', + '!extensions/terminal-suggest/src/fig/**', '!extensions/typescript-language-features/test-workspace/**', '!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace2/**', @@ -90,6 +91,8 @@ module.exports.indentationFilter = [ '!test/monaco/out/**', '!test/smoke/out/**', '!extensions/terminal-suggest/src/completions/upstream/**', + '!extensions/terminal-suggest/src/fig/**', + '!extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts', '!extensions/typescript-language-features/test-workspace/**', '!extensions/typescript-language-features/resources/walkthroughs/**', '!extensions/typescript-language-features/package-manager/node-maintainer/**', @@ -173,6 +176,8 @@ module.exports.copyrightFilter = [ '!extensions/ipynb/notebook-out/**', '!extensions/simple-browser/media/codicon.css', '!extensions/terminal-suggest/src/completions/upstream/**', + '!extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts', + '!extensions/terminal-suggest/src/fig/**', '!extensions/typescript-language-features/node-maintainer/**', '!extensions/html-language-features/server/src/modes/typescript/*', '!extensions/*/server/bin/*', @@ -191,6 +196,7 @@ module.exports.tsFormattingFilter = [ '!extensions/**/colorize-fixtures/**', '!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace2/**', + '!extensions/terminal-suggest/src/fig/**', '!extensions/**/*.test.ts', '!extensions/html-language-features/server/lib/jquery.d.ts', ]; @@ -200,6 +206,7 @@ module.exports.eslintFilter = [ '**/*.cjs', '**/*.mjs', '**/*.ts', + '!extensions/terminal-suggest/src/fig/**', ...readFileSync(join(__dirname, '..', '.eslint-ignore')) .toString() .split(/\r\n|\n/) From db011392ff0ca9c4b3168366fa919b9839a38927 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 5 Feb 2025 10:59:50 -0600 Subject: [PATCH 06/51] push current status --- extensions/terminal-suggest/package.json | 3 +- .../src/executeCommand.ts | 64 +++ .../src/executeCommandWrappers.ts | 107 +++++ .../src/fig/api-bindings-wrappers/src/fs.ts | 4 + .../fig/api-bindings-wrappers/src/index.ts | 5 + .../fig/api-bindings-wrappers/src/settings.ts | 71 ++++ .../fig/api-bindings-wrappers/src/state.ts | 60 +++ .../src/fig/autocomplete-parser/src/caches.ts | 25 ++ .../fig/autocomplete-parser/src/constants.ts | 48 +-- .../src/fig/autocomplete-parser/src/errors.ts | 16 + .../src/fig/autocomplete-parser/src/index.ts | 14 +- .../autocomplete-parser/src/loadHelpers.ts | 52 ++- .../fig/autocomplete-parser/src/loadSpec.ts | 38 +- .../autocomplete-parser/src/parseArguments.ts | 268 ++++++------ .../src/tryResolveSpecToSubcommand.ts | 16 +- .../src/fig/shared/src/errors.ts | 7 + .../src/fig/shared/src/fuzzysort.d.ts | 85 ++++ .../src/fig/shared/src/fuzzysort.js | 257 ++++++++++++ .../src/fig/shared/src/index.ts | 5 + .../src/fig/shared/src/internal.d.ts | 22 + .../src/fig/shared/src/internal.ts | 34 ++ .../src/fig/shared/src/settings.ts | 15 + .../src/fig/shared/src/utils.ts | 249 ++++++++++++ .../src/fig/shell-parser/src/command.ts | 383 +++++++++--------- .../src/fig/shell-parser/src/errors.ts | 4 + .../src/fig/shell-parser/src/index.ts | 8 +- .../src/fig/shell-parser/src/parser.ts | 5 - extensions/terminal-suggest/tsconfig.json | 9 +- 28 files changed, 1444 insertions(+), 430 deletions(-) create mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts create mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts create mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts create mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts create mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts create mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/caches.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/errors.ts create mode 100644 extensions/terminal-suggest/src/fig/shared/src/errors.ts create mode 100644 extensions/terminal-suggest/src/fig/shared/src/fuzzysort.d.ts create mode 100644 extensions/terminal-suggest/src/fig/shared/src/fuzzysort.js create mode 100644 extensions/terminal-suggest/src/fig/shared/src/index.ts create mode 100644 extensions/terminal-suggest/src/fig/shared/src/internal.d.ts create mode 100644 extensions/terminal-suggest/src/fig/shared/src/internal.ts create mode 100644 extensions/terminal-suggest/src/fig/shared/src/settings.ts create mode 100644 extensions/terminal-suggest/src/fig/shared/src/utils.ts create mode 100644 extensions/terminal-suggest/src/fig/shell-parser/src/errors.ts diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index d7f995cee24a4..bdd601a6c91e8 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -32,6 +32,7 @@ "url": "https://github.com/microsoft/vscode.git" }, "dependencies": { - "@withfig/autocomplete-helpers": "^0.1.0" + "@withfig/autocomplete-helpers": "^0.1.0", + "@aws/amazon-q-developer-cli-proto/fig": "^0.1.0" } } diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts new file mode 100644 index 0000000000000..684f055c896c7 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts @@ -0,0 +1,64 @@ +/** + * NOTE: this is intended to be separate because executeCommand + * will often be mocked during testing of functions that call it. + * If it gets bundled in the same file as the functions that call it + * vitest is not able to mock it (because of esm restrictions). + */ +import { withTimeout } from "../../shared/src/utils"; +import { Process } from "@aws/amazon-q-developer-cli-api-bindings"; +import logger from "loglevel"; +import { osIsWindows } from '../../../helpers/os'; + +export const cleanOutput = (output: string) => + output + .replace(/\r\n/g, "\n") // Replace carriage returns with just a normal return + // eslint-disable-next-line no-control-regex + .replace(/\x1b\[\?25h/g, "") // removes cursor character if present + .replace(/^\n+/, "") // strips new lines from start of output + .replace(/\n+$/, ""); // strips new lines from end of output + +export const executeCommandTimeout = async ( + input: Fig.ExecuteCommandInput, + timeout = osIsWindows() ? 20000 : 5000, +): Promise => { + const command = [input.command, ...input.args].join(" "); + try { + logger.info(`About to run shell command '${command}'`); + const start = performance.now(); + const result: any = await withTimeout( + Math.max(timeout, input.timeout ?? 0), + Process.run({ + executable: input.command, + args: input.args, + environment: input.env, + workingDirectory: input.cwd, + // terminalSessionId: window.globalTerminalSessionId, + //TODO@meganrogge + terminalSessionId: "test", + timeout: input.timeout, + }), + ); + const end = performance.now(); + logger.info(`Result of shell command '${command}'`, { + result, + time: end - start, + }); + + const cleanStdout = cleanOutput(result.stdout); + const cleanStderr = cleanOutput(result.stderr); + + if (result.exitCode !== 0) { + logger.warn( + `Command ${command} exited with exit code ${result.exitCode}: ${cleanStderr}`, + ); + } + return { + status: result.exitCode, + stdout: cleanStdout, + stderr: cleanStderr, + }; + } catch (err) { + logger.error(`Error running shell command '${command}'`, { err }); + throw err; + } +}; diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts new file mode 100644 index 0000000000000..c098e4c570233 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts @@ -0,0 +1,107 @@ +import { Process } from "@aws/amazon-q-developer-cli-api-bindings"; +import { withTimeout } from "@aws/amazon-q-developer-cli-shared/utils"; +import { createErrorInstance } from "@aws/amazon-q-developer-cli-shared/errors"; +import logger from "loglevel"; +import { cleanOutput, executeCommandTimeout } from "./executeCommand.js"; +import { fread } from "./fs.js"; +import { osIsWindows } from '../../../helpers/os.js'; + +export const LoginShellError = createErrorInstance("LoginShellError"); + +const DONE_SOURCING_OSC = "\u001b]697;DoneSourcing\u0007"; + +let etcShells: Promise | undefined; + +const getShellExecutable = async (shellName: string) => { + if (!etcShells) { + etcShells = fread("/etc/shells").then((shells) => + shells + .split("\n") + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")), + ); + } + + try { + return ( + (await etcShells).find((shell) => shell.includes(shellName)) ?? + ( + await executeCommandTimeout({ + command: "/usr/bin/which", + args: [shellName], + }) + ).stdout + ); + } catch (_) { + return undefined; + } +}; + +export const executeLoginShell = async ({ + command, + executable, + shell, + timeout, +}: { + command: string; + executable?: string; + shell?: string; + timeout?: number; +}): Promise => { + let exe = executable; + if (!exe) { + if (!shell) { + throw new LoginShellError("Must pass shell or executable"); + } + exe = await getShellExecutable(shell); + if (!exe) { + throw new LoginShellError(`Could not find executable for ${shell}`); + } + } + // const flags = window.fig.constants?.os === "linux" ? "-lc" : "-lic"; + //TODO@meganrogge + const flags = !osIsWindows() ? "-lc" : "-lic"; + + const process = Process.run({ + executable: exe, + args: [flags, command], + // terminalSessionId: window.globalTerminalSessionId, + //TODO@meganrogge + terminalSessionId: 'test', + timeout, + }); + + try { + // logger.info(`About to run login shell command '${command}'`, { + // separateProcess: Boolean(window.f.Process), + // shell: exe, + // }); + const start = performance.now(); + const result = await withTimeout( + timeout ?? 5000, + process.then((output: any) => { + if (output.exitCode !== 0) { + logger.warn( + `Command ${command} exited with exit code ${output.exitCode}: ${output.stderr}`, + ); + } + return cleanOutput(output.stdout); + }), + ); + const idx = + result.lastIndexOf(DONE_SOURCING_OSC) + DONE_SOURCING_OSC.length; + const trimmed = result.slice(idx); + const end = performance.now(); + logger.info(`Result of login shell command '${command}'`, { + result: trimmed, + time: end - start, + }); + return trimmed; + } catch (err) { + logger.error(`Error running login shell command '${command}'`, { err }); + throw err; + } +}; + +export const executeCommand: Fig.ExecuteCommandFunction = (args) => + executeCommandTimeout(args); diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts new file mode 100644 index 0000000000000..2181bd2687750 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts @@ -0,0 +1,4 @@ +import { fs as FileSystem } from "@aws/amazon-q-developer-cli-api-bindings"; + +export const fread = (path: string): Promise => + FileSystem.read(path).then((out) => out ?? ""); diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts new file mode 100644 index 0000000000000..506653aca0536 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts @@ -0,0 +1,5 @@ +export * from "./executeCommandWrappers.js"; +export * from "./executeCommand.js"; +export * from "./fs.js"; +export * from "./state.js"; +export * from "./settings.js"; diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts new file mode 100644 index 0000000000000..303d22c1e4486 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts @@ -0,0 +1,71 @@ +export enum SETTINGS { + // Dev settings. + DEV_MODE = "autocomplete.developerMode", + DEV_MODE_NPM = "autocomplete.developerModeNPM", + DEV_MODE_NPM_INVALIDATE_CACHE = "autocomplete.developerModeNPMInvalidateCache", + DEV_COMPLETIONS_FOLDER = "autocomplete.devCompletionsFolder", + DEV_COMPLETIONS_SERVER_PORT = "autocomplete.devCompletionsServerPort", + + // Style settings + WIDTH = "autocomplete.width", + HEIGHT = "autocomplete.height", + THEME = "autocomplete.theme", + USER_STYLES = "autocomplete.userStyles", + FONT_FAMILY = "autocomplete.fontFamily", + FONT_SIZE = "autocomplete.fontSize", + + CACHE_ALL_GENERATORS = "beta.autocomplete.auto-cache", + // Behavior settings + DISABLE_FOR_COMMANDS = "autocomplete.disableForCommands", + IMMEDIATELY_EXEC_AFTER_SPACE = "autocomplete.immediatelyExecuteAfterSpace", + IMMEDIATELY_RUN_DANGEROUS_COMMANDS = "autocomplete.immediatelyRunDangerousCommands", + IMMEDIATELY_RUN_GIT_ALIAS = "autocomplete.immediatelyRunGitAliases", + INSERT_SPACE_AUTOMATICALLY = "autocomplete.insertSpaceAutomatically", + SCROLL_WRAP_AROUND = "autocomplete.scrollWrapAround", + SORT_METHOD = "autocomplete.sortMethod", + ALWAYS_SUGGEST_CURRENT_TOKEN = "autocomplete.alwaysSuggestCurrentToken", + + NAVIGATE_TO_HISTORY = "autocomplete.navigateToHistory", + ONLY_SHOW_ON_TAB = "autocomplete.onlyShowOnTab", + ALWAYS_SHOW_DESCRIPTION = "autocomplete.alwaysShowDescription", + HIDE_PREVIEW = "autocomplete.hidePreviewWindow", + SCRIPT_TIMEOUT = "autocomplete.scriptTimeout", + PREFER_VERBOSE_SUGGESTIONS = "autocomplete.preferVerboseSuggestions", + HIDE_AUTO_EXECUTE_SUGGESTION = "autocomplete.hideAutoExecuteSuggestion", + + FUZZY_SEARCH = "autocomplete.fuzzySearch", + + PERSONAL_SHORTCUTS_TOKEN = "autocomplete.personalShortcutsToken", + + DISABLE_HISTORY_LOADING = "autocomplete.history.disableLoading", + // History settings + // one of "off", "history_only", "show" + HISTORY_MODE = "beta.history.mode", + HISTORY_COMMAND = "beta.history.customCommand", + HISTORY_MERGE_SHELLS = "beta.history.allShells", + HISTORY_CTRL_R_TOGGLE = "beta.history.ctrl-r", + + FIRST_COMMAND_COMPLETION = "autocomplete.firstTokenCompletion", + + TELEMETRY_ENABLED = "telemetry.enabled", +} + +export type SettingsMap = { [key in SETTINGS]?: unknown }; +let settings: SettingsMap = {}; + +export const updateSettings = (newSettings: SettingsMap) => { + settings = newSettings; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const getSetting = (key: SETTINGS, defaultValue?: any): T => + settings[key] ?? defaultValue; + +export const getSettings = () => settings; + +export function isInDevMode(): boolean { + return ( + Boolean(getSetting(SETTINGS.DEV_MODE)) || + Boolean(getSetting(SETTINGS.DEV_MODE_NPM)) + ); +} diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts new file mode 100644 index 0000000000000..9e4d5c80adc6e --- /dev/null +++ b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts @@ -0,0 +1,60 @@ +import { State as DefaultState } from "@aws/amazon-q-developer-cli-api-bindings"; + +export enum States { + DEVELOPER_API_HOST = "developer.apiHost", + DEVELOPER_AE_API_HOST = "developer.autocomplete-engine.apiHost", + + IS_FIG_PRO = "user.account.is-fig-pro", +} + +export type LocalStateMap = Partial< + { + [States.DEVELOPER_API_HOST]: string; + [States.DEVELOPER_AE_API_HOST]: string; + [States.IS_FIG_PRO]: boolean; + } & { [key in States]: unknown } +>; + +export type LocalStateSubscriber = { + initial?: (initialState: LocalStateMap) => void; + changes: (oldState: LocalStateMap, newState: LocalStateMap) => void; +}; + +export class State { + private static state: LocalStateMap = {}; + + private static subscribers = new Set(); + + static current = () => this.state; + + static subscribe = (subscriber: LocalStateSubscriber) => { + this.subscribers.add(subscriber); + }; + + static unsubscribe = (subscriber: LocalStateSubscriber) => { + this.subscribers.delete(subscriber); + }; + + static watch = async () => { + try { + const state = await DefaultState.current(); + this.state = state; + for (const subscriber of this.subscribers) { + subscriber.initial?.(state); + } + } catch { + // ignore errors + } + DefaultState.didChange.subscribe((notification: any) => { + const oldState = this.state; + const newState = JSON.parse( + notification.jsonBlob ?? "{}", + ) as LocalStateMap; + for (const subscriber of this.subscribers) { + subscriber.changes(oldState, newState); + } + this.state = newState; + return undefined; + }); + }; +} diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/caches.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/caches.ts new file mode 100644 index 0000000000000..b0313a3fcbfc2 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/caches.ts @@ -0,0 +1,25 @@ +import { Subcommand } from "../../shared/src/internal"; + +const allCaches: Array> = []; + +export const createCache = () => { + const cache = new Map(); + allCaches.push(cache); + return cache; +}; + +export const resetCaches = () => { + allCaches.forEach((cache) => { + cache.clear(); + }); +}; + +// window.resetCaches = resetCaches; + +export const specCache = createCache(); +export const generateSpecCache = createCache(); + +// window.listCache = () => { +// console.log(specCache); +// console.log(generateSpecCache); +// }; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts index 6353e39a452e9..197bc3c7d75d4 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts @@ -1,31 +1,27 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -const AWS_SPECS = ['aws', 'q']; +const AWS_SPECS = ["aws", "q"]; const UNIX_SPECS = [ - 'cd', - 'git', - 'rm', - 'ls', - 'cat', - 'mv', - 'ssh', - 'cp', - 'chmod', - 'source', - 'curl', - 'make', - 'mkdir', - 'man', - 'ln', - 'grep', - 'kill', + "cd", + "git", + "rm", + "ls", + "cat", + "mv", + "ssh", + "cp", + "chmod", + "source", + "curl", + "make", + "mkdir", + "man", + "ln", + "grep", + "kill", ]; -const EDITOR_SPECS = ['code', 'nano', 'vi', 'vim', 'nvim']; -const JS_SPECS = ['node', 'npm', 'npx', 'yarn']; -const MACOS_SPECS = ['brew', 'open']; -const OTHER_SPECS = ['docker', 'python']; +const EDITOR_SPECS = ["code", "nano", "vi", "vim", "nvim"]; +const JS_SPECS = ["node", "npm", "npx", "yarn"]; +const MACOS_SPECS = ["brew", "open"]; +const OTHER_SPECS = ["docker", "python"]; export const MOST_USED_SPECS = [ ...AWS_SPECS, diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/errors.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/errors.ts new file mode 100644 index 0000000000000..cfded140b5ed6 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/errors.ts @@ -0,0 +1,16 @@ +import { createErrorInstance } from '../../shared/src/errors'; + +// LoadSpecErrors +export const MissingSpecError = createErrorInstance("MissingSpecError"); +export const WrongDiffVersionedSpecError = createErrorInstance( + "WrongDiffVersionedSpecError", +); +export const DisabledSpecError = createErrorInstance("DisabledSpecError"); +export const LoadLocalSpecError = createErrorInstance("LoadLocalSpecError"); +export const SpecCDNError = createErrorInstance("SpecCDNError"); + +// ParsingErrors +export const ParsingHistoryError = createErrorInstance("ParsingHistoryError"); + +export const ParseArgumentsError = createErrorInstance("ParseArgumentsError"); +export const UpdateStateError = createErrorInstance("UpdateStateError"); diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts index adde6f9136db4..e02acac454d9d 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts @@ -1,9 +1,5 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export * from './constants.js'; -export * from './loadHelpers.js'; -export * from './loadSpec.js'; -export * from './parseArguments.js'; +export * from "./constants.js"; +export * from "./errors.js"; +export * from "./loadHelpers.js"; +export * from "./loadSpec.js"; +export * from "./parseArguments.js"; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts index 3b0917bbfdfac..7ebb4abbefddc 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts @@ -1,22 +1,19 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import logger, { Logger } from 'loglevel'; import * as semver from 'semver'; +import logger, { Logger } from 'loglevel'; + import { - ensureTrailingSlash, withTimeout, exponentialBackoff, -} from '@aws/amazon-q-developer-cli-shared/utils'; + ensureTrailingSlash, +} from "../../shared/src/utils"; import { executeCommand, fread, isInDevMode, -} from '@aws/amazon-q-developer-cli-api-bindings-wrappers'; -import z from 'zod'; -import { MOST_USED_SPECS } from './constants.js'; +} from "../../api-bindings-wrappers/src"; +import z from "zod"; +import { MOST_USED_SPECS } from "./constants.js"; +import { LoadLocalSpecError, SpecCDNError } from "./errors.js"; export type SpecFileImport = | { @@ -65,7 +62,7 @@ export async function importSpecFromFile( localLogger.info(`Loading spec from ${fullPath}`); const contents = await fread(fullPath); if (!contents) { - throw new Error(`Failed to read file: ${fullPath}`); + throw new LoadLocalSpecError(`Failed to read file: ${fullPath}`); } return contents; }; @@ -84,22 +81,22 @@ export async function importSpecFromFile( /** * Specs can only be loaded from non "secure" contexts, so we can't load from https */ -// export const canLoadSpecProtocol = () => getActiveWindow().location.protocol !== "https:"; +//TODO@meganrogge fix +export const canLoadSpecProtocol = () => true; // TODO: this is a problem for diff-versioned specs export async function importFromPublicCDN( name: string, ): Promise { - //TODO@meganrogge - // if (canLoadSpecProtocol()) { - return withTimeout( - 20000, - import( - /* @vite-ignore */ - `spec://localhost/${name}.js` - ), - ); - // } + if (canLoadSpecProtocol()) { + return withTimeout( + 20000, + import( + /* @vite-ignore */ + `spec://localhost/${name}.js` + ), + ); + } // Total of retries in the worst case should be close to previous timeout value // 500ms * 2^5 + 5 * 1000ms + 5 * 100ms = 21500ms, before the timeout was 20000ms @@ -118,14 +115,13 @@ export async function importFromPublicCDN( /**/ } - throw new Error("Unable to load from a CDN"); + throw new SpecCDNError("Unable to load from a CDN"); } async function jsonFromPublicCDN(path: string): Promise { - // if (canLoadSpecProtocol()) { - //TODO@meganrogge - return fetch(`spec://localhost/${path}.json`).then((res) => res.json()); - // } + if (canLoadSpecProtocol()) { + return fetch(`spec://localhost/${path}.json`).then((res) => res.json()); + } return exponentialBackoff( { diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts index 79cdb8c0a5fcd..0a77b973b361f 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts @@ -4,24 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import logger, { Logger } from 'loglevel'; -import { Settings } from '@aws/amazon-q-developer-cli-api-bindings'; +import * as Settings from '../../shared/src/settings'; import { convertSubcommand, initializeDefault } from '@fig/autocomplete-shared'; import { withTimeout, SpecLocationSource, splitPath, ensureTrailingSlash, -} from '@aws/amazon-q-developer-cli-shared/utils'; +} from "../../shared/src/utils"; import { Subcommand, SpecLocation, -} from '@aws/amazon-q-developer-cli-shared/internal'; +} from "../../shared/src/internal"; import { SETTINGS, getSetting, executeCommand, isInDevMode, -} from '@aws/amazon-q-developer-cli-api-bindings-wrappers'; +} from "../../api-bindings-wrappers/src"; import { importFromPublicCDN, publicSpecExists, @@ -29,8 +29,10 @@ import { importSpecFromFile, isDiffVersionedSpec, importFromLocalhost, -} from './loadHelpers.js'; -import { tryResolveSpecToSubcommand } from './tryResolveSpecToSubcommand.js'; +} from "./loadHelpers.js"; +import { DisabledSpecError, MissingSpecError } from "./errors.js"; +import { specCache } from "./caches.js"; +import { tryResolveSpecToSubcommand } from "./tryResolveSpecToSubcommand.js"; /** * This searches for the first directory containing a .fig/ folder in the parent directories @@ -40,9 +42,9 @@ const searchFigFolder = async (currentDirectory: string) => { return ensureTrailingSlash( ( await executeCommand({ - command: 'bash', + command: "bash", args: [ - '-c', + "-c", `until [[ -f .fig/autocomplete/build/_shortcuts.js ]] || [[ $PWD = $HOME ]] || [[ $PWD = "/" ]]; do cd ..; done; echo $PWD`, ], cwd: currentDirectory, @@ -193,7 +195,7 @@ export const importSpecFromLocation = async ( } if (!specFile) { - throw new Error("No spec found"); + throw new MissingSpecError("No spec found"); } return { specFile, resolvedLocation }; @@ -218,24 +220,24 @@ export const loadSubcommandCached = async ( context?: Fig.ShellContext, localLogger: Logger = logger, ): Promise => { - const { name } = specLocation; - // const path = - // specLocation.type === SpecLocationSource.LOCAL ? specLocation.path : ""; + const { name, type: source } = specLocation; + const path = + specLocation.type === SpecLocationSource.LOCAL ? specLocation.path : ""; // Do not load completion spec for commands that are 'disabled' by user const disabledSpecs = getSetting(SETTINGS.DISABLE_FOR_COMMANDS) || []; if (disabledSpecs.includes(name)) { localLogger.info(`Not getting path for disabled spec ${name}`); - throw new Error("Command requested disabled completion spec"); + throw new DisabledSpecError("Command requested disabled completion spec"); } - // const key = [source, path || "", name].join(","); + const key = [source, path || "", name].join(","); if (getSetting(SETTINGS.DEV_MODE_NPM_INVALIDATE_CACHE)) { - // specCache.clear(); + specCache.clear(); Settings.set(SETTINGS.DEV_MODE_NPM_INVALIDATE_CACHE, false); - // } else if (!getSetting(SETTINGS.DEV_MODE_NPM) && specCache.has(key)) { - // return specCache.get(key) as Subcommand; + } else if (!getSetting(SETTINGS.DEV_MODE_NPM) && specCache.has(key)) { + return specCache.get(key) as Subcommand; } const subcommand = await withTimeout( @@ -243,6 +245,6 @@ export const loadSubcommandCached = async ( loadFigSubcommand(specLocation, context, localLogger), ); const converted = convertSubcommand(subcommand, initializeDefault); - // specCache.set(key, converted); + specCache.set(key, converted); return converted; }; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts index ba6df71f0db66..6544069f70071 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts @@ -1,12 +1,7 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import logger from 'loglevel'; -import { convertSubcommand, initializeDefault } from '@fig/autocomplete-shared'; -import { filepaths, folders } from '@fig/autocomplete-generators'; -import * as Internal from "@aws/amazon-q-developer-cli-shared/internal"; +import logger from "loglevel"; +import { convertSubcommand, initializeDefault } from "@fig/autocomplete-shared"; +import { filepaths, folders } from "@fig/autocomplete-generators"; +import * as Internal from "../../shared/src/internal"; import { firstMatchingToken, makeArray, @@ -14,21 +9,29 @@ import { SuggestionFlag, SuggestionFlags, withTimeout, -} from '@aws/amazon-q-developer-cli-shared/utils'; +} from "../../shared/src/utils"; import { executeCommand, executeLoginShell, getSetting, + isInDevMode, SETTINGS, -} from '@aws/amazon-q-developer-cli-api-bindings-wrappers'; +} from "../../api-bindings-wrappers/src"; import { Command, substituteAlias, -} from '@aws/amazon-q-developer-cli-shell-parser'; +} from "../../shell-parser/src"; import { getSpecPath, loadSubcommandCached, -} from './loadSpec.js'; + serializeSpecLocation, +} from "./loadSpec.js"; +import { + ParseArgumentsError, + ParsingHistoryError, + UpdateStateError, +} from "./errors.js"; +import { createCache, generateSpecCache } from "./caches.js"; type ArgArrayState = { args: Array | null; @@ -146,7 +149,7 @@ export const flattenAnnotations = ( }; export const optionsAreEqual = (a: Internal.Option, b: Internal.Option) => - a.name.some((name: any) => b.name.includes(name)); + a.name.some((name) => b.name.includes(name)); export const countEqualOptions = ( option: Internal.Option, @@ -240,7 +243,7 @@ export const findOption = ( ): Internal.Option => { const option = spec.options[token] || spec.persistentOptions[token]; if (!option) { - throw new Error(`Option not found: ${token}`); + throw new UpdateStateError(`Option not found: ${token}`); } return option; }; @@ -251,7 +254,7 @@ export const findSubcommand = ( ): Internal.Subcommand => { const subcommand = spec.subcommands[token]; if (!subcommand) { - throw new Error("Subcommand not found"); + throw new UpdateStateError("Subcommand not found"); } return subcommand; }; @@ -263,11 +266,11 @@ const updateStateForSubcommand = ( ): ArgumentParserState => { const { completionObj, haveEnteredSubcommandArgs } = state; if (!completionObj.subcommands) { - throw new Error("No subcommands"); + throw new UpdateStateError("No subcommands"); } if (haveEnteredSubcommandArgs) { - throw new Error("Already entered subcommand args"); + throw new UpdateStateError("Already entered subcommand args"); } const newCompletionObj = findSubcommand(state.completionObj, token); @@ -316,7 +319,7 @@ const updateStateForOption = ( if (isRepeatable !== true && isRepeatable !== undefined) { const currentRepetitions = countEqualOptions(option, state.passedOptions); if (currentRepetitions >= isRepeatable) { - throw new Error( + throw new UpdateStateError( `Cannot pass option again, already passed ${currentRepetitions} times, ` + `and can only be passed ${isRepeatable} times`, ); @@ -346,7 +349,7 @@ const updateStateForOptionArg = ( isFinalToken = false, ): ArgumentParserState => { if (!getCurrentArg(state.optionArgState)) { - throw new Error("Cannot consume option arg."); + throw new UpdateStateError("Cannot consume option arg."); } const annotations: Annotation[] = [ @@ -372,7 +375,7 @@ const updateStateForSubcommandArg = ( ): ArgumentParserState => { // Consume token as subcommand arg if possible. if (!getCurrentArg(state.subcommandArgState)) { - throw new Error("Cannot consume subcommand arg."); + throw new UpdateStateError("Cannot consume subcommand arg."); } const annotations: Annotation[] = [ @@ -402,7 +405,7 @@ const updateStateForChainedOptionToken = ( // See https://stackoverflow.com/a/10818697 // Handle -- as special option flag. if (isFinalToken && ["-", "--"].includes(token)) { - throw new Error("Final token, not consuming as option"); + throw new UpdateStateError("Final token, not consuming as option"); } if (token === "--") { @@ -437,7 +440,7 @@ const updateStateForChainedOptionToken = ( const optionState = updateStateForOption(state, flag); if ((optionState.optionArgState.args?.length ?? 0) > 1) { - throw new Error( + throw new UpdateStateError( "Cannot pass argument with separator: option takes multiple args", ); } @@ -508,7 +511,7 @@ const updateStateForChainedOptionToken = ( if (optionArg) { if ((optionState.optionArgState.args?.length ?? 0) > 1) { - throw new Error( + throw new UpdateStateError( "Cannot chain option argument: option takes multiple args", ); } @@ -591,7 +594,7 @@ const getInitialState = ( }); const historyExecuteShellCommand: Fig.ExecuteCommandFunction = async () => { - throw new Error( + throw new ParsingHistoryError( "Cannot run shell command while parsing history", ); }; @@ -599,38 +602,38 @@ const historyExecuteShellCommand: Fig.ExecuteCommandFunction = async () => { const getExecuteShellCommandFunction = (isParsingHistory = false) => isParsingHistory ? historyExecuteShellCommand : executeCommand; -// const getGenerateSpecCacheKey = ( -// completionObj: Internal.Subcommand, -// tokenArray: string[], -// ): string | undefined => { -// let cacheKey: string | undefined; - -// const generateSpecCacheKey = completionObj?.generateSpecCacheKey; -// if (generateSpecCacheKey) { -// if (typeof generateSpecCacheKey === "string") { -// cacheKey = generateSpecCacheKey; -// } else if (typeof generateSpecCacheKey === "function") { -// cacheKey = generateSpecCacheKey({ -// tokens: tokenArray, -// }); -// } else { -// logger.error( -// "generateSpecCacheKey must be a string or function", -// generateSpecCacheKey, -// ); -// } -// } - -// // Return this late to ensure any generateSpecCacheKey side effects still happen -// if (isInDevMode()) { -// return undefined; -// } -// if (typeof cacheKey === "string") { -// // Prepend the spec name to the cacheKey to avoid collisions between specs. -// return `${tokenArray[0]}:${cacheKey}`; -// } -// return undefined; -// }; +const getGenerateSpecCacheKey = ( + completionObj: Internal.Subcommand, + tokenArray: string[], +): string | undefined => { + let cacheKey: string | undefined; + + const generateSpecCacheKey = completionObj?.generateSpecCacheKey; + if (generateSpecCacheKey) { + if (typeof generateSpecCacheKey === "string") { + cacheKey = generateSpecCacheKey; + } else if (typeof generateSpecCacheKey === "function") { + cacheKey = generateSpecCacheKey({ + tokens: tokenArray, + }); + } else { + logger.error( + "generateSpecCacheKey must be a string or function", + generateSpecCacheKey, + ); + } + } + + // Return this late to ensure any generateSpecCacheKey side effects still happen + if (isInDevMode()) { + return undefined; + } + if (typeof cacheKey === "string") { + // Prepend the spec name to the cacheKey to avoid collisions between specs. + return `${tokenArray[0]}:${cacheKey}`; + } + return undefined; +}; const generateSpecForState = async ( state: ArgumentParserState, @@ -646,24 +649,23 @@ const generateSpecForState = async ( } try { - // const cacheKey = getGenerateSpecCacheKey(completionObj, tokenArray); + const cacheKey = getGenerateSpecCacheKey(completionObj, tokenArray); let newSpec; - // if (cacheKey && generateSpecCache.has(cacheKey)) { - // newSpec = generateSpecCache.get(cacheKey)!; - // } else { - const exec = getExecuteShellCommandFunction(isParsingHistory); - const spec = await generateSpec(tokenArray, exec); - if (spec) { + if (cacheKey && generateSpecCache.has(cacheKey)) { + newSpec = generateSpecCache.get(cacheKey)!; + } else { + const exec = getExecuteShellCommandFunction(isParsingHistory); + const spec = await generateSpec(tokenArray, exec); + if (!spec) { + throw new UpdateStateError("generateSpec must return a spec"); + } newSpec = convertSubcommand( spec, initializeDefault, ); + if (cacheKey) generateSpecCache.set(cacheKey, newSpec); } - // if (cacheKey) generateSpecCache.set(cacheKey, newSpec); - // } - if (!newSpec) { - throw new Error("Failed to generate spec"); - } + const keepArgs = completionObj.args.length > 0; return { @@ -683,7 +685,7 @@ const generateSpecForState = async ( : createArgState(newSpec.args), }; } catch (err) { - if (!(err instanceof Error)) { + if (!(err instanceof ParsingHistoryError)) { localLogger.error( `There was an error with spec (generator owner: ${completionObj.name }, tokens: ${tokenArray.join(", ")}) generateSpec function`, @@ -755,27 +757,27 @@ export const initialParserState = getResultFromState( }), ); -// const parseArgumentsCache = createCache(); -// const parseArgumentsGenerateSpecCache = createCache(); -// const figCaches = new Set(); -// export const clearFigCaches = () => { -// for (const cache of figCaches) { -// parseArgumentsGenerateSpecCache.delete(cache); -// } -// return { unsubscribe: false }; -// }; - -// const getCacheKey = ( -// tokenArray: string[], -// context: Fig.ShellContext, -// specLocation: Internal.SpecLocation, -// ): string => -// [ -// tokenArray.slice(0, -1).join(" "), -// serializeSpecLocation(specLocation), -// context.currentWorkingDirectory, -// context.currentProcess, -// ].join(","); +const parseArgumentsCache = createCache(); +const parseArgumentsGenerateSpecCache = createCache(); +const figCaches = new Set(); +export const clearFigCaches = () => { + for (const cache of figCaches) { + parseArgumentsGenerateSpecCache.delete(cache); + } + return { unsubscribe: false }; +}; + +const getCacheKey = ( + tokenArray: string[], + context: Fig.ShellContext, + specLocation: Internal.SpecLocation, +): string => + [ + tokenArray.slice(0, -1).join(" "), + serializeSpecLocation(specLocation), + context.currentWorkingDirectory, + context.currentProcess, + ].join(","); // Parse all arguments in tokenArray. const parseArgumentsCached = async ( @@ -791,35 +793,35 @@ const parseArgumentsCached = async ( let currentCommand = command; let tokens = currentCommand.tokens.slice(startIndex); - const tokenText = tokens.map((token: any) => token.text); + const tokenText = tokens.map((token) => token.text); const locations = specLocations || [ await getSpecPath(tokenText[0], context.currentWorkingDirectory), ]; localLogger.debug({ locations }); - // let cacheKey = ""; - // for (let i = 0; i < locations.length; i += 1) { - // cacheKey = getCacheKey(tokenText, context, locations[i]); - // if ( - // !isInDevMode() && - // (parseArgumentsCache.has(cacheKey) || - // parseArgumentsGenerateSpecCache.has(cacheKey)) - // ) { - // return ( - // (parseArgumentsGenerateSpecCache.get( - // cacheKey, - // ) as ArgumentParserState) || - // (parseArgumentsCache.get(cacheKey) as ArgumentParserState) - // ); - // } - // } + let cacheKey = ""; + for (let i = 0; i < locations.length; i += 1) { + cacheKey = getCacheKey(tokenText, context, locations[i]); + if ( + !isInDevMode() && + (parseArgumentsCache.has(cacheKey) || + parseArgumentsGenerateSpecCache.has(cacheKey)) + ) { + return ( + (parseArgumentsGenerateSpecCache.get( + cacheKey, + ) as ArgumentParserState) || + (parseArgumentsCache.get(cacheKey) as ArgumentParserState) + ); + } + } let spec: Internal.Subcommand | undefined; let specPath: Internal.SpecLocation | undefined; for (let i = 0; i < locations.length; i += 1) { specPath = locations[i]; - if (isParsingHistory && specPath.type === SpecLocationSource.LOCAL) { + if (isParsingHistory && specPath?.type === SpecLocationSource.LOCAL) { continue; } @@ -827,21 +829,24 @@ const parseArgumentsCached = async ( 5000, loadSubcommandCached(specPath, context, localLogger), ); + if (!specPath) { + throw new Error("specPath is undefined"); + } if (!spec) { const path = - specPath.type === SpecLocationSource.LOCAL ? specPath.path : ""; + specPath.type === SpecLocationSource.LOCAL ? specPath?.path : ""; localLogger.warn( `Failed to load spec ${specPath.name} from ${specPath.type} ${path}`, ); } else { - // cacheKey = getCacheKey(tokenText, context, specPath); + cacheKey = getCacheKey(tokenText, context, specPath); break; } } if (!spec || !specPath) { - throw new Error("Failed loading spec"); + throw new UpdateStateError("Failed loading spec"); } let state: ArgumentParserState = getInitialState( @@ -850,7 +855,7 @@ const parseArgumentsCached = async ( specPath, ); - // let generatedSpec = false; + let generatedSpec = false; const substitutedAliases = new Set(); let aliasError: Error | undefined; @@ -909,11 +914,11 @@ const parseArgumentsCached = async ( if (state.completionObj.generateSpec) { state = await generateSpecForState( state, - tokens.map((token: any) => token.text), + tokens.map((token) => token.text), isParsingHistory, localLogger, ); - // generatedSpec = true; + generatedSpec = true; } if (i === tokens.length - 1) { @@ -1014,12 +1019,12 @@ const parseArgumentsCached = async ( substitutedAliases.clear(); } - // if (generatedSpec) { - // if (tokenText[0] === "fig") figCaches.add(cacheKey); - // parseArgumentsGenerateSpecCache.set(cacheKey, state); - // } else { - // parseArgumentsCache.set(cacheKey, state); - // } + if (generatedSpec) { + if (tokenText[0] === "fig") figCaches.add(cacheKey); + parseArgumentsGenerateSpecCache.set(cacheKey, state); + } else { + parseArgumentsCache.set(cacheKey, state); + } return state; }; @@ -1035,36 +1040,35 @@ const firstTokenSpec: Internal.Subcommand = { name: "command", generators: [ { - custom: async (_tokens: any, _exec: any, context: { currentProcess: string | string[]; }) => { + custom: async (_tokens, _exec, context) => { let result: Fig.Suggestion[] = []; - - if (context?.currentProcess.includes("fish") && typeof context.currentProcess === 'string') { + if (context?.currentProcess.includes("fish")) { const commands = await executeLoginShell({ command: 'complete -C ""', - executable: Array.isArray(context.currentProcess) ? context.currentProcess[0] : context.currentProcess, + executable: context.currentProcess, }); - result = commands.split("\n").map((commandString: string) => { + result = commands.split("\n").map((commandString) => { const splitIndex = commandString.indexOf("\t"); const name = commandString.slice(0, splitIndex + 1); const description = commandString.slice(splitIndex + 1); - return { name, description: description as string, type: "subcommand" }; + return { name, description, type: "subcommand" }; }); } else if (context?.currentProcess.includes("bash")) { const commands = await executeLoginShell({ command: "compgen -c", - executable: Array.isArray(context.currentProcess) ? context.currentProcess[0] : context.currentProcess, + executable: context.currentProcess, }); result = commands .split("\n") - .map((name: any) => ({ name, type: "subcommand" })); + .map((name) => ({ name, type: "subcommand" })); } else if (context?.currentProcess.includes("zsh")) { const commands = await executeLoginShell({ command: `for key in \${(k)commands}; do echo $key; done && alias +r`, - executable: Array.isArray(context.currentProcess) ? context.currentProcess[0] : context.currentProcess, + executable: context.currentProcess, }); result = commands .split("\n") - .map((name: any) => ({ name, type: "subcommand" })); + .map((name) => ({ name, type: "subcommand" })); } const names = new Set(); @@ -1096,7 +1100,7 @@ export const parseArguments = async ( ): Promise => { const tokens = command?.tokens ?? []; if (!command || tokens.length === 0) { - throw new Error("Invalid token array"); + throw new ParseArgumentsError("Invalid token array"); } if (tokens.length === 1) { diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts index 6c77d1e2a12c8..40bae26605fde 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts @@ -1,13 +1,9 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { getVersionFromVersionedSpec } from '@fig/autocomplete-helpers'; -import { splitPath } from "@aws/amazon-q-developer-cli-shared/utils"; -import { SpecLocation } from "@aws/amazon-q-developer-cli-shared/internal"; +import { getVersionFromVersionedSpec } from "@fig/autocomplete-helpers"; +import { splitPath } from "../../shared/src/utils"; +import { SpecLocation } from "../../shared/src/internal"; import { SpecFileImport, getVersionFromFullFile } from "./loadHelpers.js"; -import { importSpecFromLocation } from './loadSpec.js'; +import { WrongDiffVersionedSpecError } from "./errors.js"; +import { importSpecFromLocation } from "./loadSpec.js"; export const tryResolveSpecToSubcommand = async ( spec: SpecFileImport, @@ -37,7 +33,7 @@ export const tryResolveSpecToSubcommand = async ( return result.spec; } - throw new Error("Invalid versioned specs file"); + throw new WrongDiffVersionedSpecError("Invalid versioned specs file"); } return subcommandOrDiffVersionInfo; diff --git a/extensions/terminal-suggest/src/fig/shared/src/errors.ts b/extensions/terminal-suggest/src/fig/shared/src/errors.ts new file mode 100644 index 0000000000000..fea9c9f487fb8 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/errors.ts @@ -0,0 +1,7 @@ +export const createErrorInstance = (name: string) => + class extends Error { + constructor(message?: string) { + super(message); + this.name = `AmazonQ.${name}`; + } + }; diff --git a/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.d.ts b/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.d.ts new file mode 100644 index 0000000000000..bfbc377bfe1c4 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.d.ts @@ -0,0 +1,85 @@ + + export interface Result { + /** + * Higher is better + * + * 0 is a perfect match; -1000 is a bad match + */ + readonly score: number; + + /** Your original target string */ + readonly target: string; + + /** Indexes of the matching target characters */ + readonly indexes: number[]; + } + interface Results extends ReadonlyArray { + /** Total matches before limit */ + readonly total: number; + } + + interface KeyResult extends Result { + /** Your original object */ + readonly obj: T; + } + interface KeysResult extends ReadonlyArray { + /** + * Higher is better + * + * 0 is a perfect match; -1000 is a bad match + */ + readonly score: number; + + /** Your original object */ + readonly obj: T; + } + interface KeyResults extends ReadonlyArray> { + /** Total matches before limit */ + readonly total: number; + } + interface KeysResults extends ReadonlyArray> { + /** Total matches before limit */ + readonly total: number; + } + + interface Prepared { + /** Your original target string */ + readonly target: string; + } + + interface CancelablePromise extends Promise { + cancel(): void; + } + + interface Options { + /** Don't return matches worse than this (higher is faster) */ + threshold?: number; + + /** Don't return more results than this (lower is faster) */ + limit?: number; + + /** Allows a snigle transpoes (false is faster) */ + allowTypo?: boolean; + } + interface KeyOptions extends Options { + key: string | ReadonlyArray; + } + interface KeysOptions extends Options { + keys: ReadonlyArray>; + scoreFn?: (keysResult: ReadonlyArray>) => number; + } + + interface Fuzzysort { + /** + * Help the algorithm go fast by providing prepared targets instead of raw strings + */ + prepare(target: string): Prepared | undefined; + highlight( + result?: Result, + highlightOpen?: string, + highlightClose?: string, + ): string | null; + single(search: string, target: string | Prepared): Result | null; + } + + diff --git a/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.js b/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.js new file mode 100644 index 0000000000000..662278b56bbaa --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.js @@ -0,0 +1,257 @@ +/* + * + * + * + * NOTE: we copied and edited a local version of fuzzysort that only contains functions we require + * + * + * + */ + +var isNode = typeof require !== "undefined" && typeof window === "undefined"; +var preparedCache = new Map(); +var preparedSearchCache = new Map(); +var noResults = []; +noResults.total = 0; +var matchesSimple = []; +var matchesStrict = []; +function cleanup() { + preparedCache.clear(); + preparedSearchCache.clear(); + matchesSimple = []; + matchesStrict = []; +} +function isObj(x) { + return typeof x === "object"; +} // faster as a function + +/** + * WHAT: SublimeText-like Fuzzy Search + * USAGE: + * fuzzysort.single('fs', 'Fuzzy Search') // {score: -16} + * fuzzysort.single('test', 'test') // {score: 0} + * fuzzysort.single('doesnt exist', 'target') // null + * + * fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp']) + * // [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}] + * + * fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '', '') + * // Fuzzy Search + */ +export const fuzzysort = { + single: function (search, target) { + if (!search) return null; + if (!isObj(search)) search = fuzzysort.getPreparedSearch(search); + + if (!target) return null; + if (!isObj(target)) target = fuzzysort.getPrepared(target); + return fuzzysort.algorithm(search, target, search[0]); + }, + + highlight: function (result, hOpen, hClose) { + if (result === null) return null; + if (hOpen === undefined) hOpen = ""; + if (hClose === undefined) hClose = ""; + var highlighted = ""; + var matchesIndex = 0; + var opened = false; + var target = result.target; + var targetLen = target.length; + var matchesBest = result.indexes; + for (var i = 0; i < targetLen; ++i) { + var char = target[i]; + if (matchesBest[matchesIndex] === i) { + ++matchesIndex; + if (!opened) { + opened = true; + highlighted += hOpen; + } + + if (matchesIndex === matchesBest.length) { + highlighted += char + hClose + target.substr(i + 1); + break; + } + } else { + if (opened) { + opened = false; + highlighted += hClose; + } + } + highlighted += char; + } + + return highlighted; + }, + + prepare: function (target) { + if (!target) return; + return { + target: target, + _targetLowerCodes: fuzzysort.prepareLowerCodes(target), + _nextBeginningIndexes: null, + score: null, + indexes: null, + obj: null, + }; // hidden + }, + prepareSearch: function (search) { + if (!search) return; + return fuzzysort.prepareLowerCodes(search); + }, + + getPrepared: function (target) { + if (target.length > 999) return fuzzysort.prepare(target); // don't cache huge targets + var targetPrepared = preparedCache.get(target); + if (targetPrepared !== undefined) return targetPrepared; + targetPrepared = fuzzysort.prepare(target); + preparedCache.set(target, targetPrepared); + return targetPrepared; + }, + getPreparedSearch: function (search) { + if (search.length > 999) return fuzzysort.prepareSearch(search); // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search); + if (searchPrepared !== undefined) return searchPrepared; + searchPrepared = fuzzysort.prepareSearch(search); + preparedSearchCache.set(search, searchPrepared); + return searchPrepared; + }, + + algorithm: function (searchLowerCodes, prepared, searchLowerCode) { + var targetLowerCodes = prepared._targetLowerCodes; + var searchLen = searchLowerCodes.length; + var targetLen = targetLowerCodes.length; + var searchI = 0; // where we at + var targetI = 0; // where you at + var matchesSimpleLen = 0; + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for (;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI]; + if (isMatch) { + matchesSimple[matchesSimpleLen++] = targetI; + ++searchI; + if (searchI === searchLen) break; + searchLowerCode = searchLowerCodes[searchI]; + } + ++targetI; + if (targetI >= targetLen) return null; // Failed to find searchI + } + + var searchI = 0; + var successStrict = false; + var matchesStrictLen = 0; + + var nextBeginningIndexes = prepared._nextBeginningIndexes; + if (nextBeginningIndexes === null) + nextBeginningIndexes = prepared._nextBeginningIndexes = + fuzzysort.prepareNextBeginningIndexes(prepared.target); + var firstPossibleI = (targetI = + matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1]); + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + if (targetI !== targetLen) + for (;;) { + if (targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if (searchI <= 0) break; // We failed to push chars forward for a better match + + --searchI; + var lastMatch = matchesStrict[--matchesStrictLen]; + targetI = nextBeginningIndexes[lastMatch]; + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]; + if (isMatch) { + matchesStrict[matchesStrictLen++] = targetI; + ++searchI; + if (searchI === searchLen) { + successStrict = true; + break; + } + ++targetI; + } else { + targetI = nextBeginningIndexes[targetI]; + } + } + } + + { + // tally up the score & keep track of matches for highlighting later + if (successStrict) { + var matchesBest = matchesStrict; + var matchesBestLen = matchesStrictLen; + } else { + var matchesBest = matchesSimple; + var matchesBestLen = matchesSimpleLen; + } + var score = 0; + var lastTargetI = -1; + for (var i = 0; i < searchLen; ++i) { + var targetI = matchesBest[i]; + // score only goes down if they're not consecutive + if (lastTargetI !== targetI - 1) score -= targetI; + lastTargetI = targetI; + } + if (!successStrict) score *= 1000; + score -= targetLen - searchLen; + prepared.score = score; + prepared.indexes = new Array(matchesBestLen); + for (var i = matchesBestLen - 1; i >= 0; --i) + prepared.indexes[i] = matchesBest[i]; + + return prepared; + } + }, + + prepareLowerCodes: function (str) { + var strLen = str.length; + var lowerCodes = []; // new Array(strLen) sparse array is too slow + var lower = str.toLowerCase(); + for (var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i); + return lowerCodes; + }, + prepareBeginningIndexes: function (target) { + var targetLen = target.length; + var beginningIndexes = []; + var beginningIndexesLen = 0; + var wasUpper = false; + var wasAlphanum = false; + for (var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i); + var isUpper = targetCode >= 65 && targetCode <= 90; + var isAlphanum = + isUpper || + (targetCode >= 97 && targetCode <= 122) || + (targetCode >= 48 && targetCode <= 57); + var isBeginning = (isUpper && !wasUpper) || !wasAlphanum || !isAlphanum; + wasUpper = isUpper; + wasAlphanum = isAlphanum; + if (isBeginning) beginningIndexes[beginningIndexesLen++] = i; + } + return beginningIndexes; + }, + prepareNextBeginningIndexes: function (target) { + var targetLen = target.length; + var beginningIndexes = fuzzysort.prepareBeginningIndexes(target); + var nextBeginningIndexes = []; // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0]; + var lastIsBeginningI = 0; + for (var i = 0; i < targetLen; ++i) { + if (lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning; + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI]; + nextBeginningIndexes[i] = + lastIsBeginning === undefined ? targetLen : lastIsBeginning; + } + } + return nextBeginningIndexes; + }, + + cleanup: cleanup, +}; + +export default fuzzysort; diff --git a/extensions/terminal-suggest/src/fig/shared/src/index.ts b/extensions/terminal-suggest/src/fig/shared/src/index.ts new file mode 100644 index 0000000000000..f3bf112f7da0c --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/index.ts @@ -0,0 +1,5 @@ +import * as Errors from "./errors.js"; +import * as Internal from "./internal.js"; +import * as Utils from "./utils.js"; + +export { Errors, Internal, Utils }; diff --git a/extensions/terminal-suggest/src/fig/shared/src/internal.d.ts b/extensions/terminal-suggest/src/fig/shared/src/internal.d.ts new file mode 100644 index 0000000000000..92a0cbdf21ce2 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/internal.d.ts @@ -0,0 +1,22 @@ +import { Internal, Metadata } from "@fig/autocomplete-shared"; +import { Result } from "../../shared/src/fuzzysort"; +export type SpecLocation = Fig.SpecLocation & { + diffVersionedFile?: string; + privateNamespaceId?: number; +}; +type Override = Omit & S; +export type SuggestionType = Fig.SuggestionType | "history" | "auto-execute"; +export type Suggestion = Override string; + fuzzyMatchData?: (Result | null)[]; + originalType?: SuggestionType; +}>; +export type Arg = Metadata.ArgMeta; +export type Option = Internal.Option; +export type Subcommand = Internal.Subcommand; +export { }; diff --git a/extensions/terminal-suggest/src/fig/shared/src/internal.ts b/extensions/terminal-suggest/src/fig/shared/src/internal.ts new file mode 100644 index 0000000000000..d684bbe34dbb9 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/internal.ts @@ -0,0 +1,34 @@ +import { Internal, Metadata } from "@fig/autocomplete-shared"; +import type { Result } from "./fuzzysort"; + +export type SpecLocation = Fig.SpecLocation & { + diffVersionedFile?: string; +}; + +type Override = Omit & S; +export type SuggestionType = Fig.SuggestionType | "history" | "auto-execute"; +export type Suggestion = Override< + Fig.Suggestion, + { + type?: SuggestionType; + // Whether or not to add a space after suggestion, e.g. if user completes a + // subcommand that takes a mandatory arg. + shouldAddSpace?: boolean; + // Whether or not to add a separator after suggestion, e.g. for options with requiresSeparator + separatorToAdd?: string; + args?: ArgT[]; + // Generator information to determine whether suggestion should be filtered. + generator?: Fig.Generator; + getQueryTerm?: (x: string) => string; + fuzzyMatchData?: (Result | null)[]; + originalType?: SuggestionType; + } +>; + +export type Arg = Metadata.ArgMeta; +export type Option = Internal.Option; +export type Subcommand = Internal.Subcommand< + Metadata.ArgMeta, + Metadata.OptionMeta, + Metadata.SubcommandMeta +>; diff --git a/extensions/terminal-suggest/src/fig/shared/src/settings.ts b/extensions/terminal-suggest/src/fig/shared/src/settings.ts new file mode 100644 index 0000000000000..d56048e9fc55c --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/settings.ts @@ -0,0 +1,15 @@ +import { SettingsChangedNotification } from "@aws/amazon-q-developer-cli-proto/fig"; +export declare const didChange: { + // subscribe(handler: (notification: SettingsChangedNotification) => NotificationResponse | undefined): Promise | undefined; + //TODO@meganrogge + subscribe(handler: (notification: SettingsChangedNotification) => NotificationResponse | undefined): Promise | undefined; +}; +export declare function get(key: string): Promise; +export declare function set(key: string, value: unknown): Promise; +export declare function remove(key: string): Promise; +export declare function current(): Promise; + +export type NotificationResponse = { + unsubscribe: boolean; + }; + diff --git a/extensions/terminal-suggest/src/fig/shared/src/utils.ts b/extensions/terminal-suggest/src/fig/shared/src/utils.ts new file mode 100644 index 0000000000000..b6bd7b671aa4e --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/src/utils.ts @@ -0,0 +1,249 @@ +import { osIsWindows } from '../../../helpers/os.js'; +import { createErrorInstance } from "./errors.js"; + +// Use bitwise representation of suggestion flags. +// See here: https://stackoverflow.com/questions/39359740/what-are-enum-flags-in-typescript/ +// +// Given a number `flags` we can test `if (flags & Subcommands)` to see if we +// should be suggesting subcommands. +// +// This is more maintainable in the future if we add more options (e.g. if we +// distinguish between subcommand args and option args) as we can just add a +// number here instead of passing 3+ boolean flags everywhere. +export enum SuggestionFlag { + None = 0, + Subcommands = 1 << 0, + Options = 1 << 1, + Args = 1 << 2, + Any = (1 << 2) | (1 << 1) | (1 << 0), +} + +// Combination of suggestion flags. +export type SuggestionFlags = number; + +export enum SpecLocationSource { + GLOBAL = "global", + LOCAL = "local", +} + +export function makeArray(object: T | T[]): T[] { + return Array.isArray(object) ? object : [object]; +} + +export function firstMatchingToken( + str: string, + chars: Set, +): string | undefined { + for (const char of str) { + if (chars.has(char)) return char; + } + return undefined; +} + +export function makeArrayIfExists( + obj: T | T[] | null | undefined, +): T[] | null { + return !obj ? null : makeArray(obj); +} + +export function isOrHasValue( + obj: string | Array, + valueToMatch: string, +) { + return Array.isArray(obj) ? obj.includes(valueToMatch) : obj === valueToMatch; +} + +export const TimeoutError = createErrorInstance("TimeoutError"); + +export async function withTimeout( + time: number, + promise: Promise, +): Promise { + return Promise.race>([ + promise, + new Promise((_, reject) => { + setTimeout(() => { + reject(new TimeoutError("Function timed out")); + }, time); + }), + ]); +} + +export const longestCommonPrefix = (strings: string[]): string => { + const sorted = strings.sort(); + + const { 0: firstItem, [sorted.length - 1]: lastItem } = sorted; + const firstItemLength = firstItem.length; + + let i = 0; + + while (i < firstItemLength && firstItem.charAt(i) === lastItem.charAt(i)) { + i += 1; + } + + return firstItem.slice(0, i); +}; + +export function findLast( + values: T[], + predicate: (v: T) => boolean, +): T | undefined { + for (let i = values.length - 1; i >= 0; i -= 1) { + if (predicate(values[i])) return values[i]; + } + return undefined; +} + +type NamedObject = + | { + name?: string[] | string; + } + | string; + +export function compareNamedObjectsAlphabetically< + A extends NamedObject, + B extends NamedObject, +>(a: A, b: B): number { + const getName = (object: NamedObject): string => + typeof object === "string" ? object : makeArray(object.name)[0] || ""; + return getName(a).localeCompare(getName(b)); +} + +export const sleep = (ms: number): Promise => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +export type Func = (...args: S) => T; +type EqualFunc = (args: T, newArgs: T) => boolean; + +// Memoize a function (cache the most recent result based on the most recent args) +// Optionally can pass an equals function to determine whether or not the old arguments +// and new arguments are equal. +// +// e.g. let fn = (a, b) => a * 2 +// +// If we memoize this then we recompute every time a or b changes. if we memoize with +// isEqual = ([a, b], [newA, newB]) => newA === a +// then we will only recompute when a changes. +export function memoizeOne( + fn: Func, + isEqual?: EqualFunc, +): Func { + let lastArgs = [] as unknown[] as S; + let lastResult: T; + let hasBeenCalled = false; + const areArgsEqual: EqualFunc = + isEqual || ((args, newArgs) => args.every((x, idx) => x === newArgs[idx])); + return (...args: S): T => { + if (!hasBeenCalled || !areArgsEqual(lastArgs, args)) { + hasBeenCalled = true; + lastArgs = [...args] as unknown[] as S; + lastResult = fn(...args); + } + return lastResult; + }; +} + +function isNonNullObj(v: unknown): v is Record { + return typeof v === "object" && v !== null; +} + +function isEmptyObject(v: unknown): v is Record { + return isNonNullObj(v) && Object.keys(v).length === 0; +} + +// TODO: to fix this we may want to have the default fields as Object.keys(A) +/** + * If no fields are specified and A,B are not equal primitives/empty objects, this returns false + * even if the objects are actually equal. + */ +export function fieldsAreEqual(A: T, B: T, fields: (keyof T)[]): boolean { + if (A === B || (isEmptyObject(A) && isEmptyObject(B))) return true; + if (!fields.length || !A || !B) return false; + return fields.every((field) => { + const aField = A[field]; + const bField = B[field]; + + if (typeof aField !== typeof bField) return false; + if (isNonNullObj(aField) && isNonNullObj(bField)) { + if (Object.keys(aField).length !== Object.keys(bField).length) { + return false; + } + return fieldsAreEqual(aField, bField, Object.keys(aField) as never[]); + } + return aField === bField; + }); +} + +export const splitPath = (path: string): [string, string] => { + const idx = path.lastIndexOf("/") + 1; + return [path.slice(0, idx), path.slice(idx)]; +}; + +export const ensureTrailingSlash = (str: string) => + str.endsWith("/") ? str : `${str}/`; + +// Outputs CWD with trailing `/` +export const getCWDForFilesAndFolders = ( + cwd: string | null, + searchTerm: string, +): string => { + if (cwd === null) return "/"; + const [dirname] = splitPath(searchTerm); + + if (dirname === "") { + return ensureTrailingSlash(cwd); + } + + return dirname.startsWith("~/") || dirname.startsWith("/") + ? dirname + : `${cwd}/${dirname}`; +}; + +export function localProtocol(domain: string, path: string) { + let modifiedDomain; + //TODO@meganrogge + // if (domain === "path" && !window.fig?.constants?.newUriFormat) { + if (domain === "path") { + modifiedDomain = ""; + } else { + modifiedDomain = domain; + } + + if (osIsWindows()) { + return `https://fig.${modifiedDomain}/${path}`; + } + return `fig://${modifiedDomain}/${path}`; +} + +type ExponentialBackoffOptions = { + attemptTimeout: number; // The maximum time in milliseconds to wait for a function to execute. + baseDelay: number; // The initial delay in milliseconds. + maxRetries: number; // The maximum number of retries. + jitter: number; // A random factor to add to the delay on each retry. +}; + +export async function exponentialBackoff( + options: ExponentialBackoffOptions, + fn: () => Promise, +): Promise { + let retries = 0; + let delay = options.baseDelay; + + while (retries < options.maxRetries) { + try { + return await withTimeout(options.attemptTimeout, fn()); + } catch (_error) { + retries += 1; + delay *= 2; + delay += Math.floor(Math.random() * options.jitter); + + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); + } + } + + throw new Error("Failed to execute function after all retries."); +} diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts index 40d0e7fd894c5..94268512855de 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts @@ -1,239 +1,236 @@ +import { NodeType, BaseNode, createTextNode, parse } from "./parser.js"; +import { ConvertCommandError, SubstituteAliasError } from "./errors.js"; - -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { NodeType, BaseNode, createTextNode, parse } from './parser.js'; +export * from "./errors.js"; export type Token = { - text: string; - node: BaseNode; - originalNode: BaseNode; + text: string; + node: BaseNode; + originalNode: BaseNode; }; export type Command = { - tokens: Token[]; - tree: BaseNode; + tokens: Token[]; + tree: BaseNode; - originalTree: BaseNode; + originalTree: BaseNode; }; export type AliasMap = Record; const descendantAtIndex = ( - node: BaseNode, - index: number, - type?: NodeType, + node: BaseNode, + index: number, + type?: NodeType, ): BaseNode | null => { - if (node.startIndex <= index && index <= node.endIndex) { - const descendant = node.children - .map((child) => descendantAtIndex(child, index, type)) - .find(Boolean); - if (descendant) { - return descendant; - } - return !type || node.type === type ? node : null; - } - return null; + if (node.startIndex <= index && index <= node.endIndex) { + const descendant = node.children + .map((child) => descendantAtIndex(child, index, type)) + .find(Boolean); + if (descendant) { + return descendant; + } + return !type || node.type === type ? node : null; + } + return null; }; export const createTextToken = ( - command: Command, - index: number, - text: string, - originalNode?: BaseNode, + command: Command, + index: number, + text: string, + originalNode?: BaseNode, ): Token => { - const { tree, originalTree, tokens } = command; - - let indexDiff = 0; - const tokenIndex = tokens.findIndex( - (token) => index < token.originalNode.startIndex, - ); - const token = tokens[tokenIndex]; - if (tokenIndex === 0) { - indexDiff = token.node.startIndex - token.originalNode.startIndex; - } else if (tokenIndex === -1) { - indexDiff = tree.text.length - originalTree.text.length; - } else { - indexDiff = token.node.endIndex - token.originalNode.endIndex; - } - - return { - originalNode: - originalNode || createTextNode(originalTree.text, index, text), - node: createTextNode(text, index + indexDiff, text), - text, - }; + const { tree, originalTree, tokens } = command; + + let indexDiff = 0; + const tokenIndex = tokens.findIndex( + (token) => index < token.originalNode.startIndex, + ); + const token = tokens[tokenIndex]; + if (tokenIndex === 0) { + indexDiff = token.node.startIndex - token.originalNode.startIndex; + } else if (tokenIndex === -1) { + indexDiff = tree.text.length - originalTree.text.length; + } else { + indexDiff = token.node.endIndex - token.originalNode.endIndex; + } + + return { + originalNode: + originalNode || createTextNode(originalTree.text, index, text), + node: createTextNode(text, index + indexDiff, text), + text, + }; }; const convertCommandNodeToCommand = (tree: BaseNode): Command => { - if (tree.type !== NodeType.Command) { - throw new Error("Cannot get tokens from non-command node"); - } - - const command = { - originalTree: tree, - tree, - tokens: tree.children.map((child) => ({ - originalNode: child, - node: child, - text: child.innerText, - })), - }; - - const { children, endIndex, text } = tree; - if ( - +(children.length === 0 || children[children.length - 1].endIndex) < - endIndex && - text.endsWith(" ") - ) { - command.tokens.push(createTextToken(command, endIndex, "")); - } - return command; + if (tree.type !== NodeType.Command) { + throw new ConvertCommandError("Cannot get tokens from non-command node"); + } + + const command = { + originalTree: tree, + tree, + tokens: tree.children.map((child) => ({ + originalNode: child, + node: child, + text: child.innerText, + })), + }; + + const { children, endIndex, text } = tree; + if ( + +(children.length === 0 || children[children.length - 1].endIndex) < + endIndex && + text.endsWith(" ") + ) { + command.tokens.push(createTextToken(command, endIndex, "")); + } + return command; }; const shiftByAmount = (node: BaseNode, shift: number): BaseNode => ({ - ...node, - startIndex: node.startIndex + shift, - endIndex: node.endIndex + shift, - children: node.children.map((child) => shiftByAmount(child, shift)), + ...node, + startIndex: node.startIndex + shift, + endIndex: node.endIndex + shift, + children: node.children.map((child) => shiftByAmount(child, shift)), }); export const substituteAlias = ( - command: Command, - token: Token, - alias: string, + command: Command, + token: Token, + alias: string, ): Command => { - if (command.tokens.find((t) => t === token) === undefined) { - throw new Error("Token not in command"); - } - const { tree } = command; - - const preAliasChars = token.node.startIndex - tree.startIndex; - const postAliasChars = token.node.endIndex - tree.endIndex; - - const preAliasText = `${tree.text.slice(0, preAliasChars)}`; - const postAliasText = postAliasChars - ? `${tree.text.slice(postAliasChars)}` - : ""; - - const commandBuffer = `${preAliasText}${alias}${postAliasText}`; - - // Parse command and shift indices to align with original command. - const parseTree = shiftByAmount(parse(commandBuffer), tree.startIndex); - - if (parseTree.children.length !== 1) { - throw new Error("Invalid alias"); - } - - const newCommand = convertCommandNodeToCommand(parseTree.children[0]); - - const [aliasStart, aliasEnd] = [ - token.node.startIndex, - token.node.startIndex + alias.length, - ]; - - let tokenIndexDiff = 0; - let lastTokenInAlias = false; - // Map tokens from new command back to old command to attributing the correct original nodes. - const tokens = newCommand.tokens.map((newToken, index) => { - const tokenInAlias = - aliasStart < newToken.node.endIndex && - newToken.node.startIndex < aliasEnd; - tokenIndexDiff += tokenInAlias && lastTokenInAlias ? 1 : 0; - const { originalNode } = command.tokens[index - tokenIndexDiff]; - lastTokenInAlias = tokenInAlias; - return { ...newToken, originalNode }; - }); - - if (newCommand.tokens.length - command.tokens.length !== tokenIndexDiff) { - throw new Error("Error substituting alias"); - } - - return { - originalTree: command.originalTree, - tree: newCommand.tree, - tokens, - }; + if (command.tokens.find((t) => t === token) === undefined) { + throw new SubstituteAliasError("Token not in command"); + } + const { tree } = command; + + const preAliasChars = token.node.startIndex - tree.startIndex; + const postAliasChars = token.node.endIndex - tree.endIndex; + + const preAliasText = `${tree.text.slice(0, preAliasChars)}`; + const postAliasText = postAliasChars + ? `${tree.text.slice(postAliasChars)}` + : ""; + + const commandBuffer = `${preAliasText}${alias}${postAliasText}`; + + // Parse command and shift indices to align with original command. + const parseTree = shiftByAmount(parse(commandBuffer), tree.startIndex); + + if (parseTree.children.length !== 1) { + throw new SubstituteAliasError("Invalid alias"); + } + + const newCommand = convertCommandNodeToCommand(parseTree.children[0]); + + const [aliasStart, aliasEnd] = [ + token.node.startIndex, + token.node.startIndex + alias.length, + ]; + + let tokenIndexDiff = 0; + let lastTokenInAlias = false; + // Map tokens from new command back to old command to attributing the correct original nodes. + const tokens = newCommand.tokens.map((newToken, index) => { + const tokenInAlias = + aliasStart < newToken.node.endIndex && + newToken.node.startIndex < aliasEnd; + tokenIndexDiff += tokenInAlias && lastTokenInAlias ? 1 : 0; + const { originalNode } = command.tokens[index - tokenIndexDiff]; + lastTokenInAlias = tokenInAlias; + return { ...newToken, originalNode }; + }); + + if (newCommand.tokens.length - command.tokens.length !== tokenIndexDiff) { + throw new SubstituteAliasError("Error substituting alias"); + } + + return { + originalTree: command.originalTree, + tree: newCommand.tree, + tokens, + }; }; export const expandCommand = ( - command: Command, - _cursorIndex: number, - aliases: AliasMap, + command: Command, + _cursorIndex: number, + aliases: AliasMap, ): Command => { - let expanded = command; - const usedAliases = new Set(); - - // Check for aliases - let [name] = expanded.tokens; - while ( - expanded.tokens.length > 1 && - name && - aliases[name.text] && - !usedAliases.has(name.text) - ) { - // Remove quotes - const aliasValue = aliases[name.text].replace(/^'(.*)'$/g, "$1"); - try { - expanded = substituteAlias(expanded, name, aliasValue); - } catch (_err) { - // TODO(refactoring): add logger again - // console.error("Error substituting alias"); - } - usedAliases.add(name.text); - [name] = expanded.tokens; - } - - return expanded; + let expanded = command; + const usedAliases = new Set(); + + // Check for aliases + let [name] = expanded.tokens; + while ( + expanded.tokens.length > 1 && + name && + aliases[name.text] && + !usedAliases.has(name.text) + ) { + // Remove quotes + const aliasValue = aliases[name.text].replace(/^'(.*)'$/g, "$1"); + try { + expanded = substituteAlias(expanded, name, aliasValue); + } catch (_err) { + // TODO(refactoring): add logger again + // console.error("Error substituting alias"); + } + usedAliases.add(name.text); + [name] = expanded.tokens; + } + + return expanded; }; export const getCommand = ( - buffer: string, - aliases: AliasMap, - cursorIndex?: number, + buffer: string, + aliases: AliasMap, + cursorIndex?: number, ): Command | null => { - const index = cursorIndex === undefined ? buffer.length : cursorIndex; - const parseTree = parse(buffer); - const commandNode = descendantAtIndex(parseTree, index, NodeType.Command); - if (commandNode === null) { - return null; - } - const command = convertCommandNodeToCommand(commandNode); - return expandCommand(command, index, aliases); + const index = cursorIndex === undefined ? buffer.length : cursorIndex; + const parseTree = parse(buffer); + const commandNode = descendantAtIndex(parseTree, index, NodeType.Command); + if (commandNode === null) { + return null; + } + const command = convertCommandNodeToCommand(commandNode); + return expandCommand(command, index, aliases); }; const statements = [ - NodeType.Program, - NodeType.CompoundStatement, - NodeType.Subshell, - NodeType.Pipeline, - NodeType.List, - NodeType.Command, + NodeType.Program, + NodeType.CompoundStatement, + NodeType.Subshell, + NodeType.Pipeline, + NodeType.List, + NodeType.Command, ]; export const getTopLevelCommands = (parseTree: BaseNode): Command[] => { - if (parseTree.type === NodeType.Command) { - return [convertCommandNodeToCommand(parseTree)]; - } - if (!statements.includes(parseTree.type)) { - return []; - } - const commands: Command[] = []; - for (let i = 0; i < parseTree.children.length; i += 1) { - commands.push(...getTopLevelCommands(parseTree.children[i])); - } - return commands; + if (parseTree.type === NodeType.Command) { + return [convertCommandNodeToCommand(parseTree)]; + } + if (!statements.includes(parseTree.type)) { + return []; + } + const commands: Command[] = []; + for (let i = 0; i < parseTree.children.length; i += 1) { + commands.push(...getTopLevelCommands(parseTree.children[i])); + } + return commands; }; export const getAllCommandsWithAlias = ( - buffer: string, - aliases: AliasMap, + buffer: string, + aliases: AliasMap, ): Command[] => { - const parseTree = parse(buffer); - const commands = getTopLevelCommands(parseTree); - return commands.map((command) => - expandCommand(command, command.tree.text.length, aliases), - ); + const parseTree = parse(buffer); + const commands = getTopLevelCommands(parseTree); + return commands.map((command) => + expandCommand(command, command.tree.text.length, aliases), + ); }; diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/errors.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/errors.ts new file mode 100644 index 0000000000000..c1751c11a205c --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shell-parser/src/errors.ts @@ -0,0 +1,4 @@ +import { createErrorInstance } from '../../shared/src/errors'; + +export const SubstituteAliasError = createErrorInstance("SubstituteAliasError"); +export const ConvertCommandError = createErrorInstance("ConvertCommandError"); diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts index d528abb50217d..fe78833e92f9c 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts @@ -1,6 +1,2 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -export * from './parser.js'; -export * from './command.js'; +export * from "./parser.js"; +export * from "./command.js"; diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts index b7c5594a3f7db..e75b058fb320b 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts @@ -1,8 +1,3 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - // Loosely follows the following grammar: // terminator = ";" | "&" | "&;" // literal = string | ansi_c_string | raw_string | expansion | simple_expansion | word diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index 67e85415712b6..ac8998b8d29de 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -7,10 +7,15 @@ ], "target": "es2020", "module": "ES2020", - "moduleResolution": "bundler", + "moduleResolution": "node", "strict": true, "esModuleInterop": true, - "skipLibCheck": true, + // "skipLibCheck": true, + "lib": [ + "es2018", + "DOM", + "DOM.Iterable" + ], // Needed to suppress warnings in upstream completions "noImplicitReturns": false, From 5dbd5b91dac8b2a10fd3040c2a0a658ff5379984 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 5 Feb 2025 13:35:56 -0600 Subject: [PATCH 07/51] fix things so there are no errors Co-authored-by: Daniel Imms --- extensions/terminal-suggest/package.json | 5 - .../src/executeCommand.ts | 64 -- .../src/executeCommandWrappers.ts | 107 ---- .../src/fig/api-bindings-wrappers/src/fs.ts | 4 - .../fig/api-bindings-wrappers/src/index.ts | 5 - .../fig/api-bindings-wrappers/src/settings.ts | 71 -- .../fig/api-bindings-wrappers/src/state.ts | 60 -- .../autocomplete-helpers/src/versions.d.ts | 7 + .../src/fig/autocomplete-parser/src/index.ts | 5 - .../autocomplete-parser/src/loadHelpers.ts | 248 ------- .../fig/autocomplete-parser/src/loadSpec.ts | 250 -------- .../autocomplete-parser/src/parseArguments.ts | 605 +++++++++--------- .../src/tryResolveSpecToSubcommand.ts | 43 -- .../fig/autocomplete-shared/src/convert.ts | 73 +++ .../src/fig/autocomplete-shared/src/index.ts | 21 + .../src/fig/autocomplete-shared/src/mixins.ts | 147 +++++ .../src/fig/autocomplete-shared/src/revert.ts | 41 ++ .../autocomplete-shared/src/specMetadata.ts | 105 +++ .../src/fig/autocomplete-shared/src/utils.ts | 8 + .../src/fig/shared/src/internal.d.ts | 22 - .../src/fig/shared/src/internal.ts | 2 +- .../src/fig/shared/src/settings.ts | 15 - .../src/terminalSuggestMain.ts | 2 +- 23 files changed, 717 insertions(+), 1193 deletions(-) delete mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts delete mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts delete mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts delete mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts delete mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts delete mode 100644 extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-helpers/src/versions.d.ts delete mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts delete mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts delete mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts delete mode 100644 extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-shared/src/convert.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-shared/src/index.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-shared/src/mixins.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-shared/src/revert.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-shared/src/specMetadata.ts create mode 100644 extensions/terminal-suggest/src/fig/autocomplete-shared/src/utils.ts delete mode 100644 extensions/terminal-suggest/src/fig/shared/src/internal.d.ts delete mode 100644 extensions/terminal-suggest/src/fig/shared/src/settings.ts diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index bdd601a6c91e8..e43312fb76c9a 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -22,7 +22,6 @@ "compile": "npx gulp compile-extension:terminal-suggest", "watch": "npx gulp watch-extension:terminal-suggest" }, - "main": "./out/terminalSuggestMain", "activationEvents": [ "onTerminalCompletionsRequested" @@ -30,9 +29,5 @@ "repository": { "type": "git", "url": "https://github.com/microsoft/vscode.git" - }, - "dependencies": { - "@withfig/autocomplete-helpers": "^0.1.0", - "@aws/amazon-q-developer-cli-proto/fig": "^0.1.0" } } diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts deleted file mode 100644 index 684f055c896c7..0000000000000 --- a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommand.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * NOTE: this is intended to be separate because executeCommand - * will often be mocked during testing of functions that call it. - * If it gets bundled in the same file as the functions that call it - * vitest is not able to mock it (because of esm restrictions). - */ -import { withTimeout } from "../../shared/src/utils"; -import { Process } from "@aws/amazon-q-developer-cli-api-bindings"; -import logger from "loglevel"; -import { osIsWindows } from '../../../helpers/os'; - -export const cleanOutput = (output: string) => - output - .replace(/\r\n/g, "\n") // Replace carriage returns with just a normal return - // eslint-disable-next-line no-control-regex - .replace(/\x1b\[\?25h/g, "") // removes cursor character if present - .replace(/^\n+/, "") // strips new lines from start of output - .replace(/\n+$/, ""); // strips new lines from end of output - -export const executeCommandTimeout = async ( - input: Fig.ExecuteCommandInput, - timeout = osIsWindows() ? 20000 : 5000, -): Promise => { - const command = [input.command, ...input.args].join(" "); - try { - logger.info(`About to run shell command '${command}'`); - const start = performance.now(); - const result: any = await withTimeout( - Math.max(timeout, input.timeout ?? 0), - Process.run({ - executable: input.command, - args: input.args, - environment: input.env, - workingDirectory: input.cwd, - // terminalSessionId: window.globalTerminalSessionId, - //TODO@meganrogge - terminalSessionId: "test", - timeout: input.timeout, - }), - ); - const end = performance.now(); - logger.info(`Result of shell command '${command}'`, { - result, - time: end - start, - }); - - const cleanStdout = cleanOutput(result.stdout); - const cleanStderr = cleanOutput(result.stderr); - - if (result.exitCode !== 0) { - logger.warn( - `Command ${command} exited with exit code ${result.exitCode}: ${cleanStderr}`, - ); - } - return { - status: result.exitCode, - stdout: cleanStdout, - stderr: cleanStderr, - }; - } catch (err) { - logger.error(`Error running shell command '${command}'`, { err }); - throw err; - } -}; diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts deleted file mode 100644 index c098e4c570233..0000000000000 --- a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/executeCommandWrappers.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { Process } from "@aws/amazon-q-developer-cli-api-bindings"; -import { withTimeout } from "@aws/amazon-q-developer-cli-shared/utils"; -import { createErrorInstance } from "@aws/amazon-q-developer-cli-shared/errors"; -import logger from "loglevel"; -import { cleanOutput, executeCommandTimeout } from "./executeCommand.js"; -import { fread } from "./fs.js"; -import { osIsWindows } from '../../../helpers/os.js'; - -export const LoginShellError = createErrorInstance("LoginShellError"); - -const DONE_SOURCING_OSC = "\u001b]697;DoneSourcing\u0007"; - -let etcShells: Promise | undefined; - -const getShellExecutable = async (shellName: string) => { - if (!etcShells) { - etcShells = fread("/etc/shells").then((shells) => - shells - .split("\n") - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")), - ); - } - - try { - return ( - (await etcShells).find((shell) => shell.includes(shellName)) ?? - ( - await executeCommandTimeout({ - command: "/usr/bin/which", - args: [shellName], - }) - ).stdout - ); - } catch (_) { - return undefined; - } -}; - -export const executeLoginShell = async ({ - command, - executable, - shell, - timeout, -}: { - command: string; - executable?: string; - shell?: string; - timeout?: number; -}): Promise => { - let exe = executable; - if (!exe) { - if (!shell) { - throw new LoginShellError("Must pass shell or executable"); - } - exe = await getShellExecutable(shell); - if (!exe) { - throw new LoginShellError(`Could not find executable for ${shell}`); - } - } - // const flags = window.fig.constants?.os === "linux" ? "-lc" : "-lic"; - //TODO@meganrogge - const flags = !osIsWindows() ? "-lc" : "-lic"; - - const process = Process.run({ - executable: exe, - args: [flags, command], - // terminalSessionId: window.globalTerminalSessionId, - //TODO@meganrogge - terminalSessionId: 'test', - timeout, - }); - - try { - // logger.info(`About to run login shell command '${command}'`, { - // separateProcess: Boolean(window.f.Process), - // shell: exe, - // }); - const start = performance.now(); - const result = await withTimeout( - timeout ?? 5000, - process.then((output: any) => { - if (output.exitCode !== 0) { - logger.warn( - `Command ${command} exited with exit code ${output.exitCode}: ${output.stderr}`, - ); - } - return cleanOutput(output.stdout); - }), - ); - const idx = - result.lastIndexOf(DONE_SOURCING_OSC) + DONE_SOURCING_OSC.length; - const trimmed = result.slice(idx); - const end = performance.now(); - logger.info(`Result of login shell command '${command}'`, { - result: trimmed, - time: end - start, - }); - return trimmed; - } catch (err) { - logger.error(`Error running login shell command '${command}'`, { err }); - throw err; - } -}; - -export const executeCommand: Fig.ExecuteCommandFunction = (args) => - executeCommandTimeout(args); diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts deleted file mode 100644 index 2181bd2687750..0000000000000 --- a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/fs.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { fs as FileSystem } from "@aws/amazon-q-developer-cli-api-bindings"; - -export const fread = (path: string): Promise => - FileSystem.read(path).then((out) => out ?? ""); diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts deleted file mode 100644 index 506653aca0536..0000000000000 --- a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./executeCommandWrappers.js"; -export * from "./executeCommand.js"; -export * from "./fs.js"; -export * from "./state.js"; -export * from "./settings.js"; diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts deleted file mode 100644 index 303d22c1e4486..0000000000000 --- a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/settings.ts +++ /dev/null @@ -1,71 +0,0 @@ -export enum SETTINGS { - // Dev settings. - DEV_MODE = "autocomplete.developerMode", - DEV_MODE_NPM = "autocomplete.developerModeNPM", - DEV_MODE_NPM_INVALIDATE_CACHE = "autocomplete.developerModeNPMInvalidateCache", - DEV_COMPLETIONS_FOLDER = "autocomplete.devCompletionsFolder", - DEV_COMPLETIONS_SERVER_PORT = "autocomplete.devCompletionsServerPort", - - // Style settings - WIDTH = "autocomplete.width", - HEIGHT = "autocomplete.height", - THEME = "autocomplete.theme", - USER_STYLES = "autocomplete.userStyles", - FONT_FAMILY = "autocomplete.fontFamily", - FONT_SIZE = "autocomplete.fontSize", - - CACHE_ALL_GENERATORS = "beta.autocomplete.auto-cache", - // Behavior settings - DISABLE_FOR_COMMANDS = "autocomplete.disableForCommands", - IMMEDIATELY_EXEC_AFTER_SPACE = "autocomplete.immediatelyExecuteAfterSpace", - IMMEDIATELY_RUN_DANGEROUS_COMMANDS = "autocomplete.immediatelyRunDangerousCommands", - IMMEDIATELY_RUN_GIT_ALIAS = "autocomplete.immediatelyRunGitAliases", - INSERT_SPACE_AUTOMATICALLY = "autocomplete.insertSpaceAutomatically", - SCROLL_WRAP_AROUND = "autocomplete.scrollWrapAround", - SORT_METHOD = "autocomplete.sortMethod", - ALWAYS_SUGGEST_CURRENT_TOKEN = "autocomplete.alwaysSuggestCurrentToken", - - NAVIGATE_TO_HISTORY = "autocomplete.navigateToHistory", - ONLY_SHOW_ON_TAB = "autocomplete.onlyShowOnTab", - ALWAYS_SHOW_DESCRIPTION = "autocomplete.alwaysShowDescription", - HIDE_PREVIEW = "autocomplete.hidePreviewWindow", - SCRIPT_TIMEOUT = "autocomplete.scriptTimeout", - PREFER_VERBOSE_SUGGESTIONS = "autocomplete.preferVerboseSuggestions", - HIDE_AUTO_EXECUTE_SUGGESTION = "autocomplete.hideAutoExecuteSuggestion", - - FUZZY_SEARCH = "autocomplete.fuzzySearch", - - PERSONAL_SHORTCUTS_TOKEN = "autocomplete.personalShortcutsToken", - - DISABLE_HISTORY_LOADING = "autocomplete.history.disableLoading", - // History settings - // one of "off", "history_only", "show" - HISTORY_MODE = "beta.history.mode", - HISTORY_COMMAND = "beta.history.customCommand", - HISTORY_MERGE_SHELLS = "beta.history.allShells", - HISTORY_CTRL_R_TOGGLE = "beta.history.ctrl-r", - - FIRST_COMMAND_COMPLETION = "autocomplete.firstTokenCompletion", - - TELEMETRY_ENABLED = "telemetry.enabled", -} - -export type SettingsMap = { [key in SETTINGS]?: unknown }; -let settings: SettingsMap = {}; - -export const updateSettings = (newSettings: SettingsMap) => { - settings = newSettings; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const getSetting = (key: SETTINGS, defaultValue?: any): T => - settings[key] ?? defaultValue; - -export const getSettings = () => settings; - -export function isInDevMode(): boolean { - return ( - Boolean(getSetting(SETTINGS.DEV_MODE)) || - Boolean(getSetting(SETTINGS.DEV_MODE_NPM)) - ); -} diff --git a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts b/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts deleted file mode 100644 index 9e4d5c80adc6e..0000000000000 --- a/extensions/terminal-suggest/src/fig/api-bindings-wrappers/src/state.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { State as DefaultState } from "@aws/amazon-q-developer-cli-api-bindings"; - -export enum States { - DEVELOPER_API_HOST = "developer.apiHost", - DEVELOPER_AE_API_HOST = "developer.autocomplete-engine.apiHost", - - IS_FIG_PRO = "user.account.is-fig-pro", -} - -export type LocalStateMap = Partial< - { - [States.DEVELOPER_API_HOST]: string; - [States.DEVELOPER_AE_API_HOST]: string; - [States.IS_FIG_PRO]: boolean; - } & { [key in States]: unknown } ->; - -export type LocalStateSubscriber = { - initial?: (initialState: LocalStateMap) => void; - changes: (oldState: LocalStateMap, newState: LocalStateMap) => void; -}; - -export class State { - private static state: LocalStateMap = {}; - - private static subscribers = new Set(); - - static current = () => this.state; - - static subscribe = (subscriber: LocalStateSubscriber) => { - this.subscribers.add(subscriber); - }; - - static unsubscribe = (subscriber: LocalStateSubscriber) => { - this.subscribers.delete(subscriber); - }; - - static watch = async () => { - try { - const state = await DefaultState.current(); - this.state = state; - for (const subscriber of this.subscribers) { - subscriber.initial?.(state); - } - } catch { - // ignore errors - } - DefaultState.didChange.subscribe((notification: any) => { - const oldState = this.state; - const newState = JSON.parse( - notification.jsonBlob ?? "{}", - ) as LocalStateMap; - for (const subscriber of this.subscribers) { - subscriber.changes(oldState, newState); - } - this.state = newState; - return undefined; - }); - }; -} diff --git a/extensions/terminal-suggest/src/fig/autocomplete-helpers/src/versions.d.ts b/extensions/terminal-suggest/src/fig/autocomplete-helpers/src/versions.d.ts new file mode 100644 index 0000000000000..d90a9749c2e7c --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-helpers/src/versions.d.ts @@ -0,0 +1,7 @@ +export declare const applySpecDiff: (spec: Fig.Subcommand, diff: Fig.SpecDiff) => Fig.Subcommand; +export declare const diffSpecs: (original: Fig.Subcommand, updated: Fig.Subcommand) => Fig.SpecDiff; +export declare const getVersionFromVersionedSpec: (base: Fig.Subcommand, versions: Fig.VersionDiffMap, target?: string) => { + version: string; + spec: Fig.Subcommand; +}; +export declare const createVersionedSpec: (specName: string, versionFiles: string[]) => Fig.Spec; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts deleted file mode 100644 index e02acac454d9d..0000000000000 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./constants.js"; -export * from "./errors.js"; -export * from "./loadHelpers.js"; -export * from "./loadSpec.js"; -export * from "./parseArguments.js"; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts deleted file mode 100644 index 7ebb4abbefddc..0000000000000 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadHelpers.ts +++ /dev/null @@ -1,248 +0,0 @@ -import * as semver from 'semver'; -import logger, { Logger } from 'loglevel'; - -import { - withTimeout, - exponentialBackoff, - ensureTrailingSlash, -} from "../../shared/src/utils"; -import { - executeCommand, - fread, - isInDevMode, -} from "../../api-bindings-wrappers/src"; -import z from "zod"; -import { MOST_USED_SPECS } from "./constants.js"; -import { LoadLocalSpecError, SpecCDNError } from "./errors.js"; - -export type SpecFileImport = - | { - default: Fig.Spec; - getVersionCommand?: Fig.GetVersionCommand; - } - | { - default: Fig.Subcommand; - versions: Fig.VersionDiffMap; - }; - -const makeCdnUrlFactory = - (baseUrl: string) => - (specName: string, ext: string = "js") => - `${baseUrl}${specName}.${ext}`; - -const cdnUrlFactory = makeCdnUrlFactory( - "https://specs.q.us-east-1.amazonaws.com/", -); - -const stringImportCache = new Map(); - -export const importString = async (str: string) => { - if (stringImportCache.has(str)) { - return stringImportCache.get(str); - } - const result = await import( - /* @vite-ignore */ - URL.createObjectURL(new Blob([str], { type: "text/javascript" })) - ); - - stringImportCache.set(str, result); - return result; -}; - -/* - * Deprecated: eventually will just use importLocalSpec above - * Load a spec import("{path}/{name}") - */ -export async function importSpecFromFile( - name: string, - path: string, - localLogger: Logger = logger, -): Promise { - const importFromPath = async (fullPath: string) => { - localLogger.info(`Loading spec from ${fullPath}`); - const contents = await fread(fullPath); - if (!contents) { - throw new LoadLocalSpecError(`Failed to read file: ${fullPath}`); - } - return contents; - }; - - let result: string; - const joinedPath = `${ensureTrailingSlash(path)}${name}`; - try { - result = await importFromPath(`${joinedPath}.js`); - } catch (_) { - result = await importFromPath(`${joinedPath}/index.js`); - } - - return importString(result); -} - -/** - * Specs can only be loaded from non "secure" contexts, so we can't load from https - */ -//TODO@meganrogge fix -export const canLoadSpecProtocol = () => true; - -// TODO: this is a problem for diff-versioned specs -export async function importFromPublicCDN( - name: string, -): Promise { - if (canLoadSpecProtocol()) { - return withTimeout( - 20000, - import( - /* @vite-ignore */ - `spec://localhost/${name}.js` - ), - ); - } - - // Total of retries in the worst case should be close to previous timeout value - // 500ms * 2^5 + 5 * 1000ms + 5 * 100ms = 21500ms, before the timeout was 20000ms - try { - return await exponentialBackoff( - { - attemptTimeout: 1000, - baseDelay: 500, - maxRetries: 5, - jitter: 100, - }, - - () => import(/* @vite-ignore */ cdnUrlFactory(name)), - ); - } catch { - /**/ - } - - throw new SpecCDNError("Unable to load from a CDN"); -} - -async function jsonFromPublicCDN(path: string): Promise { - if (canLoadSpecProtocol()) { - return fetch(`spec://localhost/${path}.json`).then((res) => res.json()); - } - - return exponentialBackoff( - { - attemptTimeout: 1000, - baseDelay: 500, - maxRetries: 5, - jitter: 100, - }, - () => fetch(cdnUrlFactory(path, "json")).then((res) => res.json()), - ); -} - -// TODO: this is a problem for diff-versioned specs -export async function importFromLocalhost( - name: string, - port: number | string, -): Promise { - return withTimeout( - 20000, - import( - /* @vite-ignore */ - `http://localhost:${port}/${name}.js` - ), - ); -} - -const cachedCLIVersions: Record = {}; - -export const getCachedCLIVersion = (key: string) => - cachedCLIVersions[key] ?? null; - -export async function getVersionFromFullFile( - specData: SpecFileImport, - name: string, -) { - // if the default export is a function it is a versioned spec - if (typeof specData.default === "function") { - try { - const storageKey = `cliVersion-${name}`; - const version = getCachedCLIVersion(storageKey); - if (!isInDevMode() && version !== null) { - return version; - } - - if ("getVersionCommand" in specData && specData.getVersionCommand) { - const newVersion = await specData.getVersionCommand(executeCommand); - cachedCLIVersions[storageKey] = newVersion; - return newVersion; - } - - const newVersion = semver.clean( - ( - await executeCommand({ - command: name, - args: ["--version"], - }) - ).stdout, - ); - if (newVersion) { - cachedCLIVersions[storageKey] = newVersion; - return newVersion; - } - } catch { - /**/ - } - } - return undefined; -} - -// TODO: cache this request using SWR strategy -let publicSpecsRequest: - | Promise<{ - completions: Set; - diffVersionedSpecs: Set; - }> - | undefined; - -export function clearSpecIndex() { - publicSpecsRequest = undefined; -} - -const INDEX_ZOD = z.object({ - completions: z.array(z.string()), - diffVersionedCompletions: z.array(z.string()), -}); - -const createPublicSpecsRequest = async () => { - if (publicSpecsRequest === undefined) { - publicSpecsRequest = jsonFromPublicCDN("index") - .then(INDEX_ZOD.parse) - .then((index) => ({ - completions: new Set(index.completions), - diffVersionedSpecs: new Set(index.diffVersionedCompletions), - })) - .catch(() => { - publicSpecsRequest = undefined; - return { completions: new Set(), diffVersionedSpecs: new Set() }; - }); - } - return publicSpecsRequest; -}; - -export async function publicSpecExists(name: string): Promise { - const { completions } = await createPublicSpecsRequest(); - return completions.has(name); -} - -export async function isDiffVersionedSpec(name: string): Promise { - const { diffVersionedSpecs } = await createPublicSpecsRequest(); - return diffVersionedSpecs.has(name); -} - -export async function preloadSpecs(): Promise { - return Promise.all( - MOST_USED_SPECS.map(async (name) => { - // TODO: refactor everything to allow the correct diff-versioned specs to be loaded - // too, now we are only loading the index - if (await isDiffVersionedSpec(name)) { - return importFromPublicCDN(`${name}/index`); - } - return importFromPublicCDN(name); - }).map((promise) => promise.catch((e) => e)), - ); -} diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts deleted file mode 100644 index 0a77b973b361f..0000000000000 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/loadSpec.ts +++ /dev/null @@ -1,250 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import logger, { Logger } from 'loglevel'; -import * as Settings from '../../shared/src/settings'; -import { convertSubcommand, initializeDefault } from '@fig/autocomplete-shared'; -import { - withTimeout, - SpecLocationSource, - splitPath, - ensureTrailingSlash, -} from "../../shared/src/utils"; -import { - Subcommand, - SpecLocation, -} from "../../shared/src/internal"; -import { - SETTINGS, - getSetting, - executeCommand, - isInDevMode, -} from "../../api-bindings-wrappers/src"; -import { - importFromPublicCDN, - publicSpecExists, - SpecFileImport, - importSpecFromFile, - isDiffVersionedSpec, - importFromLocalhost, -} from "./loadHelpers.js"; -import { DisabledSpecError, MissingSpecError } from "./errors.js"; -import { specCache } from "./caches.js"; -import { tryResolveSpecToSubcommand } from "./tryResolveSpecToSubcommand.js"; - -/** - * This searches for the first directory containing a .fig/ folder in the parent directories - */ -const searchFigFolder = async (currentDirectory: string) => { - try { - return ensureTrailingSlash( - ( - await executeCommand({ - command: "bash", - args: [ - "-c", - `until [[ -f .fig/autocomplete/build/_shortcuts.js ]] || [[ $PWD = $HOME ]] || [[ $PWD = "/" ]]; do cd ..; done; echo $PWD`, - ], - cwd: currentDirectory, - }) - ).stdout, - ); - } catch { - return ensureTrailingSlash(currentDirectory); - } -}; - -export const serializeSpecLocation = (location: SpecLocation): string => { - if (location.type === SpecLocationSource.GLOBAL) { - return `global://name=${location.name}`; - } - return `local://path=${location.path ?? ""}&name=${location.name}`; -}; - -export const getSpecPath = async ( - name: string, - cwd: string, - isScript?: boolean, -): Promise => { - if (name === "?") { - // If the user is searching for _shortcuts.js by using "?" - const path = await searchFigFolder(cwd); - return { name: "_shortcuts", type: SpecLocationSource.LOCAL, path }; - } - - const personalShortcutsToken = - getSetting(SETTINGS.PERSONAL_SHORTCUTS_TOKEN) || "+"; - if (name === personalShortcutsToken) { - return { name: "+", type: SpecLocationSource.LOCAL, path: "~/" }; - } - - const [path, basename] = splitPath(name); - - if (!isScript) { - const type = SpecLocationSource.GLOBAL; - - // If `isScript` is undefined, we are parsing the first token, and - // any path with a / is a script. - if (isScript === undefined) { - // special-case: Symfony has "bin/console" which can be invoked directly - // and should not require a user to create script completions for it - if (name === "bin/console" || name.endsWith("/bin/console")) { - return { name: "php/bin-console", type }; - } - if (!path.includes("/")) { - return { name, type }; - } - } else if (["/", "./", "~/"].every((prefix) => !path.startsWith(prefix))) { - return { name, type }; - } - } - - const type = SpecLocationSource.LOCAL; - if (path.startsWith("/") || path.startsWith("~/")) { - return { name: basename, type, path }; - } - - const relative = path.startsWith("./") ? path.slice(2) : path; - return { name: basename, type, path: `${cwd}/${relative}` }; -}; - -type ResolvedSpecLocation = - | { type: "public"; name: string } - | { type: "private"; namespace: string; name: string }; - -export const importSpecFromLocation = async ( - specLocation: SpecLocation, - localLogger: Logger = logger, -): Promise<{ - specFile: SpecFileImport; - resolvedLocation?: ResolvedSpecLocation; -}> => { - // Try loading spec from `devCompletionsFolder` first. - const devPath = isInDevMode() - ? (getSetting(SETTINGS.DEV_COMPLETIONS_FOLDER) as string) - : undefined; - - const devPort = isInDevMode() - ? getSetting(SETTINGS.DEV_COMPLETIONS_SERVER_PORT) - : undefined; - - let specFile: SpecFileImport | undefined; - let resolvedLocation: ResolvedSpecLocation | undefined; - - if (typeof devPort === "string" || typeof devPort === "number") { - const { diffVersionedFile, name } = specLocation; - specFile = await importFromLocalhost( - diffVersionedFile ? `${name}/${diffVersionedFile}` : name, - devPort, - ); - } - - if (!specFile && devPath) { - try { - const { diffVersionedFile, name } = specLocation; - const spec = await importSpecFromFile( - diffVersionedFile ? `${name}/${diffVersionedFile}` : name, - devPath, - localLogger, - ); - specFile = spec; - } catch { - // fallback to loading other specs in dev mode. - } - } - - if (!specFile && specLocation.type === SpecLocationSource.LOCAL) { - // If we couldn't successfully load a dev spec try loading from specPath. - const { name, path } = specLocation; - const [dirname, basename] = splitPath(`${path || "~/"}${name}`); - - specFile = await importSpecFromFile( - basename, - `${dirname}.fig/autocomplete/build/`, - localLogger, - ); - } else if (!specFile) { - const { name, diffVersionedFile: versionFileName } = specLocation; - - if (await publicSpecExists(name)) { - // If we're here, importing was successful. - try { - const result = await importFromPublicCDN( - versionFileName ? `${name}/${versionFileName}` : name, - ); - - specFile = result; - resolvedLocation = { type: "public", name }; - } catch (err) { - localLogger.error("Unable to load from CDN", err); - throw err; - } - } else { - try { - specFile = await importSpecFromFile( - name, - `~/.fig/autocomplete/build/`, - localLogger, - ); - } catch (_err) { - /* empty */ - } - } - } - - if (!specFile) { - throw new MissingSpecError("No spec found"); - } - - return { specFile, resolvedLocation }; -}; - -export const loadFigSubcommand = async ( - specLocation: SpecLocation, - _context?: Fig.ShellContext, - localLogger: Logger = logger, -): Promise => { - const { name } = specLocation; - const location = (await isDiffVersionedSpec(name)) - ? { ...specLocation, diffVersionedFile: "index" } - : specLocation; - const { specFile } = await importSpecFromLocation(location, localLogger); - const subcommand = await tryResolveSpecToSubcommand(specFile, specLocation); - return subcommand; -}; - -export const loadSubcommandCached = async ( - specLocation: SpecLocation, - context?: Fig.ShellContext, - localLogger: Logger = logger, -): Promise => { - const { name, type: source } = specLocation; - const path = - specLocation.type === SpecLocationSource.LOCAL ? specLocation.path : ""; - - // Do not load completion spec for commands that are 'disabled' by user - const disabledSpecs = - getSetting(SETTINGS.DISABLE_FOR_COMMANDS) || []; - if (disabledSpecs.includes(name)) { - localLogger.info(`Not getting path for disabled spec ${name}`); - throw new DisabledSpecError("Command requested disabled completion spec"); - } - - const key = [source, path || "", name].join(","); - if (getSetting(SETTINGS.DEV_MODE_NPM_INVALIDATE_CACHE)) { - specCache.clear(); - Settings.set(SETTINGS.DEV_MODE_NPM_INVALIDATE_CACHE, false); - } else if (!getSetting(SETTINGS.DEV_MODE_NPM) && specCache.has(key)) { - return specCache.get(key) as Subcommand; - } - - const subcommand = await withTimeout( - 5000, - loadFigSubcommand(specLocation, context, localLogger), - ); - const converted = convertSubcommand(subcommand, initializeDefault); - specCache.set(key, converted); - return converted; -}; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts index 6544069f70071..c3c448ae252d3 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts @@ -1,6 +1,4 @@ -import logger from "loglevel"; -import { convertSubcommand, initializeDefault } from "@fig/autocomplete-shared"; -import { filepaths, folders } from "@fig/autocomplete-generators"; +// import { filepaths, folders } from "@fig/autocomplete-generators"; import * as Internal from "../../shared/src/internal"; import { firstMatchingToken, @@ -8,30 +6,29 @@ import { SpecLocationSource, SuggestionFlag, SuggestionFlags, - withTimeout, } from "../../shared/src/utils"; -import { - executeCommand, - executeLoginShell, - getSetting, - isInDevMode, - SETTINGS, -} from "../../api-bindings-wrappers/src"; +// import { +// executeCommand, +// executeLoginShell, +// getSetting, +// isInDevMode, +// SETTINGS, +// } from "../../api-bindings-wrappers/src"; import { Command, substituteAlias, } from "../../shell-parser/src"; -import { - getSpecPath, - loadSubcommandCached, - serializeSpecLocation, -} from "./loadSpec.js"; +// import { +// getSpecPath, +// loadSubcommandCached, +// serializeSpecLocation, +// } from "./loadSpec.js"; import { ParseArgumentsError, ParsingHistoryError, UpdateStateError, } from "./errors.js"; -import { createCache, generateSpecCache } from "./caches.js"; +const { exec } = require("child_process"); type ArgArrayState = { args: Array | null; @@ -109,10 +106,12 @@ export const createArgState = (args?: Internal.Arg[]): ArgArrayState => { const templateArray = makeArray(generator.template ?? []); let updatedGenerator: Fig.Generator | undefined; + // TODO: Pass templates out as a result if (templateArray.includes("filepaths")) { - updatedGenerator = filepaths; + // TODO@meganrogge + // updatedGenerator = filepaths; } else if (templateArray.includes("folders")) { - updatedGenerator = folders; + // updatedGenerator = folders; } if (updatedGenerator && generator.filterTemplateSuggestions) { @@ -600,101 +599,101 @@ const historyExecuteShellCommand: Fig.ExecuteCommandFunction = async () => { }; const getExecuteShellCommandFunction = (isParsingHistory = false) => - isParsingHistory ? historyExecuteShellCommand : executeCommand; - -const getGenerateSpecCacheKey = ( - completionObj: Internal.Subcommand, - tokenArray: string[], -): string | undefined => { - let cacheKey: string | undefined; - - const generateSpecCacheKey = completionObj?.generateSpecCacheKey; - if (generateSpecCacheKey) { - if (typeof generateSpecCacheKey === "string") { - cacheKey = generateSpecCacheKey; - } else if (typeof generateSpecCacheKey === "function") { - cacheKey = generateSpecCacheKey({ - tokens: tokenArray, - }); - } else { - logger.error( - "generateSpecCacheKey must be a string or function", - generateSpecCacheKey, - ); - } - } - - // Return this late to ensure any generateSpecCacheKey side effects still happen - if (isInDevMode()) { - return undefined; - } - if (typeof cacheKey === "string") { - // Prepend the spec name to the cacheKey to avoid collisions between specs. - return `${tokenArray[0]}:${cacheKey}`; - } - return undefined; -}; - -const generateSpecForState = async ( - state: ArgumentParserState, - tokenArray: string[], - isParsingHistory = false, - localLogger: logger.Logger = logger, -): Promise => { - localLogger.debug("generateSpec", { state, tokenArray }); - const { completionObj } = state; - const { generateSpec } = completionObj; - if (!generateSpec) { - return state; - } - - try { - const cacheKey = getGenerateSpecCacheKey(completionObj, tokenArray); - let newSpec; - if (cacheKey && generateSpecCache.has(cacheKey)) { - newSpec = generateSpecCache.get(cacheKey)!; - } else { - const exec = getExecuteShellCommandFunction(isParsingHistory); - const spec = await generateSpec(tokenArray, exec); - if (!spec) { - throw new UpdateStateError("generateSpec must return a spec"); - } - newSpec = convertSubcommand( - spec, - initializeDefault, - ); - if (cacheKey) generateSpecCache.set(cacheKey, newSpec); - } - - const keepArgs = completionObj.args.length > 0; - - return { - ...state, - completionObj: { - ...completionObj, - subcommands: { ...completionObj.subcommands, ...newSpec.subcommands }, - options: { ...completionObj.options, ...newSpec.options }, - persistentOptions: { - ...completionObj.persistentOptions, - ...newSpec.persistentOptions, - }, - args: keepArgs ? completionObj.args : newSpec.args, - }, - subcommandArgState: keepArgs - ? state.subcommandArgState - : createArgState(newSpec.args), - }; - } catch (err) { - if (!(err instanceof ParsingHistoryError)) { - localLogger.error( - `There was an error with spec (generator owner: ${completionObj.name - }, tokens: ${tokenArray.join(", ")}) generateSpec function`, - err, - ); - } - } - return state; -}; + isParsingHistory ? historyExecuteShellCommand : () => { throw new Error("Not implemented"); }; + +// const getGenerateSpecCacheKey = ( +// completionObj: Internal.Subcommand, +// tokenArray: string[], +// ): string | undefined => { +// let cacheKey: string | undefined; + +// const generateSpecCacheKey = completionObj?.generateSpecCacheKey; +// if (generateSpecCacheKey) { +// if (typeof generateSpecCacheKey === "string") { +// cacheKey = generateSpecCacheKey; +// } else if (typeof generateSpecCacheKey === "function") { +// cacheKey = generateSpecCacheKey({ +// tokens: tokenArray, +// }); +// } else { +// console.error( +// "generateSpecCacheKey must be a string or function", +// generateSpecCacheKey, +// ); +// } +// } + +// // Return this late to ensure any generateSpecCacheKey side effects still happen +// // if (isInDevMode()) { +// // return undefined; +// // } +// if (typeof cacheKey === "string") { +// // Prepend the spec name to the cacheKey to avoid collisions between specs. +// return `${tokenArray[0]}:${cacheKey}`; +// } +// return undefined; +// }; + +// const generateSpecForState = async ( +// state: ArgumentParserState, +// tokenArray: string[], +// isParsingHistory = false, +// // localconsole: console.console = console, +// ): Promise => { +// console.debug("generateSpec", { state, tokenArray }); +// const { completionObj } = state; +// const { generateSpec } = completionObj; +// if (!generateSpec) { +// return state; +// } + +// try { +// const cacheKey = getGenerateSpecCacheKey(completionObj, tokenArray); +// let newSpec; +// if (cacheKey && generateSpecCache.has(cacheKey)) { +// newSpec = generateSpecCache.get(cacheKey)!; +// } else { +// const exec = getExecuteShellCommandFunction(isParsingHistory); +// const spec = await generateSpec(tokenArray, exec); +// if (!spec) { +// throw new UpdateStateError("generateSpec must return a spec"); +// } +// newSpec = convertSubcommand( +// spec, +// initializeDefault, +// ); +// if (cacheKey) generateSpecCache.set(cacheKey, newSpec); +// } + +// const keepArgs = completionObj.args.length > 0; + +// return { +// ...state, +// completionObj: { +// ...completionObj, +// subcommands: { ...completionObj.subcommands, ...newSpec.subcommands }, +// options: { ...completionObj.options, ...newSpec.options }, +// persistentOptions: { +// ...completionObj.persistentOptions, +// ...newSpec.persistentOptions, +// }, +// args: keepArgs ? completionObj.args : newSpec.args, +// }, +// subcommandArgState: keepArgs +// ? state.subcommandArgState +// : createArgState(newSpec.args), +// }; +// } catch (err) { +// if (!(err instanceof ParsingHistoryError)) { +// console.error( +// `There was an error with spec (generator owner: ${completionObj.name +// }, tokens: ${tokenArray.join(", ")}) generateSpec function`, +// err, +// ); +// } +// } +// return state; +// }; export const getResultFromState = ( state: ArgumentParserState, @@ -757,27 +756,27 @@ export const initialParserState = getResultFromState( }), ); -const parseArgumentsCache = createCache(); -const parseArgumentsGenerateSpecCache = createCache(); -const figCaches = new Set(); -export const clearFigCaches = () => { - for (const cache of figCaches) { - parseArgumentsGenerateSpecCache.delete(cache); - } - return { unsubscribe: false }; -}; - -const getCacheKey = ( - tokenArray: string[], - context: Fig.ShellContext, - specLocation: Internal.SpecLocation, -): string => - [ - tokenArray.slice(0, -1).join(" "), - serializeSpecLocation(specLocation), - context.currentWorkingDirectory, - context.currentProcess, - ].join(","); +// const parseArgumentsCache = createCache(); +// const parseArgumentsGenerateSpecCache = createCache(); +// const figCaches = new Set(); +// export const clearFigCaches = () => { +// for (const cache of figCaches) { +// parseArgumentsGenerateSpecCache.delete(cache); +// } +// return { unsubscribe: false }; +// }; + +// const getCacheKey = ( +// tokenArray: string[], +// context: Fig.ShellContext, +// specLocation: Internal.SpecLocation, +// ): string => +// [ +// tokenArray.slice(0, -1).join(" "), +// // serializeSpecLocation(specLocation), +// context.currentWorkingDirectory, +// context.currentProcess, +// ].join(","); // Parse all arguments in tokenArray. const parseArgumentsCached = async ( @@ -787,63 +786,69 @@ const parseArgumentsCached = async ( specLocations?: Internal.SpecLocation[], isParsingHistory?: boolean, startIndex = 0, - localLogger: logger.Logger = logger, + // localconsole: console.console = console, ): Promise => { + // Route to cp.exec instead, we don't need to deal with ipc const exec = getExecuteShellCommandFunction(isParsingHistory); let currentCommand = command; let tokens = currentCommand.tokens.slice(startIndex); - const tokenText = tokens.map((token) => token.text); - - const locations = specLocations || [ - await getSpecPath(tokenText[0], context.currentWorkingDirectory), - ]; - localLogger.debug({ locations }); - - let cacheKey = ""; - for (let i = 0; i < locations.length; i += 1) { - cacheKey = getCacheKey(tokenText, context, locations[i]); - if ( - !isInDevMode() && - (parseArgumentsCache.has(cacheKey) || - parseArgumentsGenerateSpecCache.has(cacheKey)) - ) { - return ( - (parseArgumentsGenerateSpecCache.get( - cacheKey, - ) as ArgumentParserState) || - (parseArgumentsCache.get(cacheKey) as ArgumentParserState) - ); - } - } - - let spec: Internal.Subcommand | undefined; - let specPath: Internal.SpecLocation | undefined; - for (let i = 0; i < locations.length; i += 1) { - specPath = locations[i]; - if (isParsingHistory && specPath?.type === SpecLocationSource.LOCAL) { - continue; - } - - spec = await withTimeout( - 5000, - loadSubcommandCached(specPath, context, localLogger), - ); - if (!specPath) { - throw new Error("specPath is undefined"); - } - - if (!spec) { - const path = - specPath.type === SpecLocationSource.LOCAL ? specPath?.path : ""; - localLogger.warn( - `Failed to load spec ${specPath.name} from ${specPath.type} ${path}`, - ); - } else { - cacheKey = getCacheKey(tokenText, context, specPath); - break; - } - } + // const tokenText = tokens.map((token) => token.text); + + // TODO: Fill this in the with actual one (replace specLocations) + const spec: Fig.Spec = null!; + const specPath: Fig.SpecLocation = { type: 'global', name: 'fake' }; + + // tokenTest[0] is the command and the spec they need + // const locations = specLocations || [ + // await getSpecPath(tokenText[0], context.currentWorkingDirectory), + // ]; + // console.debug({ locations }); + + // let cacheKey = ""; + // for (let i = 0; i < locations.length; i += 1) { + // cacheKey = getCacheKey(tokenText, context, locations[i]); + // if ( + // // !isInDevMode() && + // (parseArgumentsCache.has(cacheKey) || + // parseArgumentsGenerateSpecCache.has(cacheKey)) + // ) { + // return ( + // (parseArgumentsGenerateSpecCache.get( + // cacheKey, + // ) as ArgumentParserState) || + // (parseArgumentsCache.get(cacheKey) as ArgumentParserState) + // ); + // } + // } + + // let spec: Internal.Subcommand | undefined; + // let specPath: Internal.SpecLocation | undefined; + // for (let i = 0; i < locations.length; i += 1) { + // specPath = locations[i]; + // if (isParsingHistory && specPath?.type === SpecLocationSource.LOCAL) { + // continue; + // } + + // spec = await withTimeout( + // 5000, + // loadSubcommandCached(specPath, context, console), + // ); + // if (!specPath) { + // throw new Error("specPath is undefined"); + // } + + // if (!spec) { + // const path = + // specPath.type === SpecLocationSource.LOCAL ? specPath?.path : ""; + // console.warn( + // `Failed to load spec ${specPath.name} from ${specPath.type} ${path}`, + // ); + // } else { + // cacheKey = getCacheKey(tokenText, context, specPath); + // break; + // } + // } if (!spec || !specPath) { throw new UpdateStateError("Failed loading spec"); @@ -855,71 +860,71 @@ const parseArgumentsCached = async ( specPath, ); - let generatedSpec = false; + // let generatedSpec = false; const substitutedAliases = new Set(); let aliasError: Error | undefined; // Returns true if we should return state immediately after calling. - const updateStateForLoadSpec = async ( - loadSpec: typeof state.completionObj.loadSpec, - index: number, - token?: string, - ) => { - const loadSpecResult = - typeof loadSpec === "function" - ? token !== undefined - ? await loadSpec(token, exec) - : undefined - : loadSpec; - - if (Array.isArray(loadSpecResult)) { - state = await parseArgumentsCached( - currentCommand, - context, - // authClient, - loadSpecResult, - isParsingHistory, - startIndex + index, - ); - state = { ...state, commandIndex: state.commandIndex + index }; - return true; - } - - if (loadSpecResult) { - state = { - ...state, - completionObj: { - ...loadSpecResult, - parserDirectives: { - ...state.completionObj.parserDirectives, - ...loadSpecResult.parserDirectives, - }, - }, - optionArgState: createArgState(), - passedOptions: [], - subcommandArgState: createArgState(loadSpecResult.args), - haveEnteredSubcommandArgs: false, - }; - } - - return false; - }; - - if (await updateStateForLoadSpec(state.completionObj.loadSpec, 0)) { - return state; - } + // const updateStateForLoadSpec = async ( + // loadSpec: typeof state.completionObj.loadSpec, + // index: number, + // token?: string, + // ) => { + // const loadSpecResult = + // typeof loadSpec === "function" + // ? token !== undefined + // ? await loadSpec(token, exec) + // : undefined + // : loadSpec; + + // if (Array.isArray(loadSpecResult)) { + // state = await parseArgumentsCached( + // currentCommand, + // context, + // // authClient, + // loadSpecResult, + // isParsingHistory, + // startIndex + index, + // ); + // state = { ...state, commandIndex: state.commandIndex + index }; + // return true; + // } + + // if (loadSpecResult) { + // state = { + // ...state, + // completionObj: { + // ...loadSpecResult, + // parserDirectives: { + // ...state.completionObj.parserDirectives, + // ...loadSpecResult.parserDirectives, + // }, + // }, + // optionArgState: createArgState(), + // passedOptions: [], + // subcommandArgState: createArgState(loadSpecResult.args), + // haveEnteredSubcommandArgs: false, + // }; + // } + + // return false; + // }; + + // if (await updateStateForLoadSpec(state.completionObj.loadSpec, 0)) { + // return state; + // } for (let i = 1; i < tokens.length; i += 1) { - if (state.completionObj.generateSpec) { - state = await generateSpecForState( - state, - tokens.map((token) => token.text), - isParsingHistory, - localLogger, - ); - generatedSpec = true; - } + // TODO: Investigate generate spec + // if (state.completionObj.generateSpec) { + // state = await generateSpecForState( + // state, + // tokens.map((token) => token.text), + // isParsingHistory, + // ); + // generatedSpec = true; + // } if (i === tokens.length - 1) { // Don't update state for last token. @@ -936,7 +941,7 @@ const parseArgumentsCached = async ( const lastState = state; state = updateState(state, token); - localLogger.debug("Parser state update", { state }); + console.debug("Parser state update", { state }); const { annotations } = state; const lastAnnotation = annotations[annotations.length - 1]; @@ -963,7 +968,7 @@ const parseArgumentsCached = async ( i -= 1; continue; } catch (err) { - localLogger.error("Error substituting alias:", err); + console.error("Error substituting alias:", err); throw err; } } catch (err) { @@ -974,41 +979,42 @@ const parseArgumentsCached = async ( } } - let loadSpec = - lastType === TokenType.Subcommand - ? state.completionObj.loadSpec - : undefined; + // TODO: Investigate whether we want to support loadSpec, vs just importing them directly + // let loadSpec = + // lastType === TokenType.Subcommand + // ? state.completionObj.loadSpec + // : undefined; // Recurse for load spec or special arg - if (lastType === lastArgType && lastArgObject) { - const { - isCommand, - isModule, - isScript, - loadSpec: argLoadSpec, - } = lastArgObject; - if (argLoadSpec) { - loadSpec = argLoadSpec; - } else if (isCommand || isScript) { - const specLocation = await getSpecPath( - token, - context.currentWorkingDirectory, - Boolean(isScript), - ); - loadSpec = [specLocation]; - } else if (isModule) { - loadSpec = [ - { - name: `${isModule}${token}`, - type: SpecLocationSource.GLOBAL, - }, - ]; - } - } - - if (await updateStateForLoadSpec(loadSpec, i, token)) { - return state; - } + // if (lastType === lastArgType && lastArgObject) { + // const { + // isCommand, + // isModule, + // isScript, + // loadSpec: argLoadSpec, + // } = lastArgObject; + // if (argLoadSpec) { + // loadSpec = argLoadSpec; + // } else if (isCommand || isScript) { + // // const specLocation = await getSpecPath( + // // token, + // // context.currentWorkingDirectory, + // // Boolean(isScript), + // // ); + // // loadSpec = [specLocation]; + // } else if (isModule) { + // loadSpec = [ + // { + // name: `${isModule}${token}`, + // type: SpecLocationSource.GLOBAL, + // }, + // ]; + // } + // } + + // if (await updateStateForLoadSpec(loadSpec, i, token)) { + // return state; + // } // If error with alias and corresponding arg was not used in a loadSpec, // throw the error. @@ -1019,12 +1025,12 @@ const parseArgumentsCached = async ( substitutedAliases.clear(); } - if (generatedSpec) { - if (tokenText[0] === "fig") figCaches.add(cacheKey); - parseArgumentsGenerateSpecCache.set(cacheKey, state); - } else { - parseArgumentsCache.set(cacheKey, state); - } + // if (generatedSpec) { + // if (tokenText[0] === "fig") figCaches.add(cacheKey); + // parseArgumentsGenerateSpecCache.set(cacheKey, state); + // } else { + // parseArgumentsCache.set(cacheKey, state); + // } return state; }; @@ -1091,12 +1097,30 @@ const firstTokenSpec: Internal.Subcommand = { parserDirectives: {}, }; +const executeLoginShell = async ({ + command, + executable, +}: { + command: string; + executable: string; +}): Promise => { + return new Promise((resolve, reject) => { + exec(`${executable} -c "${command}"`, (error: Error, stdout: string, stderr: string) => { + if (error) { + reject(stderr); + } else { + resolve(stdout); + } + }); + }); +}; + export const parseArguments = async ( command: Command | null, context: Fig.ShellContext, // authClient: AuthClient, isParsingHistory = false, - localLogger: logger.Logger = logger, + // localconsole: console.console = console, ): Promise => { const tokens = command?.tokens ?? []; if (!command || tokens.length === 0) { @@ -1104,9 +1128,7 @@ export const parseArguments = async ( } if (tokens.length === 1) { - const showFirstCommandCompletion = getSetting( - SETTINGS.FIRST_COMMAND_COMPLETION, - ); + const showFirstCommandCompletion = true; let spec = showFirstCommandCompletion ? firstTokenSpec : { ...firstTokenSpec, args: [] }; @@ -1119,7 +1141,7 @@ export const parseArguments = async ( } else { specPath = { name: "dotslash", type: SpecLocationSource.GLOBAL }; } - spec = await loadSubcommandCached(specPath, context, localLogger); + // spec = await loadSubcommandCached(specPath, context); } return getResultFromState(getInitialState(spec, tokens[0].text, specPath)); } @@ -1131,7 +1153,6 @@ export const parseArguments = async ( undefined, isParsingHistory, 0, - localLogger, ); const finalToken = tokens[tokens.length - 1].text; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts deleted file mode 100644 index 40bae26605fde..0000000000000 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/tryResolveSpecToSubcommand.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { getVersionFromVersionedSpec } from "@fig/autocomplete-helpers"; -import { splitPath } from "../../shared/src/utils"; -import { SpecLocation } from "../../shared/src/internal"; -import { SpecFileImport, getVersionFromFullFile } from "./loadHelpers.js"; -import { WrongDiffVersionedSpecError } from "./errors.js"; -import { importSpecFromLocation } from "./loadSpec.js"; - -export const tryResolveSpecToSubcommand = async ( - spec: SpecFileImport, - location: SpecLocation, -): Promise => { - if (typeof spec.default === "function") { - // Handle versioned specs, either simple versioned or diff versioned. - const cliVersion = await getVersionFromFullFile(spec, location.name); - const subcommandOrDiffVersionInfo = await spec.default(cliVersion); - - if ("versionedSpecPath" in subcommandOrDiffVersionInfo) { - // Handle diff versioned specs. - const { versionedSpecPath, version } = subcommandOrDiffVersionInfo; - const [dirname, basename] = splitPath(versionedSpecPath); - const { specFile } = await importSpecFromLocation({ - ...location, - name: dirname.slice(0, -1), - diffVersionedFile: basename, - }); - - if ("versions" in specFile) { - const result = getVersionFromVersionedSpec( - specFile.default, - specFile.versions, - version, - ); - return result.spec; - } - - throw new WrongDiffVersionedSpecError("Invalid versioned specs file"); - } - - return subcommandOrDiffVersionInfo; - } - - return spec.default; -}; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/convert.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/convert.ts new file mode 100644 index 0000000000000..dffd777aa5115 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/convert.ts @@ -0,0 +1,73 @@ +import { makeArray } from "./utils"; + +export type SuggestionType = Fig.SuggestionType | "history" | "auto-execute"; + +type Override = Omit & S; +export type Suggestion = Override; + +export type Option = OptionT & { + name: string[]; + args: ArgT[]; +}; + +export type Subcommand = SubcommandT & { + name: string[]; + subcommands: Record>; + options: Record>; + persistentOptions: Record>; + args: ArgT[]; +}; + +const makeNamedMap = (items: T[] | undefined): Record => { + const nameMapping: Record = {}; + if (!items) { + return nameMapping; + } + + for (let i = 0; i < items.length; i += 1) { + items[i].name.forEach((name) => { + nameMapping[name] = items[i]; + }); + } + return nameMapping; +}; + +export type Initializer = { + subcommand: (subcommand: Fig.Subcommand) => SubcommandT; + option: (option: Fig.Option) => OptionT; + arg: (arg: Fig.Arg) => ArgT; +}; + +function convertOption( + option: Fig.Option, + initialize: Omit, "subcommand"> +): Option { + return { + ...initialize.option(option), + name: makeArray(option.name), + args: option.args ? makeArray(option.args).map(initialize.arg) : [], + }; +} + +export function convertSubcommand( + subcommand: Fig.Subcommand, + initialize: Initializer +): Subcommand { + const { subcommands, options, args } = subcommand; + return { + ...initialize.subcommand(subcommand), + name: makeArray(subcommand.name), + subcommands: makeNamedMap(subcommands?.map((s) => convertSubcommand(s, initialize))), + options: makeNamedMap( + options + ?.filter((option) => !option.isPersistent) + ?.map((option) => convertOption(option, initialize)) + ), + persistentOptions: makeNamedMap( + options + ?.filter((option) => option.isPersistent) + ?.map((option) => convertOption(option, initialize)) + ), + args: args ? makeArray(args).map(initialize.arg) : [], + }; +} diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/index.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/index.ts new file mode 100644 index 0000000000000..96d98f6cc6ca8 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/index.ts @@ -0,0 +1,21 @@ +import type * as Internal from "./convert"; +import type * as Metadata from "./specMetadata"; +import { revertSubcommand } from "./revert"; +import { convertSubcommand } from "./convert"; +import { convertLoadSpec, initializeDefault } from "./specMetadata"; +import { SpecMixin, applyMixin, mergeSubcommands } from "./mixins"; +import { SpecLocationSource, makeArray } from "./utils"; + +export { + Internal, + revertSubcommand, + convertSubcommand, + Metadata, + convertLoadSpec, + initializeDefault, + SpecMixin, + applyMixin, + mergeSubcommands, + makeArray, + SpecLocationSource, +}; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/mixins.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/mixins.ts new file mode 100644 index 0000000000000..0363bec24452d --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/mixins.ts @@ -0,0 +1,147 @@ +import { makeArray } from "./utils"; + +export type SpecMixin = + | Fig.Subcommand + | ((currentSpec: Fig.Subcommand, context: Fig.ShellContext) => Fig.Subcommand); + +type NamedObject = { name: Fig.SingleOrArray }; + +const concatArrays = (a: T[] | undefined, b: T[] | undefined): T[] | undefined => + a && b ? [...a, ...b] : a || b; + +const mergeNames = (a: T | T[], b: T | T[]): T | T[] => [ + ...new Set(concatArrays(makeArray(a), makeArray(b))), +]; + +const mergeArrays = (a: T[] | undefined, b: T[] | undefined): T[] | undefined => + a && b ? [...new Set(concatArrays(makeArray(a), makeArray(b)))] : a || b; + +const mergeArgs = (arg: Fig.Arg, partial: Fig.Arg): Fig.Arg => ({ + ...arg, + ...partial, + suggestions: concatArrays(arg.suggestions, partial.suggestions), + generators: + arg.generators && partial.generators + ? concatArrays(makeArray(arg.generators), makeArray(partial.generators)) + : arg.generators || partial.generators, + template: + arg.template && partial.template + ? mergeNames(arg.template, partial.template) + : arg.template || partial.template, +}); + +const mergeArgArrays = ( + args: Fig.SingleOrArray | undefined, + partials: Fig.SingleOrArray | undefined +): Fig.SingleOrArray | undefined => { + if (!args || !partials) { + return args || partials; + } + const argArray = makeArray(args); + const partialArray = makeArray(partials); + const result = []; + for (let i = 0; i < Math.max(argArray.length, partialArray.length); i += 1) { + const arg = argArray[i]; + const partial = partialArray[i]; + if (arg !== undefined && partial !== undefined) { + result.push(mergeArgs(arg, partial)); + } else if (partial !== undefined || arg !== undefined) { + result.push(arg || partial); + } + } + return result.length === 1 ? result[0] : result; +}; + +const mergeOptions = (option: Fig.Option, partial: Fig.Option): Fig.Option => ({ + ...option, + ...partial, + name: mergeNames(option.name, partial.name), + args: mergeArgArrays(option.args, partial.args), + exclusiveOn: mergeArrays(option.exclusiveOn, partial.exclusiveOn), + dependsOn: mergeArrays(option.dependsOn, partial.dependsOn), +}); + +const mergeNamedObjectArrays = ( + objects: T[] | undefined, + partials: T[] | undefined, + mergeItems: (a: T, b: T) => T +): T[] | undefined => { + if (!objects || !partials) { + return objects || partials; + } + const mergedObjects = objects ? [...objects] : []; + + const existingNameIndexMap: Record = {}; + for (let i = 0; i < objects.length; i += 1) { + makeArray(objects[i].name).forEach((name) => { + existingNameIndexMap[name] = i; + }); + } + + for (let i = 0; i < partials.length; i += 1) { + const partial = partials[i]; + if (!partial) { + throw new Error("Invalid object passed to merge"); + } + const existingNames = makeArray(partial.name).filter((name) => name in existingNameIndexMap); + if (existingNames.length === 0) { + mergedObjects.push(partial); + } else { + const index = existingNameIndexMap[existingNames[0]]; + if (existingNames.some((name) => existingNameIndexMap[name] !== index)) { + throw new Error("Names provided for option matched multiple existing options"); + } + mergedObjects[index] = mergeItems(mergedObjects[index], partial); + } + } + return mergedObjects; +}; + +function mergeOptionArrays( + options: Fig.Option[] | undefined, + partials: Fig.Option[] | undefined +): Fig.Option[] | undefined { + return mergeNamedObjectArrays(options, partials, mergeOptions); +} + +function mergeSubcommandArrays( + subcommands: Fig.Subcommand[] | undefined, + partials: Fig.Subcommand[] | undefined +): Fig.Subcommand[] | undefined { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return mergeNamedObjectArrays(subcommands, partials, mergeSubcommands); +} + +export function mergeSubcommands( + subcommand: Fig.Subcommand, + partial: Fig.Subcommand +): Fig.Subcommand { + return { + ...subcommand, + ...partial, + name: mergeNames(subcommand.name, partial.name), + args: mergeArgArrays(subcommand.args, partial.args), + additionalSuggestions: concatArrays( + subcommand.additionalSuggestions, + partial.additionalSuggestions + ), + subcommands: mergeSubcommandArrays(subcommand.subcommands, partial.subcommands), + options: mergeOptionArrays(subcommand.options, partial.options), + parserDirectives: + subcommand.parserDirectives && partial.parserDirectives + ? { ...subcommand.parserDirectives, ...partial.parserDirectives } + : subcommand.parserDirectives || partial.parserDirectives, + }; +} + +export const applyMixin = ( + spec: Fig.Subcommand, + context: Fig.ShellContext, + mixin: SpecMixin +): Fig.Subcommand => { + if (typeof mixin === "function") { + return mixin(spec, context); + } + const partial = mixin; + return mergeSubcommands(spec, partial); +}; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/revert.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/revert.ts new file mode 100644 index 0000000000000..0ce6679bfe004 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/revert.ts @@ -0,0 +1,41 @@ +import { Option, Subcommand } from "./convert"; + +function makeSingleOrArray(arr: T[]): Fig.SingleOrArray { + return arr.length === 1 ? (arr[0] as Fig.SingleOrArray) : (arr as Fig.SingleOrArray); +} + +function revertOption(option: Option): Fig.Option { + const { name, args } = option; + + return { + name: makeSingleOrArray(name), + args, + }; +} + +export function revertSubcommand( + subcommand: Subcommand, + postProcessingFn: ( + oldSub: Subcommand, + newSub: Fig.Subcommand + ) => Fig.Subcommand +): Fig.Subcommand { + const { name, subcommands, options, persistentOptions, args } = subcommand; + + const newSubcommand: Fig.Subcommand = { + name: makeSingleOrArray(name), + subcommands: + Object.values(subcommands).length !== 0 + ? Object.values(subcommands).map((sub) => revertSubcommand(sub, postProcessingFn)) + : undefined, + options: + Object.values(options).length !== 0 + ? [ + ...Object.values(options).map((option) => revertOption(option)), + ...Object.values(persistentOptions).map((option) => revertOption(option)), + ] + : undefined, + args: Object.values(args).length !== 0 ? makeSingleOrArray(Object.values(args)) : undefined, + }; + return postProcessingFn(subcommand, newSubcommand); +} diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/specMetadata.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/specMetadata.ts new file mode 100644 index 0000000000000..3efaee8379175 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/specMetadata.ts @@ -0,0 +1,105 @@ +import { Subcommand, convertSubcommand, Initializer } from "./convert"; +import { makeArray, SpecLocationSource } from "./utils"; + +// eslint-disable-next-line @typescript-eslint/ban-types +type FigLoadSpecFn = Fig.LoadSpec extends infer U ? (U extends Function ? U : never) : never; +export type LoadSpec = + | Fig.SpecLocation[] + | Subcommand + | (( + ...args: Parameters + ) => Promise>); + +export type OptionMeta = Omit; +export type ArgMeta = Omit & { + generators: Fig.Generator[]; + loadSpec?: LoadSpec; +}; + +type SubcommandMetaExcludes = + | "subcommands" + | "options" + | "loadSpec" + | "persistentOptions" + | "args" + | "name"; +export type SubcommandMeta = Omit & { + loadSpec?: LoadSpec; +}; + +export function convertLoadSpec( + loadSpec: Fig.LoadSpec, + initialize: Initializer +): LoadSpec { + if (typeof loadSpec === "string") { + return [{ name: loadSpec, type: SpecLocationSource.GLOBAL }]; + } + + if (typeof loadSpec === "function") { + return (...args) => + loadSpec(...args).then((result) => { + if (Array.isArray(result)) { + return result; + } + if ("type" in result) { + return [result]; + } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return convertSubcommand(result, initialize); + }); + } + + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return convertSubcommand(loadSpec, initialize); +} + +function initializeOptionMeta(option: Fig.Option): OptionMeta { + return option; +} + +// Default initialization functions: +function initializeArgMeta(arg: Fig.Arg): ArgMeta { + const { template, ...rest } = arg; + const generators = template ? [{ template }] : makeArray(arg.generators ?? []); + return { + ...rest, + loadSpec: arg.loadSpec + ? convertLoadSpec(arg.loadSpec, { + option: initializeOptionMeta, + // eslint-disable-next-line @typescript-eslint/no-use-before-define + subcommand: initializeSubcommandMeta, + arg: initializeArgMeta, + }) + : undefined, + generators: generators.map((generator) => { + let { trigger, getQueryTerm } = generator; + if (generator.template) { + const templates = makeArray(generator.template); + if (templates.includes("folders") || templates.includes("filepaths")) { + trigger = trigger ?? "/"; + getQueryTerm = getQueryTerm ?? "/"; + } + } + return { ...generator, trigger, getQueryTerm }; + }), + }; +} + +function initializeSubcommandMeta(subcommand: Fig.Subcommand): SubcommandMeta { + return { + ...subcommand, + loadSpec: subcommand.loadSpec + ? convertLoadSpec(subcommand.loadSpec, { + subcommand: initializeSubcommandMeta, + option: initializeOptionMeta, + arg: initializeArgMeta, + }) + : undefined, + }; +} + +export const initializeDefault: Initializer = { + subcommand: initializeSubcommandMeta, + option: initializeOptionMeta, + arg: initializeArgMeta, +}; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/utils.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/utils.ts new file mode 100644 index 0000000000000..1e9afcefa3e2c --- /dev/null +++ b/extensions/terminal-suggest/src/fig/autocomplete-shared/src/utils.ts @@ -0,0 +1,8 @@ +export function makeArray(object: T | T[]): T[] { + return Array.isArray(object) ? object : [object]; +} + +export enum SpecLocationSource { + GLOBAL = "global", + LOCAL = "local", +} diff --git a/extensions/terminal-suggest/src/fig/shared/src/internal.d.ts b/extensions/terminal-suggest/src/fig/shared/src/internal.d.ts deleted file mode 100644 index 92a0cbdf21ce2..0000000000000 --- a/extensions/terminal-suggest/src/fig/shared/src/internal.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Internal, Metadata } from "@fig/autocomplete-shared"; -import { Result } from "../../shared/src/fuzzysort"; -export type SpecLocation = Fig.SpecLocation & { - diffVersionedFile?: string; - privateNamespaceId?: number; -}; -type Override = Omit & S; -export type SuggestionType = Fig.SuggestionType | "history" | "auto-execute"; -export type Suggestion = Override string; - fuzzyMatchData?: (Result | null)[]; - originalType?: SuggestionType; -}>; -export type Arg = Metadata.ArgMeta; -export type Option = Internal.Option; -export type Subcommand = Internal.Subcommand; -export { }; diff --git a/extensions/terminal-suggest/src/fig/shared/src/internal.ts b/extensions/terminal-suggest/src/fig/shared/src/internal.ts index d684bbe34dbb9..3a1ae7ccec227 100644 --- a/extensions/terminal-suggest/src/fig/shared/src/internal.ts +++ b/extensions/terminal-suggest/src/fig/shared/src/internal.ts @@ -1,4 +1,4 @@ -import { Internal, Metadata } from "@fig/autocomplete-shared"; +import { Internal, Metadata } from "../../autocomplete-shared/src"; import type { Result } from "./fuzzysort"; export type SpecLocation = Fig.SpecLocation & { diff --git a/extensions/terminal-suggest/src/fig/shared/src/settings.ts b/extensions/terminal-suggest/src/fig/shared/src/settings.ts deleted file mode 100644 index d56048e9fc55c..0000000000000 --- a/extensions/terminal-suggest/src/fig/shared/src/settings.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { SettingsChangedNotification } from "@aws/amazon-q-developer-cli-proto/fig"; -export declare const didChange: { - // subscribe(handler: (notification: SettingsChangedNotification) => NotificationResponse | undefined): Promise | undefined; - //TODO@meganrogge - subscribe(handler: (notification: SettingsChangedNotification) => NotificationResponse | undefined): Promise | undefined; -}; -export declare function get(key: string): Promise; -export declare function set(key: string, value: unknown): Promise; -export declare function remove(key: string): Promise; -export declare function current(): Promise; - -export type NotificationResponse = { - unsubscribe: boolean; - }; - diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 78673131e8fcd..754085d23c545 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -19,7 +19,7 @@ import { getPwshGlobals } from './shell/pwsh'; import { getTokenType, TokenType } from './tokens'; import { PathExecutableCache } from './env/pathExecutableCache'; import { getFriendlyResourcePath } from './helpers/uri'; -import { parseArguments } from './fig/autocomplete-parser/src'; +import { parseArguments } from './fig/autocomplete-parser/src/parseArguments'; import { getCommand } from './fig/shell-parser/src/command'; // TODO: remove once API is finalized From bd5797889698bec3d768a8f99ece00263ea3572e Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Wed, 5 Feb 2025 14:13:36 -0600 Subject: [PATCH 08/51] get arg parsing to work Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com> --- .../src/fig/autocomplete-parser/src/parseArguments.ts | 6 ++++-- extensions/terminal-suggest/src/terminalSuggestMain.ts | 4 ++++ extensions/terminal-suggest/tsconfig.json | 9 ++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts index c3c448ae252d3..860edb90c0d65 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts @@ -1,4 +1,5 @@ // import { filepaths, folders } from "@fig/autocomplete-generators"; +import codeCompletionSpec from '../../../completions/code'; import * as Internal from "../../shared/src/internal"; import { firstMatchingToken, @@ -28,6 +29,7 @@ import { ParsingHistoryError, UpdateStateError, } from "./errors.js"; +import { convertSubcommand, initializeDefault } from '../../autocomplete-shared/src'; const { exec } = require("child_process"); type ArgArrayState = { @@ -796,7 +798,7 @@ const parseArgumentsCached = async ( // const tokenText = tokens.map((token) => token.text); // TODO: Fill this in the with actual one (replace specLocations) - const spec: Fig.Spec = null!; + const spec = codeCompletionSpec; // null!; const specPath: Fig.SpecLocation = { type: 'global', name: 'fake' }; // tokenTest[0] is the command and the spec they need @@ -855,7 +857,7 @@ const parseArgumentsCached = async ( } let state: ArgumentParserState = getInitialState( - spec, + convertSubcommand(spec, initializeDefault), tokens[0].text, specPath, ); diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 754085d23c545..63149ca7670e2 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -112,6 +112,10 @@ export async function activate(context: vscode.ExtensionContext) { } } } + // to do figure out spec, pass that in only if it's valid + // replacing get options/args from spec with the parsed arguments + // to do: items for folders/files + // use suggestion flags to determine which to provide const parsedArguments = await parseArguments(getCommand(terminalContext.commandLine, {}, terminalContext.cursorPosition), { environmentVariables: env, currentWorkingDirectory: terminal.shellIntegration!.cwd!.fsPath, sshPrefix: '', currentProcess: terminal.name }); console.log(parsedArguments); const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index ac8998b8d29de..f3d3aa73975cf 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -6,16 +6,11 @@ "node" ], "target": "es2020", - "module": "ES2020", + "module": "CommonJS", "moduleResolution": "node", "strict": true, "esModuleInterop": true, - // "skipLibCheck": true, - "lib": [ - "es2018", - "DOM", - "DOM.Iterable" - ], + "skipLibCheck": true, // Needed to suppress warnings in upstream completions "noImplicitReturns": false, From 42d7d0b6c7b335feb2cfd3f5e9b09110f1efda83 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 01:54:32 -0800 Subject: [PATCH 09/51] Add tests for shell-parser --- .../fixtures/shell-parser/basic/input.sh | 29 + .../fixtures/shell-parser/basic/output.txt | 448 +++ .../shell-parser/multipleStatements/input.sh | 47 + .../multipleStatements/output.txt | 1035 +++++++ .../shell-parser/primaryExpressions/input.sh | 35 + .../primaryExpressions/output.txt | 724 +++++ .../fixtures/shell-parser/variables/input.sh | 77 + .../shell-parser/variables/output.txt | 2439 +++++++++++++++++ .../src/fig/shell-parser/test/command.test.ts | 33 + .../src/fig/shell-parser/test/parser.test.ts | 100 + 10 files changed, 4967 insertions(+) create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/basic/input.sh create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/basic/output.txt create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/input.sh create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/output.txt create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/input.sh create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/output.txt create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/variables/input.sh create mode 100644 extensions/terminal-suggest/fixtures/shell-parser/variables/output.txt create mode 100644 extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts create mode 100644 extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts diff --git a/extensions/terminal-suggest/fixtures/shell-parser/basic/input.sh b/extensions/terminal-suggest/fixtures/shell-parser/basic/input.sh new file mode 100644 index 0000000000000..72075545c0568 --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/basic/input.sh @@ -0,0 +1,29 @@ +### Case 1 +a b\\ c + +### Case 2 +a "b" + +### Case 3 +a 'b' + +### Case 4 +a $'b' + +### Case 5 +a $commit + +### Case 6 +a $$ + +### Case 7 +a $((b)) + +### Case 8 +a $(b) + +### Case 9 +a \`b\` + +### Case 10 +a $(\`b\`) diff --git a/extensions/terminal-suggest/fixtures/shell-parser/basic/output.txt b/extensions/terminal-suggest/fixtures/shell-parser/basic/output.txt new file mode 100644 index 0000000000000..8e816c5fe1846 --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/basic/output.txt @@ -0,0 +1,448 @@ +// Case 1 +{ + "startIndex": 0, + "type": "program", + "endIndex": 7, + "text": "a b\\\\ c", + "innerText": "a b\\\\ c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 7, + "text": "a b\\\\ c", + "innerText": "a b\\\\ c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "word", + "endIndex": 5, + "text": "b\\\\", + "innerText": "b\\", + "complete": true, + "children": [] + }, + { + "startIndex": 6, + "type": "word", + "endIndex": 7, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 2 +{ + "startIndex": 0, + "type": "program", + "endIndex": 5, + "text": "a \"b\"", + "innerText": "a \"b\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 5, + "text": "a \"b\"", + "innerText": "a \"b\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "string", + "endIndex": 5, + "text": "\"b\"", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 3 +{ + "startIndex": 0, + "type": "program", + "endIndex": 5, + "text": "a 'b'", + "innerText": "a 'b'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 5, + "text": "a 'b'", + "innerText": "a 'b'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "raw_string", + "endIndex": 5, + "text": "'b'", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 4 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a $'b'", + "innerText": "a $'b'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 6, + "text": "a $'b'", + "innerText": "a $'b'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "ansi_c_string", + "endIndex": 6, + "text": "$'b'", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 5 +{ + "startIndex": 0, + "type": "program", + "endIndex": 9, + "text": "a $commit", + "innerText": "a $commit", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 9, + "text": "a $commit", + "innerText": "a $commit", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "simple_expansion", + "endIndex": 9, + "text": "$commit", + "innerText": "$commit", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 6 +{ + "startIndex": 0, + "type": "program", + "endIndex": 4, + "text": "a $$", + "innerText": "a $$", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 4, + "text": "a $$", + "innerText": "a $$", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "special_expansion", + "endIndex": 4, + "text": "$$", + "innerText": "$$", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 7 +{ + "startIndex": 0, + "type": "program", + "endIndex": 8, + "text": "a $((b))", + "innerText": "a $((b))", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 8, + "text": "a $((b))", + "innerText": "a $((b))", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "arithmetic_expansion", + "endIndex": 8, + "text": "$((b))", + "innerText": "$((b))", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 8 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a $(b)", + "innerText": "a $(b)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 6, + "text": "a $(b)", + "innerText": "a $(b)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "command_substitution", + "endIndex": 6, + "text": "$(b)", + "innerText": "$(b)", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "command", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] + } + ] +} + +// Case 9 +{ + "startIndex": 0, + "type": "program", + "endIndex": 7, + "text": "a \\`b\\`", + "innerText": "a \\`b\\`", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 7, + "text": "a \\`b\\`", + "innerText": "a \\`b\\`", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 3, + "type": "word", + "endIndex": 7, + "text": "`b\\`", + "innerText": "`b`", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 10 +{ + "startIndex": 0, + "type": "program", + "endIndex": 10, + "text": "a $(\\`b\\`)", + "innerText": "a $(\\`b\\`)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 10, + "text": "a $(\\`b\\`)", + "innerText": "a $(\\`b\\`)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "command_substitution", + "endIndex": 10, + "text": "$(\\`b\\`)", + "innerText": "$(\\`b\\`)", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "command", + "endIndex": 9, + "text": "\\`b\\`", + "innerText": "\\`b\\`", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 9, + "text": "`b\\`", + "innerText": "`b`", + "complete": true, + "children": [] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/input.sh b/extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/input.sh new file mode 100644 index 0000000000000..ba6858ea1a593 --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/input.sh @@ -0,0 +1,47 @@ +### Case 1 +a && b + +### Case 2 +a || b + +### Case 3 +a | b + +### Case 4 +a |& b + +### Case 5 +(a; b) + +### Case 6 +(a; b;) + +### Case 7 +{a; b} + +### Case 8 +{a; b;} + +### Case 9 +a; b + +### Case 10 +a & b + +### Case 11 +a &; b + +### Case 12 +a ; b; + +### Case 13 +a && b || c + +### Case 14 +a && b | c + +### Case 15 +a | b && c + +### Case 16 +(a) | b && c \ No newline at end of file diff --git a/extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/output.txt b/extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/output.txt new file mode 100644 index 0000000000000..624f016637189 --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/multipleStatements/output.txt @@ -0,0 +1,1035 @@ +// Case 1 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a && b", + "innerText": "a && b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 6, + "text": "a && b", + "innerText": "a && b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 5, + "type": "command", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 2 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a || b", + "innerText": "a || b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 6, + "text": "a || b", + "innerText": "a || b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 5, + "type": "command", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 3 +{ + "startIndex": 0, + "type": "program", + "endIndex": 5, + "text": "a | b", + "innerText": "a | b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "pipeline", + "endIndex": 5, + "text": "a | b", + "innerText": "a | b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 4 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a |& b", + "innerText": "a |& b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "pipeline", + "endIndex": 6, + "text": "a |& b", + "innerText": "a |& b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 5, + "type": "command", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 5 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "(a; b)", + "innerText": "(a; b)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "subshell", + "endIndex": 6, + "text": "(a; b)", + "innerText": "(a; b)", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "command", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "word", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 6 +{ + "startIndex": 0, + "type": "program", + "endIndex": 7, + "text": "(a; b;)", + "innerText": "(a; b;)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "subshell", + "endIndex": 7, + "text": "(a; b;)", + "innerText": "(a; b;)", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "command", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "word", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 7 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "{a; b}", + "innerText": "{a; b}", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "compound_statement", + "endIndex": 6, + "text": "{a; b}", + "innerText": "{a; b}", + "complete": false, + "children": [ + { + "startIndex": 1, + "type": "command", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "word", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 6, + "text": "b}", + "innerText": "b}", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 6, + "text": "b}", + "innerText": "b}", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 8 +{ + "startIndex": 0, + "type": "program", + "endIndex": 7, + "text": "{a; b;}", + "innerText": "{a; b;}", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "compound_statement", + "endIndex": 7, + "text": "{a; b;}", + "innerText": "{a; b;}", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "command", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "word", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 9 +{ + "startIndex": 0, + "type": "program", + "endIndex": 4, + "text": "a; b", + "innerText": "a; b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 3, + "type": "command", + "endIndex": 4, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 3, + "type": "word", + "endIndex": 4, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 10 +{ + "startIndex": 0, + "type": "program", + "endIndex": 5, + "text": "a & b", + "innerText": "a & b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 11 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a &; b", + "innerText": "a &; b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 5, + "type": "command", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 12 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a ; b;", + "innerText": "a ; b;", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 13 +{ + "startIndex": 0, + "type": "program", + "endIndex": 11, + "text": "a && b || c", + "innerText": "a && b || c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 11, + "text": "a && b || c", + "innerText": "a && b || c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 5, + "type": "command", + "endIndex": 7, + "text": "b ", + "innerText": "b ", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 10, + "type": "command", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 14 +{ + "startIndex": 0, + "type": "program", + "endIndex": 10, + "text": "a && b | c", + "innerText": "a && b | c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 10, + "text": "a && b | c", + "innerText": "a && b | c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 5, + "type": "pipeline", + "endIndex": 10, + "text": "b | c", + "innerText": "b | c", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "command", + "endIndex": 7, + "text": "b ", + "innerText": "b ", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 9, + "type": "command", + "endIndex": 10, + "text": "c", + "innerText": "c", + "complete": true, + "children": [ + { + "startIndex": 9, + "type": "word", + "endIndex": 10, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ] + } + ] + } + ] +} + +// Case 15 +{ + "startIndex": 0, + "type": "program", + "endIndex": 10, + "text": "a | b && c", + "innerText": "a | b && c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 10, + "text": "a | b && c", + "innerText": "a | b && c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "pipeline", + "endIndex": 6, + "text": "a | b ", + "innerText": "a | b ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 2, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 4, + "type": "command", + "endIndex": 6, + "text": "b ", + "innerText": "b ", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + }, + { + "startIndex": 9, + "type": "command", + "endIndex": 10, + "text": "c", + "innerText": "c", + "complete": true, + "children": [ + { + "startIndex": 9, + "type": "word", + "endIndex": 10, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 16 +{ + "startIndex": 0, + "type": "program", + "endIndex": 12, + "text": "(a) | b && c", + "innerText": "(a) | b && c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 12, + "text": "(a) | b && c", + "innerText": "(a) | b && c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "pipeline", + "endIndex": 8, + "text": "(a) | b ", + "innerText": "(a) | b ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "subshell", + "endIndex": 3, + "text": "(a)", + "innerText": "(a)", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "command", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 1, + "type": "word", + "endIndex": 2, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + } + ] + }, + { + "startIndex": 6, + "type": "command", + "endIndex": 8, + "text": "b ", + "innerText": "b ", + "complete": true, + "children": [ + { + "startIndex": 6, + "type": "word", + "endIndex": 7, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + }, + { + "startIndex": 11, + "type": "command", + "endIndex": 12, + "text": "c", + "innerText": "c", + "complete": true, + "children": [ + { + "startIndex": 11, + "type": "word", + "endIndex": 12, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/input.sh b/extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/input.sh new file mode 100644 index 0000000000000..d2c5977e3185f --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/input.sh @@ -0,0 +1,35 @@ +### Case 1 +a "\${b}" + +### Case 2 +a "'b'" + +### Case 3 +a "\${b:+"c"}" + +### Case 4 +a b"c" + +### Case 5 +a '\${b}' + +### Case 6 +a $'\${b}' + +### Case 7 +a $'b''c'd$$$e\${f}"g" + +### Case 8 +a $'b\\'c' + +### Case 9 +a 'b\\'c' + +### Case 10 +a "b$" + +### Case 11 +a "$b" + +### Case 12 +a "$(b "c" && d)" \ No newline at end of file diff --git a/extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/output.txt b/extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/output.txt new file mode 100644 index 0000000000000..4783411a82b74 --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/primaryExpressions/output.txt @@ -0,0 +1,724 @@ +// Case 1 +{ + "startIndex": 0, + "type": "program", + "endIndex": 9, + "text": "a \"\\${b}\"", + "innerText": "a \"\\${b}\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 9, + "text": "a \"\\${b}\"", + "innerText": "a \"\\${b}\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "string", + "endIndex": 9, + "text": "\"\\${b}\"", + "innerText": "${b}", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 2 +{ + "startIndex": 0, + "type": "program", + "endIndex": 7, + "text": "a \"'b'\"", + "innerText": "a \"'b'\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 7, + "text": "a \"'b'\"", + "innerText": "a \"'b'\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "string", + "endIndex": 7, + "text": "\"'b'\"", + "innerText": "'b'", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 3 +{ + "startIndex": 0, + "type": "program", + "endIndex": 14, + "text": "a \"\\${b:+\"c\"}\"", + "innerText": "a \"\\${b:+\"c\"}\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 14, + "text": "a \"\\${b:+\"c\"}\"", + "innerText": "a \"\\${b:+\"c\"}\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "concatenation", + "endIndex": 14, + "text": "\"\\${b:+\"c\"}\"", + "innerText": "${b:+c}", + "complete": true, + "children": [ + { + "startIndex": 2, + "type": "string", + "endIndex": 10, + "text": "\"\\${b:+\"", + "innerText": "${b:+", + "complete": true, + "children": [] + }, + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + }, + { + "startIndex": 11, + "type": "string", + "endIndex": 14, + "text": "\"}\"", + "innerText": "}", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 4 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a b\"c\"", + "innerText": "a b\"c\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 6, + "text": "a b\"c\"", + "innerText": "a b\"c\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "concatenation", + "endIndex": 6, + "text": "b\"c\"", + "innerText": "bc", + "complete": true, + "children": [ + { + "startIndex": 2, + "type": "word", + "endIndex": 3, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + }, + { + "startIndex": 3, + "type": "string", + "endIndex": 6, + "text": "\"c\"", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 5 +{ + "startIndex": 0, + "type": "program", + "endIndex": 9, + "text": "a '\\${b}'", + "innerText": "a '\\${b}'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 9, + "text": "a '\\${b}'", + "innerText": "a '\\${b}'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "raw_string", + "endIndex": 9, + "text": "'\\${b}'", + "innerText": "\\${b}", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 6 +{ + "startIndex": 0, + "type": "program", + "endIndex": 10, + "text": "a $'\\${b}'", + "innerText": "a $'\\${b}'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 10, + "text": "a $'\\${b}'", + "innerText": "a $'\\${b}'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "ansi_c_string", + "endIndex": 10, + "text": "$'\\${b}'", + "innerText": "\\${b}", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 7 +{ + "startIndex": 0, + "type": "program", + "endIndex": 22, + "text": "a $'b''c'd$$$e\\${f}\"g\"", + "innerText": "a $'b''c'd$$$e\\${f}\"g\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 22, + "text": "a $'b''c'd$$$e\\${f}\"g\"", + "innerText": "a $'b''c'd$$$e\\${f}\"g\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "concatenation", + "endIndex": 22, + "text": "$'b''c'd$$$e\\${f}\"g\"", + "innerText": "bcd$$$e${f}g", + "complete": true, + "children": [ + { + "startIndex": 2, + "type": "ansi_c_string", + "endIndex": 6, + "text": "$'b'", + "innerText": "b", + "complete": true, + "children": [] + }, + { + "startIndex": 6, + "type": "raw_string", + "endIndex": 9, + "text": "'c'", + "innerText": "c", + "complete": true, + "children": [] + }, + { + "startIndex": 9, + "type": "word", + "endIndex": 10, + "text": "d", + "innerText": "d", + "complete": true, + "children": [] + }, + { + "startIndex": 10, + "type": "special_expansion", + "endIndex": 12, + "text": "$$", + "innerText": "$$", + "complete": true, + "children": [] + }, + { + "startIndex": 12, + "type": "simple_expansion", + "endIndex": 14, + "text": "$e", + "innerText": "$e", + "complete": true, + "children": [] + }, + { + "startIndex": 15, + "type": "word", + "endIndex": 19, + "text": "${f}", + "innerText": "${f}", + "complete": true, + "children": [] + }, + { + "startIndex": 19, + "type": "string", + "endIndex": 22, + "text": "\"g\"", + "innerText": "g", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 8 +{ + "startIndex": 0, + "type": "program", + "endIndex": 10, + "text": "a $'b\\\\'c'", + "innerText": "a $'b\\\\'c'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 10, + "text": "a $'b\\\\'c'", + "innerText": "a $'b\\\\'c'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "concatenation", + "endIndex": 10, + "text": "$'b\\\\'c'", + "innerText": "b\\\\c", + "complete": false, + "children": [ + { + "startIndex": 2, + "type": "ansi_c_string", + "endIndex": 8, + "text": "$'b\\\\'", + "innerText": "b\\\\", + "complete": true, + "children": [] + }, + { + "startIndex": 8, + "type": "word", + "endIndex": 9, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + }, + { + "startIndex": 9, + "type": "raw_string", + "endIndex": 10, + "text": "'", + "innerText": "", + "complete": false, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 9 +{ + "startIndex": 0, + "type": "program", + "endIndex": 9, + "text": "a 'b\\\\'c'", + "innerText": "a 'b\\\\'c'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 9, + "text": "a 'b\\\\'c'", + "innerText": "a 'b\\\\'c'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "concatenation", + "endIndex": 9, + "text": "'b\\\\'c'", + "innerText": "b\\\\c", + "complete": false, + "children": [ + { + "startIndex": 2, + "type": "raw_string", + "endIndex": 7, + "text": "'b\\\\'", + "innerText": "b\\\\", + "complete": true, + "children": [] + }, + { + "startIndex": 7, + "type": "word", + "endIndex": 8, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + }, + { + "startIndex": 8, + "type": "raw_string", + "endIndex": 9, + "text": "'", + "innerText": "", + "complete": false, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 10 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a \"b$\"", + "innerText": "a \"b$\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 6, + "text": "a \"b$\"", + "innerText": "a \"b$\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "string", + "endIndex": 6, + "text": "\"b$\"", + "innerText": "b$", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 11 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "a \"$b\"", + "innerText": "a \"$b\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 6, + "text": "a \"$b\"", + "innerText": "a \"$b\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "string", + "endIndex": 6, + "text": "\"$b\"", + "innerText": "$b", + "complete": true, + "children": [ + { + "startIndex": 3, + "type": "simple_expansion", + "endIndex": 5, + "text": "$b", + "innerText": "$b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 12 +{ + "startIndex": 0, + "type": "program", + "endIndex": 17, + "text": "a \"$(b \"c\" && d)\"", + "innerText": "a \"$(b \"c\" && d)\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 17, + "text": "a \"$(b \"c\" && d)\"", + "innerText": "a \"$(b \"c\" && d)\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "string", + "endIndex": 17, + "text": "\"$(b \"c\" && d)\"", + "innerText": "$(b \"c\" && d)", + "complete": true, + "children": [ + { + "startIndex": 3, + "type": "command_substitution", + "endIndex": 16, + "text": "$(b \"c\" && d)", + "innerText": "$(b \"c\" && d)", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "list", + "endIndex": 15, + "text": "b \"c\" && d", + "innerText": "b \"c\" && d", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "command", + "endIndex": 11, + "text": "b \"c\" ", + "innerText": "b \"c\" ", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + }, + { + "startIndex": 7, + "type": "string", + "endIndex": 10, + "text": "\"c\"", + "innerText": "c", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 14, + "type": "command", + "endIndex": 15, + "text": "d", + "innerText": "d", + "complete": true, + "children": [ + { + "startIndex": 14, + "type": "word", + "endIndex": 15, + "text": "d", + "innerText": "d", + "complete": true, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/extensions/terminal-suggest/fixtures/shell-parser/variables/input.sh b/extensions/terminal-suggest/fixtures/shell-parser/variables/input.sh new file mode 100644 index 0000000000000..30b8788a90f2b --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/variables/input.sh @@ -0,0 +1,77 @@ +### Case 1 +ENV=a b + +### Case 2 +ENV=a b c d --op=e + +### Case 3 +ENV=a ENV=b a + +### Case 4 +ENV=a ENV=b a && ENV=c c + +### Case 5 +ENV="a b" c + +### Case 6 +ENV='a b' c + +### Case 7 +ENV=`cmd` a + +### Case 8 +ENV+='100' b + +### Case 9 +ENV+=a ENV=b + +### Case 10 +ENV+=a ENV=b && foo + +### Case 11 +ENV="a + +### Case 12 +ENV='a + +### Case 13 +ENV=a ENV=`b + +### Case 14 +ENV=`ENV="a" b` && ENV="c" d + +### Case 15 +c $(ENV=a foo) + +### Case 16 +ENV=a; b + +### Case 17 +ENV=a ; b + +### Case 18 +ENV=a & b + +### Case 19 +ENV=a|b + +### Case 20 +ENV[0]=a b + +### Case 21 +ENV[0]=a; b + +### Case 22 +ENV[1]=`a b + +### Case 23 +ENV[2]+="a b " + +### Case 24 +MY_VAR='echo'hi$'quote'"command: $(ps | VAR=2 grep ps)" + +### Case 25 +ENV="a"'b'c d + +### Case 26 +ENV=a"b"'c' \ No newline at end of file diff --git a/extensions/terminal-suggest/fixtures/shell-parser/variables/output.txt b/extensions/terminal-suggest/fixtures/shell-parser/variables/output.txt new file mode 100644 index 0000000000000..9cbf4ab2ffb05 --- /dev/null +++ b/extensions/terminal-suggest/fixtures/shell-parser/variables/output.txt @@ -0,0 +1,2439 @@ +// Case 1 +{ + "startIndex": 0, + "type": "program", + "endIndex": 7, + "text": "ENV=a b", + "innerText": "ENV=a b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 7, + "text": "ENV=a b", + "innerText": "ENV=a b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 6, + "type": "command", + "endIndex": 7, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 6, + "type": "word", + "endIndex": 7, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 2 +{ + "startIndex": 0, + "type": "program", + "endIndex": 18, + "text": "ENV=a b c d --op=e", + "innerText": "ENV=a b c d --op=e", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 18, + "text": "ENV=a b c d --op=e", + "innerText": "ENV=a b c d --op=e", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 6, + "type": "command", + "endIndex": 18, + "text": "b c d --op=e", + "innerText": "b c d --op=e", + "complete": true, + "children": [ + { + "startIndex": 6, + "type": "word", + "endIndex": 7, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + }, + { + "startIndex": 8, + "type": "word", + "endIndex": 9, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + }, + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "d", + "innerText": "d", + "complete": true, + "children": [] + }, + { + "startIndex": 12, + "type": "word", + "endIndex": 18, + "text": "--op=e", + "innerText": "--op=e", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 3 +{ + "startIndex": 0, + "type": "program", + "endIndex": 13, + "text": "ENV=a ENV=b a", + "innerText": "ENV=a ENV=b a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 13, + "text": "ENV=a ENV=b a", + "innerText": "ENV=a ENV=b a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 6, + "type": "assignment", + "endIndex": 11, + "text": "ENV=b", + "innerText": "ENV=b", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 6, + "type": "variable_name", + "endIndex": 9, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 12, + "type": "command", + "endIndex": 13, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 12, + "type": "word", + "endIndex": 13, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 4 +{ + "startIndex": 0, + "type": "program", + "endIndex": 24, + "text": "ENV=a ENV=b a && ENV=c c", + "innerText": "ENV=a ENV=b a && ENV=c c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 24, + "text": "ENV=a ENV=b a && ENV=c c", + "innerText": "ENV=a ENV=b a && ENV=c c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 14, + "text": "ENV=a ENV=b a ", + "innerText": "ENV=a ENV=b a ", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 6, + "type": "assignment", + "endIndex": 11, + "text": "ENV=b", + "innerText": "ENV=b", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 6, + "type": "variable_name", + "endIndex": 9, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 12, + "type": "command", + "endIndex": 14, + "text": "a ", + "innerText": "a ", + "complete": true, + "children": [ + { + "startIndex": 12, + "type": "word", + "endIndex": 13, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + }, + { + "startIndex": 17, + "type": "assignment_list", + "endIndex": 24, + "text": "ENV=c c", + "innerText": "ENV=c c", + "complete": true, + "children": [ + { + "startIndex": 17, + "type": "assignment", + "endIndex": 22, + "text": "ENV=c", + "innerText": "ENV=c", + "complete": true, + "children": [ + { + "startIndex": 21, + "type": "word", + "endIndex": 22, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 17, + "type": "variable_name", + "endIndex": 20, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 23, + "type": "command", + "endIndex": 24, + "text": "c", + "innerText": "c", + "complete": true, + "children": [ + { + "startIndex": 23, + "type": "word", + "endIndex": 24, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] + } + ] +} + +// Case 5 +{ + "startIndex": 0, + "type": "program", + "endIndex": 11, + "text": "ENV=\"a b\" c", + "innerText": "ENV=\"a b\" c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 11, + "text": "ENV=\"a b\" c", + "innerText": "ENV=\"a b\" c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 9, + "text": "ENV=\"a b\"", + "innerText": "ENV=\"a b\"", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "string", + "endIndex": 9, + "text": "\"a b\"", + "innerText": "a b", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 10, + "type": "command", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 6 +{ + "startIndex": 0, + "type": "program", + "endIndex": 11, + "text": "ENV='a b' c", + "innerText": "ENV='a b' c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 11, + "text": "ENV='a b' c", + "innerText": "ENV='a b' c", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 9, + "text": "ENV='a b'", + "innerText": "ENV='a b'", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "raw_string", + "endIndex": 9, + "text": "'a b'", + "innerText": "a b", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 10, + "type": "command", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 7 +{ + "startIndex": 0, + "type": "program", + "endIndex": 11, + "text": "ENV=`cmd` a", + "innerText": "ENV=`cmd` a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 11, + "text": "ENV=`cmd` a", + "innerText": "ENV=`cmd` a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 9, + "text": "ENV=`cmd`", + "innerText": "ENV=`cmd`", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "command_substitution", + "endIndex": 9, + "text": "`cmd`", + "innerText": "`cmd`", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "command", + "endIndex": 8, + "text": "cmd", + "innerText": "cmd", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 8, + "text": "cmd", + "innerText": "cmd", + "complete": true, + "children": [] + } + ] + } + ] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 10, + "type": "command", + "endIndex": 11, + "text": "a", + "innerText": "a", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 8 +{ + "startIndex": 0, + "type": "program", + "endIndex": 12, + "text": "ENV+='100' b", + "innerText": "ENV+='100' b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 12, + "text": "ENV+='100' b", + "innerText": "ENV+='100' b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 10, + "text": "ENV+='100'", + "innerText": "ENV+='100'", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "raw_string", + "endIndex": 10, + "text": "'100'", + "innerText": "100", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "+=" + }, + { + "startIndex": 11, + "type": "command", + "endIndex": 12, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 11, + "type": "word", + "endIndex": 12, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 9 +{ + "startIndex": 0, + "type": "program", + "endIndex": 12, + "text": "ENV+=a ENV=b", + "innerText": "ENV+=a ENV=b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 12, + "text": "ENV+=a ENV=b", + "innerText": "ENV+=a ENV=b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 6, + "text": "ENV+=a", + "innerText": "ENV+=a", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "+=" + }, + { + "startIndex": 7, + "type": "assignment", + "endIndex": 12, + "text": "ENV=b", + "innerText": "ENV=b", + "complete": true, + "children": [ + { + "startIndex": 11, + "type": "word", + "endIndex": 12, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 7, + "type": "variable_name", + "endIndex": 10, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + } + ] +} + +// Case 10 +{ + "startIndex": 0, + "type": "program", + "endIndex": 19, + "text": "ENV+=a ENV=b && foo", + "innerText": "ENV+=a ENV=b && foo", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 19, + "text": "ENV+=a ENV=b && foo", + "innerText": "ENV+=a ENV=b && foo", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 12, + "text": "ENV+=a ENV=b", + "innerText": "ENV+=a ENV=b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 6, + "text": "ENV+=a", + "innerText": "ENV+=a", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "word", + "endIndex": 6, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "+=" + }, + { + "startIndex": 7, + "type": "assignment", + "endIndex": 12, + "text": "ENV=b", + "innerText": "ENV=b", + "complete": true, + "children": [ + { + "startIndex": 11, + "type": "word", + "endIndex": 12, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 7, + "type": "variable_name", + "endIndex": 10, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + }, + { + "startIndex": 16, + "type": "command", + "endIndex": 19, + "text": "foo", + "innerText": "foo", + "complete": true, + "children": [ + { + "startIndex": 16, + "type": "word", + "endIndex": 19, + "text": "foo", + "innerText": "foo", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 11 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "ENV=\"a", + "innerText": "ENV=\"a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 6, + "text": "ENV=\"a", + "innerText": "ENV=\"a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 6, + "text": "ENV=\"a", + "innerText": "ENV=\"a", + "complete": false, + "children": [ + { + "startIndex": 4, + "type": "string", + "endIndex": 6, + "text": "\"a", + "innerText": "a", + "complete": false, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + } + ] +} + +// Case 12 +{ + "startIndex": 0, + "type": "program", + "endIndex": 6, + "text": "ENV='a", + "innerText": "ENV='a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 6, + "text": "ENV='a", + "innerText": "ENV='a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 6, + "text": "ENV='a", + "innerText": "ENV='a", + "complete": false, + "children": [ + { + "startIndex": 4, + "type": "raw_string", + "endIndex": 6, + "text": "'a", + "innerText": "a", + "complete": false, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + } + ] +} + +// Case 13 +{ + "startIndex": 0, + "type": "program", + "endIndex": 12, + "text": "ENV=a ENV=`b", + "innerText": "ENV=a ENV=`b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 12, + "text": "ENV=a ENV=`b", + "innerText": "ENV=a ENV=`b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 6, + "type": "assignment", + "endIndex": 12, + "text": "ENV=`b", + "innerText": "ENV=`b", + "complete": false, + "children": [ + { + "startIndex": 10, + "type": "command_substitution", + "endIndex": 12, + "text": "`b", + "innerText": "`b", + "complete": false, + "children": [ + { + "startIndex": 11, + "type": "command", + "endIndex": 12, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 11, + "type": "word", + "endIndex": 12, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ], + "name": { + "startIndex": 6, + "type": "variable_name", + "endIndex": 9, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + } + ] +} + +// Case 14 +{ + "startIndex": 0, + "type": "program", + "endIndex": 28, + "text": "ENV=`ENV=\"a\" b` && ENV=\"c\" d", + "innerText": "ENV=`ENV=\"a\" b` && ENV=\"c\" d", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "list", + "endIndex": 28, + "text": "ENV=`ENV=\"a\" b` && ENV=\"c\" d", + "innerText": "ENV=`ENV=\"a\" b` && ENV=\"c\" d", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 15, + "text": "ENV=`ENV=\"a\" b`", + "innerText": "ENV=`ENV=\"a\" b`", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 15, + "text": "ENV=`ENV=\"a\" b`", + "innerText": "ENV=`ENV=\"a\" b`", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "command_substitution", + "endIndex": 15, + "text": "`ENV=\"a\" b`", + "innerText": "`ENV=\"a\" b`", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "assignment_list", + "endIndex": 14, + "text": "ENV=\"a\" b", + "innerText": "ENV=\"a\" b", + "complete": true, + "children": [ + { + "startIndex": 5, + "type": "assignment", + "endIndex": 12, + "text": "ENV=\"a\"", + "innerText": "ENV=\"a\"", + "complete": true, + "children": [ + { + "startIndex": 9, + "type": "string", + "endIndex": 12, + "text": "\"a\"", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 5, + "type": "variable_name", + "endIndex": 8, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 13, + "type": "command", + "endIndex": 14, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 13, + "type": "word", + "endIndex": 14, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + }, + { + "startIndex": 19, + "type": "assignment_list", + "endIndex": 28, + "text": "ENV=\"c\" d", + "innerText": "ENV=\"c\" d", + "complete": true, + "children": [ + { + "startIndex": 19, + "type": "assignment", + "endIndex": 26, + "text": "ENV=\"c\"", + "innerText": "ENV=\"c\"", + "complete": true, + "children": [ + { + "startIndex": 23, + "type": "string", + "endIndex": 26, + "text": "\"c\"", + "innerText": "c", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 19, + "type": "variable_name", + "endIndex": 22, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 27, + "type": "command", + "endIndex": 28, + "text": "d", + "innerText": "d", + "complete": true, + "children": [ + { + "startIndex": 27, + "type": "word", + "endIndex": 28, + "text": "d", + "innerText": "d", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] + } + ] +} + +// Case 15 +{ + "startIndex": 0, + "type": "program", + "endIndex": 14, + "text": "c $(ENV=a foo)", + "innerText": "c $(ENV=a foo)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "command", + "endIndex": 14, + "text": "c $(ENV=a foo)", + "innerText": "c $(ENV=a foo)", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "word", + "endIndex": 1, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + }, + { + "startIndex": 2, + "type": "command_substitution", + "endIndex": 14, + "text": "$(ENV=a foo)", + "innerText": "$(ENV=a foo)", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "assignment_list", + "endIndex": 13, + "text": "ENV=a foo", + "innerText": "ENV=a foo", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "assignment", + "endIndex": 9, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 8, + "type": "word", + "endIndex": 9, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 4, + "type": "variable_name", + "endIndex": 7, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 10, + "type": "command", + "endIndex": 13, + "text": "foo", + "innerText": "foo", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 13, + "text": "foo", + "innerText": "foo", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] + } + ] + } + ] +} + +// Case 16 +{ + "startIndex": 0, + "type": "program", + "endIndex": 8, + "text": "ENV=a; b", + "innerText": "ENV=a; b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + }, + { + "startIndex": 7, + "type": "command", + "endIndex": 8, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 7, + "type": "word", + "endIndex": 8, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 17 +{ + "startIndex": 0, + "type": "program", + "endIndex": 9, + "text": "ENV=a ; b", + "innerText": "ENV=a ; b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + }, + { + "startIndex": 8, + "type": "command", + "endIndex": 9, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 8, + "type": "word", + "endIndex": 9, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 18 +{ + "startIndex": 0, + "type": "program", + "endIndex": 9, + "text": "ENV=a & b", + "innerText": "ENV=a & b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + }, + { + "startIndex": 8, + "type": "command", + "endIndex": 9, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 8, + "type": "word", + "endIndex": 9, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 19 +{ + "startIndex": 0, + "type": "program", + "endIndex": 7, + "text": "ENV=a|b", + "innerText": "ENV=a|b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "pipeline", + "endIndex": 7, + "text": "ENV=a|b", + "innerText": "ENV=a|b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 5, + "text": "ENV=a", + "innerText": "ENV=a", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + }, + { + "startIndex": 6, + "type": "command", + "endIndex": 7, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 6, + "type": "word", + "endIndex": 7, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ] +} + +// Case 20 +{ + "startIndex": 0, + "type": "program", + "endIndex": 10, + "text": "ENV[0]=a b", + "innerText": "ENV[0]=a b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 10, + "text": "ENV[0]=a b", + "innerText": "ENV[0]=a b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 8, + "text": "ENV[0]=a", + "innerText": "ENV[0]=a", + "complete": true, + "children": [ + { + "startIndex": 7, + "type": "word", + "endIndex": 8, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "subscript", + "endIndex": 6, + "text": "ENV[0]", + "innerText": "ENV[0]", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "0", + "innerText": "0", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + } + }, + "operator": "=" + }, + { + "startIndex": 9, + "type": "command", + "endIndex": 10, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 9, + "type": "word", + "endIndex": 10, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 21 +{ + "startIndex": 0, + "type": "program", + "endIndex": 11, + "text": "ENV[0]=a; b", + "innerText": "ENV[0]=a; b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 8, + "text": "ENV[0]=a", + "innerText": "ENV[0]=a", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 8, + "text": "ENV[0]=a", + "innerText": "ENV[0]=a", + "complete": true, + "children": [ + { + "startIndex": 7, + "type": "word", + "endIndex": 8, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "subscript", + "endIndex": 6, + "text": "ENV[0]", + "innerText": "ENV[0]", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "0", + "innerText": "0", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + } + }, + "operator": "=" + } + ], + "hasCommand": false + }, + { + "startIndex": 10, + "type": "command", + "endIndex": 11, + "text": "b", + "innerText": "b", + "complete": true, + "children": [ + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] +} + +// Case 22 +{ + "startIndex": 0, + "type": "program", + "endIndex": 11, + "text": "ENV[1]=`a b", + "innerText": "ENV[1]=`a b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 11, + "text": "ENV[1]=`a b", + "innerText": "ENV[1]=`a b", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 11, + "text": "ENV[1]=`a b", + "innerText": "ENV[1]=`a b", + "complete": false, + "children": [ + { + "startIndex": 7, + "type": "command_substitution", + "endIndex": 11, + "text": "`a b", + "innerText": "`a b", + "complete": false, + "children": [ + { + "startIndex": 8, + "type": "command", + "endIndex": 11, + "text": "a b", + "innerText": "a b", + "complete": true, + "children": [ + { + "startIndex": 8, + "type": "word", + "endIndex": 9, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "b", + "innerText": "b", + "complete": true, + "children": [] + } + ] + } + ] + } + ], + "name": { + "startIndex": 0, + "type": "subscript", + "endIndex": 6, + "text": "ENV[1]", + "innerText": "ENV[1]", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "1", + "innerText": "1", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + } + }, + "operator": "=" + } + ], + "hasCommand": false + } + ] +} + +// Case 23 +{ + "startIndex": 0, + "type": "program", + "endIndex": 14, + "text": "ENV[2]+=\"a b \"", + "innerText": "ENV[2]+=\"a b \"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 14, + "text": "ENV[2]+=\"a b \"", + "innerText": "ENV[2]+=\"a b \"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 14, + "text": "ENV[2]+=\"a b \"", + "innerText": "ENV[2]+=\"a b \"", + "complete": true, + "children": [ + { + "startIndex": 8, + "type": "string", + "endIndex": 14, + "text": "\"a b \"", + "innerText": "a b ", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "subscript", + "endIndex": 6, + "text": "ENV[2]", + "innerText": "ENV[2]", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "2", + "innerText": "2", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + } + }, + "operator": "+=" + } + ], + "hasCommand": false + } + ] +} + +// Case 24 +{ + "startIndex": 0, + "type": "program", + "endIndex": 55, + "text": "MY_VAR='echo'hi$'quote'\"command: $(ps | VAR=2 grep ps)\"", + "innerText": "MY_VAR='echo'hi$'quote'\"command: $(ps | VAR=2 grep ps)\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 55, + "text": "MY_VAR='echo'hi$'quote'\"command: $(ps | VAR=2 grep ps)\"", + "innerText": "MY_VAR='echo'hi$'quote'\"command: $(ps | VAR=2 grep ps)\"", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 55, + "text": "MY_VAR='echo'hi$'quote'\"command: $(ps | VAR=2 grep ps)\"", + "innerText": "MY_VAR='echo'hi$'quote'\"command: $(ps | VAR=2 grep ps)\"", + "complete": true, + "children": [ + { + "startIndex": 7, + "type": "concatenation", + "endIndex": 55, + "text": "'echo'hi$'quote'\"command: $(ps | VAR=2 grep ps)\"", + "innerText": "echohiquotecommand: $(ps | VAR=2 grep ps)", + "complete": true, + "children": [ + { + "startIndex": 7, + "type": "raw_string", + "endIndex": 13, + "text": "'echo'", + "innerText": "echo", + "complete": true, + "children": [] + }, + { + "startIndex": 13, + "type": "word", + "endIndex": 15, + "text": "hi", + "innerText": "hi", + "complete": true, + "children": [] + }, + { + "startIndex": 15, + "type": "ansi_c_string", + "endIndex": 23, + "text": "$'quote'", + "innerText": "quote", + "complete": true, + "children": [] + }, + { + "startIndex": 23, + "type": "string", + "endIndex": 55, + "text": "\"command: $(ps | VAR=2 grep ps)\"", + "innerText": "command: $(ps | VAR=2 grep ps)", + "complete": true, + "children": [ + { + "startIndex": 33, + "type": "command_substitution", + "endIndex": 54, + "text": "$(ps | VAR=2 grep ps)", + "innerText": "$(ps | VAR=2 grep ps)", + "complete": true, + "children": [ + { + "startIndex": 35, + "type": "pipeline", + "endIndex": 53, + "text": "ps | VAR=2 grep ps", + "innerText": "ps | VAR=2 grep ps", + "complete": true, + "children": [ + { + "startIndex": 35, + "type": "command", + "endIndex": 38, + "text": "ps ", + "innerText": "ps ", + "complete": true, + "children": [ + { + "startIndex": 35, + "type": "word", + "endIndex": 37, + "text": "ps", + "innerText": "ps", + "complete": true, + "children": [] + } + ] + }, + { + "startIndex": 40, + "type": "assignment_list", + "endIndex": 53, + "text": "VAR=2 grep ps", + "innerText": "VAR=2 grep ps", + "complete": true, + "children": [ + { + "startIndex": 40, + "type": "assignment", + "endIndex": 45, + "text": "VAR=2", + "innerText": "VAR=2", + "complete": true, + "children": [ + { + "startIndex": 44, + "type": "word", + "endIndex": 45, + "text": "2", + "innerText": "2", + "complete": true, + "children": [] + } + ], + "name": { + "startIndex": 40, + "type": "variable_name", + "endIndex": 43, + "text": "VAR", + "innerText": "VAR", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 46, + "type": "command", + "endIndex": 53, + "text": "grep ps", + "innerText": "grep ps", + "complete": true, + "children": [ + { + "startIndex": 46, + "type": "word", + "endIndex": 50, + "text": "grep", + "innerText": "grep", + "complete": true, + "children": [] + }, + { + "startIndex": 51, + "type": "word", + "endIndex": 53, + "text": "ps", + "innerText": "ps", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] + } + ] + } + ] + } + ] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 6, + "text": "MY_VAR", + "innerText": "MY_VAR", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + } + ] +} + +// Case 25 +{ + "startIndex": 0, + "type": "program", + "endIndex": 13, + "text": "ENV=\"a\"'b'c d", + "innerText": "ENV=\"a\"'b'c d", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 13, + "text": "ENV=\"a\"'b'c d", + "innerText": "ENV=\"a\"'b'c d", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 11, + "text": "ENV=\"a\"'b'c", + "innerText": "ENV=\"a\"'b'c", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "concatenation", + "endIndex": 11, + "text": "\"a\"'b'c", + "innerText": "abc", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "string", + "endIndex": 7, + "text": "\"a\"", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 7, + "type": "raw_string", + "endIndex": 10, + "text": "'b'", + "innerText": "b", + "complete": true, + "children": [] + }, + { + "startIndex": 10, + "type": "word", + "endIndex": 11, + "text": "c", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + }, + { + "startIndex": 12, + "type": "command", + "endIndex": 13, + "text": "d", + "innerText": "d", + "complete": true, + "children": [ + { + "startIndex": 12, + "type": "word", + "endIndex": 13, + "text": "d", + "innerText": "d", + "complete": true, + "children": [] + } + ] + } + ], + "hasCommand": true + } + ] +} + +// Case 26 +{ + "startIndex": 0, + "type": "program", + "endIndex": 11, + "text": "ENV=a\"b\"'c'", + "innerText": "ENV=a\"b\"'c'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment_list", + "endIndex": 11, + "text": "ENV=a\"b\"'c'", + "innerText": "ENV=a\"b\"'c'", + "complete": true, + "children": [ + { + "startIndex": 0, + "type": "assignment", + "endIndex": 11, + "text": "ENV=a\"b\"'c'", + "innerText": "ENV=a\"b\"'c'", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "concatenation", + "endIndex": 11, + "text": "a\"b\"'c'", + "innerText": "abc", + "complete": true, + "children": [ + { + "startIndex": 4, + "type": "word", + "endIndex": 5, + "text": "a", + "innerText": "a", + "complete": true, + "children": [] + }, + { + "startIndex": 5, + "type": "string", + "endIndex": 8, + "text": "\"b\"", + "innerText": "b", + "complete": true, + "children": [] + }, + { + "startIndex": 8, + "type": "raw_string", + "endIndex": 11, + "text": "'c'", + "innerText": "c", + "complete": true, + "children": [] + } + ] + } + ], + "name": { + "startIndex": 0, + "type": "variable_name", + "endIndex": 3, + "text": "ENV", + "innerText": "ENV", + "complete": true, + "children": [] + }, + "operator": "=" + } + ], + "hasCommand": false + } + ] +} \ No newline at end of file diff --git a/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts b/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts new file mode 100644 index 0000000000000..aa34c15d4810e --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts @@ -0,0 +1,33 @@ +import { deepStrictEqual } from 'node:assert'; +import { getCommand, Command } from "../src/command"; + +suite("shell-parser getCommand", () => { + const aliases = { + woman: "man", + quote: "'q'", + g: "git", + }; + const getTokenText = (command: Command | null) => command?.tokens.map((token) => token.text) ?? []; + + test("works without matching aliases", () => { + deepStrictEqual(getTokenText(getCommand("git co ", {})), ["git", "co", ""]); + deepStrictEqual(getTokenText(getCommand("git co ", aliases)), ["git", "co", ""]); + deepStrictEqual(getTokenText(getCommand("woman ", {})), ["woman", ""]); + deepStrictEqual(getTokenText(getCommand("another string ", aliases)), [ + "another", + "string", + "", + ]); + }); + + test("works with regular aliases", () => { + // Don't change a single token. + deepStrictEqual(getTokenText(getCommand("woman", aliases)), ["woman"]); + // Change first token if length > 1. + deepStrictEqual(getTokenText(getCommand("woman ", aliases)), ["man", ""]); + // Don't change later tokens. + deepStrictEqual(getTokenText(getCommand("man woman ", aliases)), ["man", "woman", ""]); + // Handle quotes + deepStrictEqual(getTokenText(getCommand("quote ", aliases)), ["q", ""]); + }); +}); diff --git a/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts b/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts new file mode 100644 index 0000000000000..d389137a68cea --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts @@ -0,0 +1,100 @@ +import fs from "node:fs"; +import path from "node:path"; +import { parse } from "../src/parser"; +import { strictEqual } from 'node:assert'; + +function parseCommand(command: string): string { + return JSON.stringify(parse(command), null, " "); +} + +/** + * + * @param filePath The path to the file to parse + * @param nameComment The first character of each title line + */ +function getData( + filePath: string, + nameComment: string, +): [name: string, value: string][] { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, ""); + return []; + } + return fs + .readFileSync(filePath, { encoding: "utf8" }) + .split("\n\n") + .map((testCase) => { + const firstNewline = testCase.indexOf("\n"); + const title = testCase.slice(0, firstNewline); + const block = testCase.slice(firstNewline); + return [title.slice(nameComment.length).trim(), block.trim()]; + }); +} + +// function outputNewFile( +// filePath: string, +// nameComment: string, +// data: [name: string, value: string][], +// ) { +// fs.writeFileSync( +// filePath, +// data.reduce( +// (previous, current, index) => +// `${previous}${index > 0 ? "\n\n" : ""}${nameComment} ${current[0]}\n${current[1] +// }`, +// "", +// ), +// ); +// } + +// function notIncludedIn(setA: Set, setB: Set): K[] { +// const notIncluded: K[] = []; +// for (const v of setA) { +// if (!setB.has(v)) notIncluded.push(v); +// } +// return notIncluded; +// } + +// function mapKeysDiff(mapA: Map, mapB: Map) { +// const keysA = new Set(mapA.keys()); +// const keysB = new Set(mapB.keys()); +// return [ +// notIncludedIn(keysA, keysB), // keys of A not included in B +// notIncludedIn(keysB, keysA), // keys of B not included in A +// ]; +// } + +suite("shell-parser fixtures", () => { + const fixturesPath = path.join(__dirname, "../../../../fixtures/shell-parser"); + const fixtures = fs.readdirSync(fixturesPath); + for (const fixture of fixtures) { + // console.log('fixture', fixture); + suite(fixture, () => { + const inputFile = path.join(fixturesPath, fixture, "input.sh"); + const outputFile = path.join(fixturesPath, fixture, "output.txt"); + const inputData = new Map(getData(inputFile, "###")); + const outputData = new Map(getData(outputFile, "//")); + + // clean diffs and regenerate files if required. + // if (!process.env.NO_FIXTURES_EDIT) { + // const [newInputs, extraOutputs] = mapKeysDiff(inputData, outputData); + // extraOutputs.forEach((v) => outputData.delete(v)); + // newInputs.forEach((v) => + // outputData.set(v, parseCommand(inputData.get(v) ?? "")), + // ); + // if (extraOutputs.length || newInputs.length) { + // outputNewFile(outputFile, "//", [...outputData.entries()]); + // } + // } + + for (const [caseName, input] of inputData.entries()) { + if (caseName) { + test(caseName, () => { + const output = outputData.get(caseName); + strictEqual(parseCommand(input ?? ""), output); + }); + } + } + }); + } +}); From a730d35db6b5f75a7573e83ee5ffb8df4b73f125 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 02:08:40 -0800 Subject: [PATCH 10/51] Add readme explaining fork and cgmanifest --- extensions/terminal-suggest/src/fig/README.md | 5 ++ .../terminal-suggest/src/fig/cgmanifest.json | 73 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 extensions/terminal-suggest/src/fig/README.md create mode 100644 extensions/terminal-suggest/src/fig/cgmanifest.json diff --git a/extensions/terminal-suggest/src/fig/README.md b/extensions/terminal-suggest/src/fig/README.md new file mode 100644 index 0000000000000..c348d54958546 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/README.md @@ -0,0 +1,5 @@ +This folder contains the `autocomplete-parser` project from https://github.com/aws/amazon-q-developer-cli/blob/main/packages/autocomplete-parser and its dependencies which were located in siblings folders and https://github.com/withfig/autocomplete-tools, both licenses under MIT. The fork was necessary for a few reasons: + +- They ship as ESM modules which we're not ready to consume just yet. +- We want the more complete `autocomplete-parser` that contains the important `parseArguments` function that does the bulk of the smarts in parsing the fig commands. +- We needed to strip out all the implementation-specific parts from their `api-bindings` project that deals with settings, IPC, etc. diff --git a/extensions/terminal-suggest/src/fig/cgmanifest.json b/extensions/terminal-suggest/src/fig/cgmanifest.json new file mode 100644 index 0000000000000..894a1add38890 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/cgmanifest.json @@ -0,0 +1,73 @@ +{ + "registrations": [ + { + "component": { + "type": "git", + "git": { + "name": "amazon-q-developer-cli", + "repositoryUrl": "https://github.com/aws/amazon-q-developer-cli", + "commitHash": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d" + } + }, + "licenseDetail": [ + "MIT License", + "", + "Copyright (c) 2024 Amazon.com, Inc. or its affiliates.", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "version": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d" + }, + { + "component": { + "type": "git", + "git": { + "name": "@fig/autocomplete-shared", + "repositoryUrl": "https://github.com/withfig/autocomplete-tools/blob/main/shared", + "commitHash": "104377c19a91ca8a312cb38c115a74468f6227cb" + } + }, + "licenseDetail": [ + "MIT License", + "", + "Copyright (c) 2021 Hercules Labs Inc. (Fig)", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "version": "1.1.2" + } + ], + "version": 1 +} From 9e603c8d8d03c676c966bf272e0abc1baba7e0b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 02:08:56 -0800 Subject: [PATCH 11/51] Add upstream tests for shell-parser --- .../autocomplete-parser/src/parseArguments.ts | 2 +- .../src/fig/shell-parser/command.ts | 236 ++++++++++++++++++ .../src/fig/shell-parser/{src => }/errors.ts | 2 +- .../src/fig/shell-parser/{src => }/index.ts | 0 .../src/fig/shell-parser/{src => }/parser.ts | 0 .../src/fig/shell-parser/src/command.ts | 236 ------------------ .../src/fig/shell-parser/test/command.test.ts | 2 +- .../src/fig/shell-parser/test/parser.test.ts | 2 +- .../src/terminalSuggestMain.ts | 2 +- 9 files changed, 241 insertions(+), 241 deletions(-) create mode 100644 extensions/terminal-suggest/src/fig/shell-parser/command.ts rename extensions/terminal-suggest/src/fig/shell-parser/{src => }/errors.ts (71%) rename extensions/terminal-suggest/src/fig/shell-parser/{src => }/index.ts (100%) rename extensions/terminal-suggest/src/fig/shell-parser/{src => }/parser.ts (100%) delete mode 100644 extensions/terminal-suggest/src/fig/shell-parser/src/command.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts index 860edb90c0d65..b437beb88151f 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts @@ -18,7 +18,7 @@ import { import { Command, substituteAlias, -} from "../../shell-parser/src"; +} from "../../shell-parser"; // import { // getSpecPath, // loadSubcommandCached, diff --git a/extensions/terminal-suggest/src/fig/shell-parser/command.ts b/extensions/terminal-suggest/src/fig/shell-parser/command.ts new file mode 100644 index 0000000000000..9e95f17daa7ee --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shell-parser/command.ts @@ -0,0 +1,236 @@ +import { NodeType, BaseNode, createTextNode, parse } from "./parser.js"; +import { ConvertCommandError, SubstituteAliasError } from "./errors.js"; + +export * from "./errors.js"; + +export type Token = { + text: string; + node: BaseNode; + originalNode: BaseNode; +}; + +export type Command = { + tokens: Token[]; + tree: BaseNode; + + originalTree: BaseNode; +}; + +export type AliasMap = Record; + +const descendantAtIndex = ( + node: BaseNode, + index: number, + type?: NodeType, +): BaseNode | null => { + if (node.startIndex <= index && index <= node.endIndex) { + const descendant = node.children + .map((child) => descendantAtIndex(child, index, type)) + .find(Boolean); + if (descendant) { + return descendant; + } + return !type || node.type === type ? node : null; + } + return null; +}; + +export const createTextToken = ( + command: Command, + index: number, + text: string, + originalNode?: BaseNode, +): Token => { + const { tree, originalTree, tokens } = command; + + let indexDiff = 0; + const tokenIndex = tokens.findIndex( + (token) => index < token.originalNode.startIndex, + ); + const token = tokens[tokenIndex]; + if (tokenIndex === 0) { + indexDiff = token.node.startIndex - token.originalNode.startIndex; + } else if (tokenIndex === -1) { + indexDiff = tree.text.length - originalTree.text.length; + } else { + indexDiff = token.node.endIndex - token.originalNode.endIndex; + } + + return { + originalNode: + originalNode || createTextNode(originalTree.text, index, text), + node: createTextNode(text, index + indexDiff, text), + text, + }; +}; + +const convertCommandNodeToCommand = (tree: BaseNode): Command => { + if (tree.type !== NodeType.Command) { + throw new ConvertCommandError("Cannot get tokens from non-command node"); + } + + const command = { + originalTree: tree, + tree, + tokens: tree.children.map((child) => ({ + originalNode: child, + node: child, + text: child.innerText, + })), + }; + + const { children, endIndex, text } = tree; + if ( + +(children.length === 0 || children[children.length - 1].endIndex) < + endIndex && + text.endsWith(" ") + ) { + command.tokens.push(createTextToken(command, endIndex, "")); + } + return command; +}; + +const shiftByAmount = (node: BaseNode, shift: number): BaseNode => ({ + ...node, + startIndex: node.startIndex + shift, + endIndex: node.endIndex + shift, + children: node.children.map((child) => shiftByAmount(child, shift)), +}); + +export const substituteAlias = ( + command: Command, + token: Token, + alias: string, +): Command => { + if (command.tokens.find((t) => t === token) === undefined) { + throw new SubstituteAliasError("Token not in command"); + } + const { tree } = command; + + const preAliasChars = token.node.startIndex - tree.startIndex; + const postAliasChars = token.node.endIndex - tree.endIndex; + + const preAliasText = `${tree.text.slice(0, preAliasChars)}`; + const postAliasText = postAliasChars + ? `${tree.text.slice(postAliasChars)}` + : ""; + + const commandBuffer = `${preAliasText}${alias}${postAliasText}`; + + // Parse command and shift indices to align with original command. + const parseTree = shiftByAmount(parse(commandBuffer), tree.startIndex); + + if (parseTree.children.length !== 1) { + throw new SubstituteAliasError("Invalid alias"); + } + + const newCommand = convertCommandNodeToCommand(parseTree.children[0]); + + const [aliasStart, aliasEnd] = [ + token.node.startIndex, + token.node.startIndex + alias.length, + ]; + + let tokenIndexDiff = 0; + let lastTokenInAlias = false; + // Map tokens from new command back to old command to attributing the correct original nodes. + const tokens = newCommand.tokens.map((newToken, index) => { + const tokenInAlias = + aliasStart < newToken.node.endIndex && + newToken.node.startIndex < aliasEnd; + tokenIndexDiff += tokenInAlias && lastTokenInAlias ? 1 : 0; + const { originalNode } = command.tokens[index - tokenIndexDiff]; + lastTokenInAlias = tokenInAlias; + return { ...newToken, originalNode }; + }); + + if (newCommand.tokens.length - command.tokens.length !== tokenIndexDiff) { + throw new SubstituteAliasError("Error substituting alias"); + } + + return { + originalTree: command.originalTree, + tree: newCommand.tree, + tokens, + }; +}; + +export const expandCommand = ( + command: Command, + _cursorIndex: number, + aliases: AliasMap, +): Command => { + let expanded = command; + const usedAliases = new Set(); + + // Check for aliases + let [name] = expanded.tokens; + while ( + expanded.tokens.length > 1 && + name && + aliases[name.text] && + !usedAliases.has(name.text) + ) { + // Remove quotes + const aliasValue = aliases[name.text].replace(/^'(.*)'$/g, "$1"); + try { + expanded = substituteAlias(expanded, name, aliasValue); + } catch (_err) { + // TODO(refactoring): add logger again + // console.error("Error substituting alias"); + } + usedAliases.add(name.text); + [name] = expanded.tokens; + } + + return expanded; +}; + +export const getCommand = ( + buffer: string, + aliases: AliasMap, + cursorIndex?: number, +): Command | null => { + const index = cursorIndex === undefined ? buffer.length : cursorIndex; + const parseTree = parse(buffer); + const commandNode = descendantAtIndex(parseTree, index, NodeType.Command); + if (commandNode === null) { + return null; + } + const command = convertCommandNodeToCommand(commandNode); + return expandCommand(command, index, aliases); +}; + +const statements = [ + NodeType.Program, + NodeType.CompoundStatement, + NodeType.Subshell, + NodeType.Pipeline, + NodeType.List, + NodeType.Command, +]; + +export const getTopLevelCommands = (parseTree: BaseNode): Command[] => { + if (parseTree.type === NodeType.Command) { + return [convertCommandNodeToCommand(parseTree)]; + } + if (!statements.includes(parseTree.type)) { + return []; + } + const commands: Command[] = []; + for (let i = 0; i < parseTree.children.length; i += 1) { + commands.push(...getTopLevelCommands(parseTree.children[i])); + } + return commands; +}; + +export const getAllCommandsWithAlias = ( + buffer: string, + aliases: AliasMap, +): Command[] => { + const parseTree = parse(buffer); + const commands = getTopLevelCommands(parseTree); + return commands.map((command) => + expandCommand(command, command.tree.text.length, aliases), + ); +}; diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/errors.ts b/extensions/terminal-suggest/src/fig/shell-parser/errors.ts similarity index 71% rename from extensions/terminal-suggest/src/fig/shell-parser/src/errors.ts rename to extensions/terminal-suggest/src/fig/shell-parser/errors.ts index c1751c11a205c..0b6afeab64497 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/src/errors.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/errors.ts @@ -1,4 +1,4 @@ -import { createErrorInstance } from '../../shared/src/errors'; +import { createErrorInstance } from '../shared/src/errors'; export const SubstituteAliasError = createErrorInstance("SubstituteAliasError"); export const ConvertCommandError = createErrorInstance("ConvertCommandError"); diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/index.ts b/extensions/terminal-suggest/src/fig/shell-parser/index.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/shell-parser/src/index.ts rename to extensions/terminal-suggest/src/fig/shell-parser/index.ts diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts b/extensions/terminal-suggest/src/fig/shell-parser/parser.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/shell-parser/src/parser.ts rename to extensions/terminal-suggest/src/fig/shell-parser/parser.ts diff --git a/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts b/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts deleted file mode 100644 index 94268512855de..0000000000000 --- a/extensions/terminal-suggest/src/fig/shell-parser/src/command.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { NodeType, BaseNode, createTextNode, parse } from "./parser.js"; -import { ConvertCommandError, SubstituteAliasError } from "./errors.js"; - -export * from "./errors.js"; - -export type Token = { - text: string; - node: BaseNode; - originalNode: BaseNode; -}; - -export type Command = { - tokens: Token[]; - tree: BaseNode; - - originalTree: BaseNode; -}; - -export type AliasMap = Record; - -const descendantAtIndex = ( - node: BaseNode, - index: number, - type?: NodeType, -): BaseNode | null => { - if (node.startIndex <= index && index <= node.endIndex) { - const descendant = node.children - .map((child) => descendantAtIndex(child, index, type)) - .find(Boolean); - if (descendant) { - return descendant; - } - return !type || node.type === type ? node : null; - } - return null; -}; - -export const createTextToken = ( - command: Command, - index: number, - text: string, - originalNode?: BaseNode, -): Token => { - const { tree, originalTree, tokens } = command; - - let indexDiff = 0; - const tokenIndex = tokens.findIndex( - (token) => index < token.originalNode.startIndex, - ); - const token = tokens[tokenIndex]; - if (tokenIndex === 0) { - indexDiff = token.node.startIndex - token.originalNode.startIndex; - } else if (tokenIndex === -1) { - indexDiff = tree.text.length - originalTree.text.length; - } else { - indexDiff = token.node.endIndex - token.originalNode.endIndex; - } - - return { - originalNode: - originalNode || createTextNode(originalTree.text, index, text), - node: createTextNode(text, index + indexDiff, text), - text, - }; -}; - -const convertCommandNodeToCommand = (tree: BaseNode): Command => { - if (tree.type !== NodeType.Command) { - throw new ConvertCommandError("Cannot get tokens from non-command node"); - } - - const command = { - originalTree: tree, - tree, - tokens: tree.children.map((child) => ({ - originalNode: child, - node: child, - text: child.innerText, - })), - }; - - const { children, endIndex, text } = tree; - if ( - +(children.length === 0 || children[children.length - 1].endIndex) < - endIndex && - text.endsWith(" ") - ) { - command.tokens.push(createTextToken(command, endIndex, "")); - } - return command; -}; - -const shiftByAmount = (node: BaseNode, shift: number): BaseNode => ({ - ...node, - startIndex: node.startIndex + shift, - endIndex: node.endIndex + shift, - children: node.children.map((child) => shiftByAmount(child, shift)), -}); - -export const substituteAlias = ( - command: Command, - token: Token, - alias: string, -): Command => { - if (command.tokens.find((t) => t === token) === undefined) { - throw new SubstituteAliasError("Token not in command"); - } - const { tree } = command; - - const preAliasChars = token.node.startIndex - tree.startIndex; - const postAliasChars = token.node.endIndex - tree.endIndex; - - const preAliasText = `${tree.text.slice(0, preAliasChars)}`; - const postAliasText = postAliasChars - ? `${tree.text.slice(postAliasChars)}` - : ""; - - const commandBuffer = `${preAliasText}${alias}${postAliasText}`; - - // Parse command and shift indices to align with original command. - const parseTree = shiftByAmount(parse(commandBuffer), tree.startIndex); - - if (parseTree.children.length !== 1) { - throw new SubstituteAliasError("Invalid alias"); - } - - const newCommand = convertCommandNodeToCommand(parseTree.children[0]); - - const [aliasStart, aliasEnd] = [ - token.node.startIndex, - token.node.startIndex + alias.length, - ]; - - let tokenIndexDiff = 0; - let lastTokenInAlias = false; - // Map tokens from new command back to old command to attributing the correct original nodes. - const tokens = newCommand.tokens.map((newToken, index) => { - const tokenInAlias = - aliasStart < newToken.node.endIndex && - newToken.node.startIndex < aliasEnd; - tokenIndexDiff += tokenInAlias && lastTokenInAlias ? 1 : 0; - const { originalNode } = command.tokens[index - tokenIndexDiff]; - lastTokenInAlias = tokenInAlias; - return { ...newToken, originalNode }; - }); - - if (newCommand.tokens.length - command.tokens.length !== tokenIndexDiff) { - throw new SubstituteAliasError("Error substituting alias"); - } - - return { - originalTree: command.originalTree, - tree: newCommand.tree, - tokens, - }; -}; - -export const expandCommand = ( - command: Command, - _cursorIndex: number, - aliases: AliasMap, -): Command => { - let expanded = command; - const usedAliases = new Set(); - - // Check for aliases - let [name] = expanded.tokens; - while ( - expanded.tokens.length > 1 && - name && - aliases[name.text] && - !usedAliases.has(name.text) - ) { - // Remove quotes - const aliasValue = aliases[name.text].replace(/^'(.*)'$/g, "$1"); - try { - expanded = substituteAlias(expanded, name, aliasValue); - } catch (_err) { - // TODO(refactoring): add logger again - // console.error("Error substituting alias"); - } - usedAliases.add(name.text); - [name] = expanded.tokens; - } - - return expanded; -}; - -export const getCommand = ( - buffer: string, - aliases: AliasMap, - cursorIndex?: number, -): Command | null => { - const index = cursorIndex === undefined ? buffer.length : cursorIndex; - const parseTree = parse(buffer); - const commandNode = descendantAtIndex(parseTree, index, NodeType.Command); - if (commandNode === null) { - return null; - } - const command = convertCommandNodeToCommand(commandNode); - return expandCommand(command, index, aliases); -}; - -const statements = [ - NodeType.Program, - NodeType.CompoundStatement, - NodeType.Subshell, - NodeType.Pipeline, - NodeType.List, - NodeType.Command, -]; - -export const getTopLevelCommands = (parseTree: BaseNode): Command[] => { - if (parseTree.type === NodeType.Command) { - return [convertCommandNodeToCommand(parseTree)]; - } - if (!statements.includes(parseTree.type)) { - return []; - } - const commands: Command[] = []; - for (let i = 0; i < parseTree.children.length; i += 1) { - commands.push(...getTopLevelCommands(parseTree.children[i])); - } - return commands; -}; - -export const getAllCommandsWithAlias = ( - buffer: string, - aliases: AliasMap, -): Command[] => { - const parseTree = parse(buffer); - const commands = getTopLevelCommands(parseTree); - return commands.map((command) => - expandCommand(command, command.tree.text.length, aliases), - ); -}; diff --git a/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts b/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts index aa34c15d4810e..51c8bbf18e6a6 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts @@ -1,5 +1,5 @@ import { deepStrictEqual } from 'node:assert'; -import { getCommand, Command } from "../src/command"; +import { getCommand, Command } from "../command"; suite("shell-parser getCommand", () => { const aliases = { diff --git a/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts b/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts index d389137a68cea..a6097f8260aa5 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { parse } from "../src/parser"; +import { parse } from "../parser"; import { strictEqual } from 'node:assert'; function parseCommand(command: string): string { diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 2c5903e7e5db5..9413ee1ab33ae 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -20,7 +20,7 @@ import { getTokenType, TokenType } from './tokens'; import { PathExecutableCache } from './env/pathExecutableCache'; import { getFriendlyResourcePath } from './helpers/uri'; import { parseArguments } from './fig/autocomplete-parser/src/parseArguments'; -import { getCommand } from './fig/shell-parser/src/command'; +import { getCommand } from './fig/shell-parser/command'; // TODO: remove once API is finalized export const enum TerminalShellType { From cf7a01d8b079a0cdadbc9ae39bcfb0dd7e28f7af Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 02:14:37 -0800 Subject: [PATCH 12/51] Remove additional src dir in forked fig packages --- .../fig/autocomplete-helpers/{src => }/versions.d.ts | 0 .../src/fig/autocomplete-parser/{src => }/caches.ts | 2 +- .../src/fig/autocomplete-parser/{src => }/constants.ts | 0 .../src/fig/autocomplete-parser/{src => }/errors.ts | 2 +- .../autocomplete-parser/{src => }/parseArguments.ts | 10 +++++----- .../src/fig/autocomplete-shared/{src => }/convert.ts | 0 .../src/fig/autocomplete-shared/{src => }/index.ts | 0 .../src/fig/autocomplete-shared/{src => }/mixins.ts | 0 .../src/fig/autocomplete-shared/{src => }/revert.ts | 0 .../fig/autocomplete-shared/{src => }/specMetadata.ts | 0 .../src/fig/autocomplete-shared/{src => }/utils.ts | 0 .../src/fig/shared/{src => }/errors.ts | 0 .../src/fig/shared/{src => }/fuzzysort.d.ts | 0 .../src/fig/shared/{src => }/fuzzysort.js | 0 .../terminal-suggest/src/fig/shared/{src => }/index.ts | 0 .../src/fig/shared/{src => }/internal.ts | 2 +- .../terminal-suggest/src/fig/shared/{src => }/utils.ts | 2 +- .../terminal-suggest/src/fig/shell-parser/errors.ts | 2 +- extensions/terminal-suggest/src/terminalSuggestMain.ts | 2 +- 19 files changed, 11 insertions(+), 11 deletions(-) rename extensions/terminal-suggest/src/fig/autocomplete-helpers/{src => }/versions.d.ts (100%) rename extensions/terminal-suggest/src/fig/autocomplete-parser/{src => }/caches.ts (90%) rename extensions/terminal-suggest/src/fig/autocomplete-parser/{src => }/constants.ts (100%) rename extensions/terminal-suggest/src/fig/autocomplete-parser/{src => }/errors.ts (91%) rename extensions/terminal-suggest/src/fig/autocomplete-parser/{src => }/parseArguments.ts (99%) rename extensions/terminal-suggest/src/fig/autocomplete-shared/{src => }/convert.ts (100%) rename extensions/terminal-suggest/src/fig/autocomplete-shared/{src => }/index.ts (100%) rename extensions/terminal-suggest/src/fig/autocomplete-shared/{src => }/mixins.ts (100%) rename extensions/terminal-suggest/src/fig/autocomplete-shared/{src => }/revert.ts (100%) rename extensions/terminal-suggest/src/fig/autocomplete-shared/{src => }/specMetadata.ts (100%) rename extensions/terminal-suggest/src/fig/autocomplete-shared/{src => }/utils.ts (100%) rename extensions/terminal-suggest/src/fig/shared/{src => }/errors.ts (100%) rename extensions/terminal-suggest/src/fig/shared/{src => }/fuzzysort.d.ts (100%) rename extensions/terminal-suggest/src/fig/shared/{src => }/fuzzysort.js (100%) rename extensions/terminal-suggest/src/fig/shared/{src => }/index.ts (100%) rename extensions/terminal-suggest/src/fig/shared/{src => }/internal.ts (94%) rename extensions/terminal-suggest/src/fig/shared/{src => }/utils.ts (99%) diff --git a/extensions/terminal-suggest/src/fig/autocomplete-helpers/src/versions.d.ts b/extensions/terminal-suggest/src/fig/autocomplete-helpers/versions.d.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-helpers/src/versions.d.ts rename to extensions/terminal-suggest/src/fig/autocomplete-helpers/versions.d.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/caches.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/caches.ts similarity index 90% rename from extensions/terminal-suggest/src/fig/autocomplete-parser/src/caches.ts rename to extensions/terminal-suggest/src/fig/autocomplete-parser/caches.ts index b0313a3fcbfc2..f7cb937c142d9 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/caches.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/caches.ts @@ -1,4 +1,4 @@ -import { Subcommand } from "../../shared/src/internal"; +import { Subcommand } from "../shared/internal"; const allCaches: Array> = []; diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/constants.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-parser/src/constants.ts rename to extensions/terminal-suggest/src/fig/autocomplete-parser/constants.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/errors.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/errors.ts similarity index 91% rename from extensions/terminal-suggest/src/fig/autocomplete-parser/src/errors.ts rename to extensions/terminal-suggest/src/fig/autocomplete-parser/errors.ts index cfded140b5ed6..abf92ec4e827c 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/errors.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/errors.ts @@ -1,4 +1,4 @@ -import { createErrorInstance } from '../../shared/src/errors'; +import { createErrorInstance } from '../shared/errors'; // LoadSpecErrors export const MissingSpecError = createErrorInstance("MissingSpecError"); diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts similarity index 99% rename from extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts rename to extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts index b437beb88151f..a6bc1ec2ba9ed 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/src/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts @@ -1,13 +1,13 @@ // import { filepaths, folders } from "@fig/autocomplete-generators"; -import codeCompletionSpec from '../../../completions/code'; -import * as Internal from "../../shared/src/internal"; +import codeCompletionSpec from '../../completions/code'; +import * as Internal from "../shared/internal"; import { firstMatchingToken, makeArray, SpecLocationSource, SuggestionFlag, SuggestionFlags, -} from "../../shared/src/utils"; +} from "../shared/utils"; // import { // executeCommand, // executeLoginShell, @@ -18,7 +18,7 @@ import { import { Command, substituteAlias, -} from "../../shell-parser"; +} from "../shell-parser"; // import { // getSpecPath, // loadSubcommandCached, @@ -29,7 +29,7 @@ import { ParsingHistoryError, UpdateStateError, } from "./errors.js"; -import { convertSubcommand, initializeDefault } from '../../autocomplete-shared/src'; +import { convertSubcommand, initializeDefault } from '../autocomplete-shared'; const { exec } = require("child_process"); type ArgArrayState = { diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/convert.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/convert.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/src/convert.ts rename to extensions/terminal-suggest/src/fig/autocomplete-shared/convert.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/index.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/index.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/src/index.ts rename to extensions/terminal-suggest/src/fig/autocomplete-shared/index.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/mixins.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/mixins.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/src/mixins.ts rename to extensions/terminal-suggest/src/fig/autocomplete-shared/mixins.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/revert.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/revert.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/src/revert.ts rename to extensions/terminal-suggest/src/fig/autocomplete-shared/revert.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/specMetadata.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/specMetadata.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/src/specMetadata.ts rename to extensions/terminal-suggest/src/fig/autocomplete-shared/specMetadata.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/src/utils.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/utils.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/src/utils.ts rename to extensions/terminal-suggest/src/fig/autocomplete-shared/utils.ts diff --git a/extensions/terminal-suggest/src/fig/shared/src/errors.ts b/extensions/terminal-suggest/src/fig/shared/errors.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/shared/src/errors.ts rename to extensions/terminal-suggest/src/fig/shared/errors.ts diff --git a/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.d.ts b/extensions/terminal-suggest/src/fig/shared/fuzzysort.d.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/shared/src/fuzzysort.d.ts rename to extensions/terminal-suggest/src/fig/shared/fuzzysort.d.ts diff --git a/extensions/terminal-suggest/src/fig/shared/src/fuzzysort.js b/extensions/terminal-suggest/src/fig/shared/fuzzysort.js similarity index 100% rename from extensions/terminal-suggest/src/fig/shared/src/fuzzysort.js rename to extensions/terminal-suggest/src/fig/shared/fuzzysort.js diff --git a/extensions/terminal-suggest/src/fig/shared/src/index.ts b/extensions/terminal-suggest/src/fig/shared/index.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/shared/src/index.ts rename to extensions/terminal-suggest/src/fig/shared/index.ts diff --git a/extensions/terminal-suggest/src/fig/shared/src/internal.ts b/extensions/terminal-suggest/src/fig/shared/internal.ts similarity index 94% rename from extensions/terminal-suggest/src/fig/shared/src/internal.ts rename to extensions/terminal-suggest/src/fig/shared/internal.ts index 3a1ae7ccec227..fd755f8caf79e 100644 --- a/extensions/terminal-suggest/src/fig/shared/src/internal.ts +++ b/extensions/terminal-suggest/src/fig/shared/internal.ts @@ -1,4 +1,4 @@ -import { Internal, Metadata } from "../../autocomplete-shared/src"; +import { Internal, Metadata } from "../autocomplete-shared"; import type { Result } from "./fuzzysort"; export type SpecLocation = Fig.SpecLocation & { diff --git a/extensions/terminal-suggest/src/fig/shared/src/utils.ts b/extensions/terminal-suggest/src/fig/shared/utils.ts similarity index 99% rename from extensions/terminal-suggest/src/fig/shared/src/utils.ts rename to extensions/terminal-suggest/src/fig/shared/utils.ts index b6bd7b671aa4e..84af906e5cdd9 100644 --- a/extensions/terminal-suggest/src/fig/shared/src/utils.ts +++ b/extensions/terminal-suggest/src/fig/shared/utils.ts @@ -1,4 +1,4 @@ -import { osIsWindows } from '../../../helpers/os.js'; +import { osIsWindows } from '../../helpers/os.js'; import { createErrorInstance } from "./errors.js"; // Use bitwise representation of suggestion flags. diff --git a/extensions/terminal-suggest/src/fig/shell-parser/errors.ts b/extensions/terminal-suggest/src/fig/shell-parser/errors.ts index 0b6afeab64497..db30ee11d07a9 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/errors.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/errors.ts @@ -1,4 +1,4 @@ -import { createErrorInstance } from '../shared/src/errors'; +import { createErrorInstance } from '../shared/errors'; export const SubstituteAliasError = createErrorInstance("SubstituteAliasError"); export const ConvertCommandError = createErrorInstance("ConvertCommandError"); diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 9413ee1ab33ae..198e938c583d0 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -19,7 +19,7 @@ import { getPwshGlobals } from './shell/pwsh'; import { getTokenType, TokenType } from './tokens'; import { PathExecutableCache } from './env/pathExecutableCache'; import { getFriendlyResourcePath } from './helpers/uri'; -import { parseArguments } from './fig/autocomplete-parser/src/parseArguments'; +import { parseArguments } from './fig/autocomplete-parser/parseArguments'; import { getCommand } from './fig/shell-parser/command'; // TODO: remove once API is finalized From 988c880d54121ef7c2b5ef8b7cb57aea6a810b4b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 02:19:31 -0800 Subject: [PATCH 13/51] Add fig/shared/ unit tests --- .../src/fig/shared/test/utils.test.ts | 138 ++++++++++++++++++ .../src/fig/shell-parser/test/command.test.ts | 2 +- .../src/fig/shell-parser/test/parser.test.ts | 2 +- 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 extensions/terminal-suggest/src/fig/shared/test/utils.test.ts diff --git a/extensions/terminal-suggest/src/fig/shared/test/utils.test.ts b/extensions/terminal-suggest/src/fig/shared/test/utils.test.ts new file mode 100644 index 0000000000000..2d6c084293949 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/shared/test/utils.test.ts @@ -0,0 +1,138 @@ +import { deepStrictEqual, ok } from 'node:assert'; +import { + makeArray, + makeArrayIfExists, + longestCommonPrefix, + compareNamedObjectsAlphabetically, + fieldsAreEqual, +} from "../utils"; + +function expect(a: T): { toEqual: (b: T) => void } { + return { + toEqual: (b: T) => { + deepStrictEqual(a, b); + } + }; +} + +suite("fig/shared/ fieldsAreEqual", () => { + test("should return immediately if two values are the same", () => { + expect(fieldsAreEqual("hello", "hello", [])).toEqual(true); + expect(fieldsAreEqual("hello", "hell", [])).toEqual(false); + expect(fieldsAreEqual(1, 1, ["valueOf"])).toEqual(true); + expect(fieldsAreEqual(null, null, [])).toEqual(true); + expect(fieldsAreEqual(null, undefined, [])).toEqual(false); + expect(fieldsAreEqual(undefined, undefined, [])).toEqual(true); + expect(fieldsAreEqual(null, "hello", [])).toEqual(false); + expect(fieldsAreEqual(100, null, [])).toEqual(false); + expect(fieldsAreEqual({}, {}, [])).toEqual(true); + expect( + fieldsAreEqual( + () => { }, + () => { }, + [], + ), + ).toEqual(false); + }); + + test("should return true if fields are equal", () => { + const fn = () => { }; + expect( + fieldsAreEqual( + { + a: "hello", + b: 100, + c: undefined, + d: false, + e: fn, + f: { fa: true, fb: { fba: true } }, + g: null, + }, + { + a: "hello", + b: 100, + c: undefined, + d: false, + e: fn, + f: { fa: true, fb: { fba: true } }, + g: null, + }, + ["a", "b", "c", "d", "e", "f", "g"], + ), + ).toEqual(true); + expect(fieldsAreEqual({ a: {} }, { a: {} }, ["a"])).toEqual(true); + }); + + test("should return false if any field is not equal or fields are not specified", () => { + expect(fieldsAreEqual({ a: null }, { a: {} }, ["a"])).toEqual(false); + expect(fieldsAreEqual({ a: undefined }, { a: "hello" }, ["a"])).toEqual( + false, + ); + expect(fieldsAreEqual({ a: false }, { a: true }, ["a"])).toEqual(false); + expect( + fieldsAreEqual( + { a: { b: { c: "hello" } } }, + { a: { b: { c: "hell" } } }, + ["a"], + ), + ).toEqual(false); + expect(fieldsAreEqual({ a: "true" }, { b: "true" }, [])).toEqual(false); + }); +}); + +suite("fig/shared/ makeArray", () => { + test("should transform an object into an array", () => { + expect(makeArray(true)).toEqual([true]); + }); + + test("should not transform arrays with one value", () => { + expect(makeArray([true])).toEqual([true]); + }); + + test("should not transform arrays with multiple values", () => { + expect(makeArray([true, false])).toEqual([true, false]); + }); +}); + +suite("fig/shared/ makeArrayIfExists", () => { + test("works", () => { + expect(makeArrayIfExists(null)).toEqual(null); + expect(makeArrayIfExists(undefined)).toEqual(null); + expect(makeArrayIfExists("a")).toEqual(["a"]); + expect(makeArrayIfExists(["a"])).toEqual(["a"]); + }); +}); + +suite("fig/shared/ longestCommonPrefix", () => { + test("should return the shared match", () => { + expect(longestCommonPrefix(["foo", "foo bar", "foo hello world"])).toEqual( + "foo", + ); + }); + + test("should return nothing if not all items starts by the same chars", () => { + expect(longestCommonPrefix(["foo", "foo bar", "hello world"])).toEqual(""); + }); +}); + +suite("fig/shared/ compareNamedObjectsAlphabetically", () => { + test("should return 1 to sort alphabetically z against b for string", () => { + ok(compareNamedObjectsAlphabetically("z", "b") > 0); + }); + + test("should return 1 to sort alphabetically z against b for object with name", () => { + ok(compareNamedObjectsAlphabetically({ name: "z" }, { name: "b" }) > 0); + }); + + test("should return 1 to sort alphabetically c against x for object with name", () => { + ok(compareNamedObjectsAlphabetically({ name: "c" }, { name: "x" }) < 0); + }); + + test("should return 1 to sort alphabetically z against b for object with name array", () => { + ok(compareNamedObjectsAlphabetically({ name: ["z"] }, { name: ["b"] }) > 0); + }); + + test("should return 1 to sort alphabetically c against x for object with name array", () => { + ok(compareNamedObjectsAlphabetically({ name: ["c"] }, { name: ["x"] }) < 0); + }); +}); diff --git a/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts b/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts index 51c8bbf18e6a6..adb7539e4a45a 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/test/command.test.ts @@ -1,7 +1,7 @@ import { deepStrictEqual } from 'node:assert'; import { getCommand, Command } from "../command"; -suite("shell-parser getCommand", () => { +suite("fig/shell-parser/ getCommand", () => { const aliases = { woman: "man", quote: "'q'", diff --git a/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts b/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts index a6097f8260aa5..4768cf3bd0d83 100644 --- a/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts +++ b/extensions/terminal-suggest/src/fig/shell-parser/test/parser.test.ts @@ -64,7 +64,7 @@ function getData( // ]; // } -suite("shell-parser fixtures", () => { +suite("fig/shell-parser/ fixtures", () => { const fixturesPath = path.join(__dirname, "../../../../fixtures/shell-parser"); const fixtures = fs.readdirSync(fixturesPath); for (const fixture of fixtures) { From 5a041bba7576c9d23a958bc6614f6d276d1cbe57 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 02:22:50 -0800 Subject: [PATCH 14/51] Remove unneeded files/content, move chmanifest to ext root --- extensions/terminal-suggest/cgmanifest.json | 71 ++++- extensions/terminal-suggest/src/fig/README.md | 2 +- .../fig/autocomplete-helpers/versions.d.ts | 7 - .../terminal-suggest/src/fig/cgmanifest.json | 73 ----- .../src/fig/shared/fuzzysort.d.ts | 85 ------ .../src/fig/shared/fuzzysort.js | 257 ------------------ .../src/fig/shared/internal.ts | 3 +- 7 files changed, 72 insertions(+), 426 deletions(-) delete mode 100644 extensions/terminal-suggest/src/fig/autocomplete-helpers/versions.d.ts delete mode 100644 extensions/terminal-suggest/src/fig/cgmanifest.json delete mode 100644 extensions/terminal-suggest/src/fig/shared/fuzzysort.d.ts delete mode 100644 extensions/terminal-suggest/src/fig/shared/fuzzysort.js diff --git a/extensions/terminal-suggest/cgmanifest.json b/extensions/terminal-suggest/cgmanifest.json index e0eaafe83ca67..c13f6e6a15288 100644 --- a/extensions/terminal-suggest/cgmanifest.json +++ b/extensions/terminal-suggest/cgmanifest.json @@ -14,6 +14,75 @@ "url": "https://github.com/withfig/autocomplete/blob/main/LICENSE.md" }, "description": "IDE-style autocomplete for your existing terminal & shell from withfig/autocomplete." + }, + { + "component": { + "type": "git", + "git": { + "name": "amazon-q-developer-cli", + "repositoryUrl": "https://github.com/aws/amazon-q-developer-cli", + "commitHash": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d" + } + }, + "licenseDetail": [ + "MIT License", + "", + "Copyright (c) 2024 Amazon.com, Inc. or its affiliates.", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "version": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d" + }, + { + "component": { + "type": "git", + "git": { + "name": "@fig/autocomplete-shared", + "repositoryUrl": "https://github.com/withfig/autocomplete-tools/blob/main/shared", + "commitHash": "104377c19a91ca8a312cb38c115a74468f6227cb" + } + }, + "licenseDetail": [ + "MIT License", + "", + "Copyright (c) 2021 Hercules Labs Inc. (Fig)", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE." + ], + "version": "1.1.2" } - ] + ], + "version": 1 } diff --git a/extensions/terminal-suggest/src/fig/README.md b/extensions/terminal-suggest/src/fig/README.md index c348d54958546..20c105f5d65e8 100644 --- a/extensions/terminal-suggest/src/fig/README.md +++ b/extensions/terminal-suggest/src/fig/README.md @@ -2,4 +2,4 @@ This folder contains the `autocomplete-parser` project from https://github.com/a - They ship as ESM modules which we're not ready to consume just yet. - We want the more complete `autocomplete-parser` that contains the important `parseArguments` function that does the bulk of the smarts in parsing the fig commands. -- We needed to strip out all the implementation-specific parts from their `api-bindings` project that deals with settings, IPC, etc. +- We needed to strip out all the implementation-specific parts from their `api-bindings` project that deals with settings, IPC, fuzzy sorting, etc. diff --git a/extensions/terminal-suggest/src/fig/autocomplete-helpers/versions.d.ts b/extensions/terminal-suggest/src/fig/autocomplete-helpers/versions.d.ts deleted file mode 100644 index d90a9749c2e7c..0000000000000 --- a/extensions/terminal-suggest/src/fig/autocomplete-helpers/versions.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export declare const applySpecDiff: (spec: Fig.Subcommand, diff: Fig.SpecDiff) => Fig.Subcommand; -export declare const diffSpecs: (original: Fig.Subcommand, updated: Fig.Subcommand) => Fig.SpecDiff; -export declare const getVersionFromVersionedSpec: (base: Fig.Subcommand, versions: Fig.VersionDiffMap, target?: string) => { - version: string; - spec: Fig.Subcommand; -}; -export declare const createVersionedSpec: (specName: string, versionFiles: string[]) => Fig.Spec; diff --git a/extensions/terminal-suggest/src/fig/cgmanifest.json b/extensions/terminal-suggest/src/fig/cgmanifest.json deleted file mode 100644 index 894a1add38890..0000000000000 --- a/extensions/terminal-suggest/src/fig/cgmanifest.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "registrations": [ - { - "component": { - "type": "git", - "git": { - "name": "amazon-q-developer-cli", - "repositoryUrl": "https://github.com/aws/amazon-q-developer-cli", - "commitHash": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d" - } - }, - "licenseDetail": [ - "MIT License", - "", - "Copyright (c) 2024 Amazon.com, Inc. or its affiliates.", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." - ], - "version": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d" - }, - { - "component": { - "type": "git", - "git": { - "name": "@fig/autocomplete-shared", - "repositoryUrl": "https://github.com/withfig/autocomplete-tools/blob/main/shared", - "commitHash": "104377c19a91ca8a312cb38c115a74468f6227cb" - } - }, - "licenseDetail": [ - "MIT License", - "", - "Copyright (c) 2021 Hercules Labs Inc. (Fig)", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in all", - "copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", - "SOFTWARE." - ], - "version": "1.1.2" - } - ], - "version": 1 -} diff --git a/extensions/terminal-suggest/src/fig/shared/fuzzysort.d.ts b/extensions/terminal-suggest/src/fig/shared/fuzzysort.d.ts deleted file mode 100644 index bfbc377bfe1c4..0000000000000 --- a/extensions/terminal-suggest/src/fig/shared/fuzzysort.d.ts +++ /dev/null @@ -1,85 +0,0 @@ - - export interface Result { - /** - * Higher is better - * - * 0 is a perfect match; -1000 is a bad match - */ - readonly score: number; - - /** Your original target string */ - readonly target: string; - - /** Indexes of the matching target characters */ - readonly indexes: number[]; - } - interface Results extends ReadonlyArray { - /** Total matches before limit */ - readonly total: number; - } - - interface KeyResult extends Result { - /** Your original object */ - readonly obj: T; - } - interface KeysResult extends ReadonlyArray { - /** - * Higher is better - * - * 0 is a perfect match; -1000 is a bad match - */ - readonly score: number; - - /** Your original object */ - readonly obj: T; - } - interface KeyResults extends ReadonlyArray> { - /** Total matches before limit */ - readonly total: number; - } - interface KeysResults extends ReadonlyArray> { - /** Total matches before limit */ - readonly total: number; - } - - interface Prepared { - /** Your original target string */ - readonly target: string; - } - - interface CancelablePromise extends Promise { - cancel(): void; - } - - interface Options { - /** Don't return matches worse than this (higher is faster) */ - threshold?: number; - - /** Don't return more results than this (lower is faster) */ - limit?: number; - - /** Allows a snigle transpoes (false is faster) */ - allowTypo?: boolean; - } - interface KeyOptions extends Options { - key: string | ReadonlyArray; - } - interface KeysOptions extends Options { - keys: ReadonlyArray>; - scoreFn?: (keysResult: ReadonlyArray>) => number; - } - - interface Fuzzysort { - /** - * Help the algorithm go fast by providing prepared targets instead of raw strings - */ - prepare(target: string): Prepared | undefined; - highlight( - result?: Result, - highlightOpen?: string, - highlightClose?: string, - ): string | null; - single(search: string, target: string | Prepared): Result | null; - } - - diff --git a/extensions/terminal-suggest/src/fig/shared/fuzzysort.js b/extensions/terminal-suggest/src/fig/shared/fuzzysort.js deleted file mode 100644 index 662278b56bbaa..0000000000000 --- a/extensions/terminal-suggest/src/fig/shared/fuzzysort.js +++ /dev/null @@ -1,257 +0,0 @@ -/* - * - * - * - * NOTE: we copied and edited a local version of fuzzysort that only contains functions we require - * - * - * - */ - -var isNode = typeof require !== "undefined" && typeof window === "undefined"; -var preparedCache = new Map(); -var preparedSearchCache = new Map(); -var noResults = []; -noResults.total = 0; -var matchesSimple = []; -var matchesStrict = []; -function cleanup() { - preparedCache.clear(); - preparedSearchCache.clear(); - matchesSimple = []; - matchesStrict = []; -} -function isObj(x) { - return typeof x === "object"; -} // faster as a function - -/** - * WHAT: SublimeText-like Fuzzy Search - * USAGE: - * fuzzysort.single('fs', 'Fuzzy Search') // {score: -16} - * fuzzysort.single('test', 'test') // {score: 0} - * fuzzysort.single('doesnt exist', 'target') // null - * - * fuzzysort.go('mr', ['Monitor.cpp', 'MeshRenderer.cpp']) - * // [{score: -18, target: "MeshRenderer.cpp"}, {score: -6009, target: "Monitor.cpp"}] - * - * fuzzysort.highlight(fuzzysort.single('fs', 'Fuzzy Search'), '', '') - * // Fuzzy Search - */ -export const fuzzysort = { - single: function (search, target) { - if (!search) return null; - if (!isObj(search)) search = fuzzysort.getPreparedSearch(search); - - if (!target) return null; - if (!isObj(target)) target = fuzzysort.getPrepared(target); - return fuzzysort.algorithm(search, target, search[0]); - }, - - highlight: function (result, hOpen, hClose) { - if (result === null) return null; - if (hOpen === undefined) hOpen = ""; - if (hClose === undefined) hClose = ""; - var highlighted = ""; - var matchesIndex = 0; - var opened = false; - var target = result.target; - var targetLen = target.length; - var matchesBest = result.indexes; - for (var i = 0; i < targetLen; ++i) { - var char = target[i]; - if (matchesBest[matchesIndex] === i) { - ++matchesIndex; - if (!opened) { - opened = true; - highlighted += hOpen; - } - - if (matchesIndex === matchesBest.length) { - highlighted += char + hClose + target.substr(i + 1); - break; - } - } else { - if (opened) { - opened = false; - highlighted += hClose; - } - } - highlighted += char; - } - - return highlighted; - }, - - prepare: function (target) { - if (!target) return; - return { - target: target, - _targetLowerCodes: fuzzysort.prepareLowerCodes(target), - _nextBeginningIndexes: null, - score: null, - indexes: null, - obj: null, - }; // hidden - }, - prepareSearch: function (search) { - if (!search) return; - return fuzzysort.prepareLowerCodes(search); - }, - - getPrepared: function (target) { - if (target.length > 999) return fuzzysort.prepare(target); // don't cache huge targets - var targetPrepared = preparedCache.get(target); - if (targetPrepared !== undefined) return targetPrepared; - targetPrepared = fuzzysort.prepare(target); - preparedCache.set(target, targetPrepared); - return targetPrepared; - }, - getPreparedSearch: function (search) { - if (search.length > 999) return fuzzysort.prepareSearch(search); // don't cache huge searches - var searchPrepared = preparedSearchCache.get(search); - if (searchPrepared !== undefined) return searchPrepared; - searchPrepared = fuzzysort.prepareSearch(search); - preparedSearchCache.set(search, searchPrepared); - return searchPrepared; - }, - - algorithm: function (searchLowerCodes, prepared, searchLowerCode) { - var targetLowerCodes = prepared._targetLowerCodes; - var searchLen = searchLowerCodes.length; - var targetLen = targetLowerCodes.length; - var searchI = 0; // where we at - var targetI = 0; // where you at - var matchesSimpleLen = 0; - - // very basic fuzzy match; to remove non-matching targets ASAP! - // walk through target. find sequential matches. - // if all chars aren't found then exit - for (;;) { - var isMatch = searchLowerCode === targetLowerCodes[targetI]; - if (isMatch) { - matchesSimple[matchesSimpleLen++] = targetI; - ++searchI; - if (searchI === searchLen) break; - searchLowerCode = searchLowerCodes[searchI]; - } - ++targetI; - if (targetI >= targetLen) return null; // Failed to find searchI - } - - var searchI = 0; - var successStrict = false; - var matchesStrictLen = 0; - - var nextBeginningIndexes = prepared._nextBeginningIndexes; - if (nextBeginningIndexes === null) - nextBeginningIndexes = prepared._nextBeginningIndexes = - fuzzysort.prepareNextBeginningIndexes(prepared.target); - var firstPossibleI = (targetI = - matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[matchesSimple[0] - 1]); - - // Our target string successfully matched all characters in sequence! - // Let's try a more advanced and strict test to improve the score - // only count it as a match if it's consecutive or a beginning character! - if (targetI !== targetLen) - for (;;) { - if (targetI >= targetLen) { - // We failed to find a good spot for this search char, go back to the previous search char and force it forward - if (searchI <= 0) break; // We failed to push chars forward for a better match - - --searchI; - var lastMatch = matchesStrict[--matchesStrictLen]; - targetI = nextBeginningIndexes[lastMatch]; - } else { - var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]; - if (isMatch) { - matchesStrict[matchesStrictLen++] = targetI; - ++searchI; - if (searchI === searchLen) { - successStrict = true; - break; - } - ++targetI; - } else { - targetI = nextBeginningIndexes[targetI]; - } - } - } - - { - // tally up the score & keep track of matches for highlighting later - if (successStrict) { - var matchesBest = matchesStrict; - var matchesBestLen = matchesStrictLen; - } else { - var matchesBest = matchesSimple; - var matchesBestLen = matchesSimpleLen; - } - var score = 0; - var lastTargetI = -1; - for (var i = 0; i < searchLen; ++i) { - var targetI = matchesBest[i]; - // score only goes down if they're not consecutive - if (lastTargetI !== targetI - 1) score -= targetI; - lastTargetI = targetI; - } - if (!successStrict) score *= 1000; - score -= targetLen - searchLen; - prepared.score = score; - prepared.indexes = new Array(matchesBestLen); - for (var i = matchesBestLen - 1; i >= 0; --i) - prepared.indexes[i] = matchesBest[i]; - - return prepared; - } - }, - - prepareLowerCodes: function (str) { - var strLen = str.length; - var lowerCodes = []; // new Array(strLen) sparse array is too slow - var lower = str.toLowerCase(); - for (var i = 0; i < strLen; ++i) lowerCodes[i] = lower.charCodeAt(i); - return lowerCodes; - }, - prepareBeginningIndexes: function (target) { - var targetLen = target.length; - var beginningIndexes = []; - var beginningIndexesLen = 0; - var wasUpper = false; - var wasAlphanum = false; - for (var i = 0; i < targetLen; ++i) { - var targetCode = target.charCodeAt(i); - var isUpper = targetCode >= 65 && targetCode <= 90; - var isAlphanum = - isUpper || - (targetCode >= 97 && targetCode <= 122) || - (targetCode >= 48 && targetCode <= 57); - var isBeginning = (isUpper && !wasUpper) || !wasAlphanum || !isAlphanum; - wasUpper = isUpper; - wasAlphanum = isAlphanum; - if (isBeginning) beginningIndexes[beginningIndexesLen++] = i; - } - return beginningIndexes; - }, - prepareNextBeginningIndexes: function (target) { - var targetLen = target.length; - var beginningIndexes = fuzzysort.prepareBeginningIndexes(target); - var nextBeginningIndexes = []; // new Array(targetLen) sparse array is too slow - var lastIsBeginning = beginningIndexes[0]; - var lastIsBeginningI = 0; - for (var i = 0; i < targetLen; ++i) { - if (lastIsBeginning > i) { - nextBeginningIndexes[i] = lastIsBeginning; - } else { - lastIsBeginning = beginningIndexes[++lastIsBeginningI]; - nextBeginningIndexes[i] = - lastIsBeginning === undefined ? targetLen : lastIsBeginning; - } - } - return nextBeginningIndexes; - }, - - cleanup: cleanup, -}; - -export default fuzzysort; diff --git a/extensions/terminal-suggest/src/fig/shared/internal.ts b/extensions/terminal-suggest/src/fig/shared/internal.ts index fd755f8caf79e..ca1a862f6259c 100644 --- a/extensions/terminal-suggest/src/fig/shared/internal.ts +++ b/extensions/terminal-suggest/src/fig/shared/internal.ts @@ -1,5 +1,4 @@ import { Internal, Metadata } from "../autocomplete-shared"; -import type { Result } from "./fuzzysort"; export type SpecLocation = Fig.SpecLocation & { diffVersionedFile?: string; @@ -20,7 +19,7 @@ export type Suggestion = Override< // Generator information to determine whether suggestion should be filtered. generator?: Fig.Generator; getQueryTerm?: (x: string) => string; - fuzzyMatchData?: (Result | null)[]; + // fuzzyMatchData?: (Result | null)[]; originalType?: SuggestionType; } >; From 6aaeebfd4518ff9e5745af60ec8ce179d28f15ce Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 02:26:35 -0800 Subject: [PATCH 15/51] Tweak folder names, rewrite require to import --- .../src/fig/autocomplete-parser/parseArguments.ts | 6 +++--- .../terminal-suggest/src/fig/autocomplete-shared/utils.ts | 8 -------- .../convert.ts | 0 .../index.ts | 0 .../mixins.ts | 0 .../revert.ts | 0 .../specMetadata.ts | 0 .../src/fig/fig-autocomplete-shared/utils.ts | 8 ++++++++ extensions/terminal-suggest/src/fig/shared/internal.ts | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 extensions/terminal-suggest/src/fig/autocomplete-shared/utils.ts rename extensions/terminal-suggest/src/fig/{autocomplete-shared => fig-autocomplete-shared}/convert.ts (100%) rename extensions/terminal-suggest/src/fig/{autocomplete-shared => fig-autocomplete-shared}/index.ts (100%) rename extensions/terminal-suggest/src/fig/{autocomplete-shared => fig-autocomplete-shared}/mixins.ts (100%) rename extensions/terminal-suggest/src/fig/{autocomplete-shared => fig-autocomplete-shared}/revert.ts (100%) rename extensions/terminal-suggest/src/fig/{autocomplete-shared => fig-autocomplete-shared}/specMetadata.ts (100%) create mode 100644 extensions/terminal-suggest/src/fig/fig-autocomplete-shared/utils.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts index a6bc1ec2ba9ed..a28eda84106a7 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts @@ -29,8 +29,8 @@ import { ParsingHistoryError, UpdateStateError, } from "./errors.js"; -import { convertSubcommand, initializeDefault } from '../autocomplete-shared'; -const { exec } = require("child_process"); +import { convertSubcommand, initializeDefault } from '../fig-autocomplete-shared'; +import { exec, type ExecException } from 'child_process'; type ArgArrayState = { args: Array | null; @@ -1107,7 +1107,7 @@ const executeLoginShell = async ({ executable: string; }): Promise => { return new Promise((resolve, reject) => { - exec(`${executable} -c "${command}"`, (error: Error, stdout: string, stderr: string) => { + exec(`${executable} -c "${command}"`, (error: ExecException | null, stdout: string, stderr: string) => { if (error) { reject(stderr); } else { diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/utils.ts b/extensions/terminal-suggest/src/fig/autocomplete-shared/utils.ts deleted file mode 100644 index 1e9afcefa3e2c..0000000000000 --- a/extensions/terminal-suggest/src/fig/autocomplete-shared/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function makeArray(object: T | T[]): T[] { - return Array.isArray(object) ? object : [object]; -} - -export enum SpecLocationSource { - GLOBAL = "global", - LOCAL = "local", -} diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/convert.ts b/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/convert.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/convert.ts rename to extensions/terminal-suggest/src/fig/fig-autocomplete-shared/convert.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/index.ts b/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/index.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/index.ts rename to extensions/terminal-suggest/src/fig/fig-autocomplete-shared/index.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/mixins.ts b/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/mixins.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/mixins.ts rename to extensions/terminal-suggest/src/fig/fig-autocomplete-shared/mixins.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/revert.ts b/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/revert.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/revert.ts rename to extensions/terminal-suggest/src/fig/fig-autocomplete-shared/revert.ts diff --git a/extensions/terminal-suggest/src/fig/autocomplete-shared/specMetadata.ts b/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/specMetadata.ts similarity index 100% rename from extensions/terminal-suggest/src/fig/autocomplete-shared/specMetadata.ts rename to extensions/terminal-suggest/src/fig/fig-autocomplete-shared/specMetadata.ts diff --git a/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/utils.ts b/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/utils.ts new file mode 100644 index 0000000000000..979d2084c3ee8 --- /dev/null +++ b/extensions/terminal-suggest/src/fig/fig-autocomplete-shared/utils.ts @@ -0,0 +1,8 @@ +export function makeArray(object: T | T[]): T[] { + return Array.isArray(object) ? object : [object]; +} + +export enum SpecLocationSource { + GLOBAL = "global", + LOCAL = "local", +} diff --git a/extensions/terminal-suggest/src/fig/shared/internal.ts b/extensions/terminal-suggest/src/fig/shared/internal.ts index ca1a862f6259c..3460d7be4a682 100644 --- a/extensions/terminal-suggest/src/fig/shared/internal.ts +++ b/extensions/terminal-suggest/src/fig/shared/internal.ts @@ -1,4 +1,4 @@ -import { Internal, Metadata } from "../autocomplete-shared"; +import { Internal, Metadata } from "../fig-autocomplete-shared"; export type SpecLocation = Fig.SpecLocation & { diffVersionedFile?: string; From 2172c4e0fdbb2c7e7b2de245cef3d9f32d240187 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 6 Feb 2025 02:48:42 -0800 Subject: [PATCH 16/51] Pass in code spec --- .../src/fig/autocomplete-parser/parseArguments.ts | 8 +++----- extensions/terminal-suggest/src/terminalSuggestMain.ts | 6 +++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts index a28eda84106a7..d087fb1d1dd7d 100644 --- a/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts +++ b/extensions/terminal-suggest/src/fig/autocomplete-parser/parseArguments.ts @@ -1,5 +1,4 @@ // import { filepaths, folders } from "@fig/autocomplete-generators"; -import codeCompletionSpec from '../../completions/code'; import * as Internal from "../shared/internal"; import { firstMatchingToken, @@ -784,8 +783,8 @@ export const initialParserState = getResultFromState( const parseArgumentsCached = async ( command: Command, context: Fig.ShellContext, + spec: Fig.Spec, // authClient: AuthClient, - specLocations?: Internal.SpecLocation[], isParsingHistory?: boolean, startIndex = 0, // localconsole: console.console = console, @@ -797,8 +796,6 @@ const parseArgumentsCached = async ( let tokens = currentCommand.tokens.slice(startIndex); // const tokenText = tokens.map((token) => token.text); - // TODO: Fill this in the with actual one (replace specLocations) - const spec = codeCompletionSpec; // null!; const specPath: Fig.SpecLocation = { type: 'global', name: 'fake' }; // tokenTest[0] is the command and the spec they need @@ -1120,6 +1117,7 @@ const executeLoginShell = async ({ export const parseArguments = async ( command: Command | null, context: Fig.ShellContext, + spec: Fig.Spec, // authClient: AuthClient, isParsingHistory = false, // localconsole: console.console = console, @@ -1152,7 +1150,7 @@ export const parseArguments = async ( command, context, // authClient, - undefined, + spec, isParsingHistory, 0, ); diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 198e938c583d0..e4e362ea5023a 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -116,7 +116,11 @@ export async function activate(context: vscode.ExtensionContext) { // replacing get options/args from spec with the parsed arguments // to do: items for folders/files // use suggestion flags to determine which to provide - const parsedArguments = await parseArguments(getCommand(terminalContext.commandLine, {}, terminalContext.cursorPosition), { environmentVariables: env, currentWorkingDirectory: terminal.shellIntegration!.cwd!.fsPath, sshPrefix: '', currentProcess: terminal.name }); + const parsedArguments = await parseArguments( + getCommand(terminalContext.commandLine, {}, terminalContext.cursorPosition), + { environmentVariables: env, currentWorkingDirectory: terminal.shellIntegration!.cwd!.fsPath, sshPrefix: '', currentProcess: terminal.name }, + codeCompletionSpec, + ); console.log(parsedArguments); const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; From b997cfb58bd492f5e436cf3dc9d93934919b3716 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 6 Feb 2025 15:31:52 -0600 Subject: [PATCH 17/51] get parsing to work for options/args --- .../src/terminalSuggestMain.ts | 124 ++++++++++++++---- 1 file changed, 96 insertions(+), 28 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index e4e362ea5023a..e3d4fe8be6ff1 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -19,8 +19,9 @@ import { getPwshGlobals } from './shell/pwsh'; import { getTokenType, TokenType } from './tokens'; import { PathExecutableCache } from './env/pathExecutableCache'; import { getFriendlyResourcePath } from './helpers/uri'; -import { parseArguments } from './fig/autocomplete-parser/parseArguments'; +import { ArgumentParserResult, parseArguments } from './fig/autocomplete-parser/parseArguments'; import { getCommand } from './fig/shell-parser/command'; +import { SuggestionFlag } from './fig/shared/utils'; // TODO: remove once API is finalized export const enum TerminalShellType { @@ -112,20 +113,10 @@ export async function activate(context: vscode.ExtensionContext) { } } } - // to do figure out spec, pass that in only if it's valid - // replacing get options/args from spec with the parsed arguments - // to do: items for folders/files - // use suggestion flags to determine which to provide - const parsedArguments = await parseArguments( - getCommand(terminalContext.commandLine, {}, terminalContext.cursorPosition), - { environmentVariables: env, currentWorkingDirectory: terminal.shellIntegration!.cwd!.fsPath, sshPrefix: '', currentProcess: terminal.name }, - codeCompletionSpec, - ); - console.log(parsedArguments); const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition); const pathSeparator = isWindows ? '\\' : '/'; const tokenType = getTokenType(terminalContext, shellType); - const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, token); + const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, env, terminal.name, token); if (terminal.shellIntegration?.env) { const homeDirCompletion = result.items.find(i => i.label === '~'); if (homeDirCompletion && terminal.shellIntegration.env.HOME) { @@ -245,6 +236,72 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } +function addSuggestionsFromParsedArguments(parsedArguments: ArgumentParserResult, prefix: string, terminalContext: any, items: vscode.TerminalCompletionItem[]) { + const addSuggestions = (parsedArguments: ArgumentParserResult, kind: vscode.TerminalCompletionItemKind) => { + switch (kind) { + case vscode.TerminalCompletionItemKind.Argument: + if (parsedArguments.currentArg?.suggestions) { + for (const item of parsedArguments.currentArg.suggestions) { + const suggestionLabels = getLabel(item); + if (!suggestionLabels) { + continue; + } + for (const suggestionLabel of suggestionLabels) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: suggestionLabel }, typeof item === 'string' ? item : item.description, undefined, kind)); + } + } + } + break; + case vscode.TerminalCompletionItemKind.Flag: + if (parsedArguments.completionObj.options) { + for (const item of Object.values(parsedArguments.completionObj.options)) { + const suggestionLabels = getLabel(item); + if (!suggestionLabels) { + continue; + } + for (const suggestionLabel of suggestionLabels) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: suggestionLabel }, item.description, undefined, kind)); + } + } + } + break; + case vscode.TerminalCompletionItemKind.Method: { + if (parsedArguments.completionObj.subcommands) { + for (const item of Object.values(parsedArguments.completionObj.subcommands)) { + const suggestionLabels = getLabel(item); + if (!suggestionLabels) { + continue; + } + for (const suggestionLabel of suggestionLabels) { + items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: suggestionLabel }, item.description, undefined, kind)); + } + } + } + break; + } + } + }; + + switch (parsedArguments.suggestionFlags) { + case SuggestionFlag.None: + break; + case SuggestionFlag.Args: + addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Argument); + break; + case SuggestionFlag.Subcommands: + addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Method); + break; + case SuggestionFlag.Options: + addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Flag); + break; + case SuggestionFlag.Any: + addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Method); + addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Flag); + addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Argument); + break; + } +} + export async function getCompletionItemsFromSpecs( specs: Fig.Spec[], terminalContext: { commandLine: string; cursorPosition: number }, @@ -252,6 +309,8 @@ export async function getCompletionItemsFromSpecs( prefix: string, tokenType: TokenType, shellIntegrationCwd?: vscode.Uri, + env?: Record, + name?: string, token?: vscode.CancellationToken ): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> { const items: vscode.TerminalCompletionItem[] = []; @@ -304,27 +363,36 @@ export async function getCompletionItemsFromSpecs( continue; } - const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); - if (optionsCompletionResult) { - items.push(...optionsCompletionResult.items); - filesRequested ||= optionsCompletionResult.filesRequested; - foldersRequested ||= optionsCompletionResult.foldersRequested; - specificItemsProvided ||= optionsCompletionResult.items.length > 0; - } - if (!optionsCompletionResult?.isOptionArg) { - const argsCompletionResult = handleArguments(specLabel, spec, terminalContext, precedingText); - if (argsCompletionResult) { - items.push(...argsCompletionResult.items); - filesRequested ||= argsCompletionResult.filesRequested; - foldersRequested ||= argsCompletionResult.foldersRequested; - specificItemsProvided ||= argsCompletionResult.items.length > 0; + // to do: items for folders/files + if (shellIntegrationCwd && env && name) { + const parsedArguments: ArgumentParserResult = await parseArguments( + // TODO: pass in aliases + getCommand(terminalContext.commandLine, {}, terminalContext.cursorPosition), + { environmentVariables: env, currentWorkingDirectory: shellIntegrationCwd?.fsPath, sshPrefix: '', currentProcess: name }, + spec, + ); + addSuggestionsFromParsedArguments(parsedArguments, prefix, terminalContext, items); + } else { + const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix); + if (optionsCompletionResult) { + items.push(...optionsCompletionResult.items); + filesRequested ||= optionsCompletionResult.filesRequested; + foldersRequested ||= optionsCompletionResult.foldersRequested; + specificItemsProvided ||= optionsCompletionResult.items.length > 0; + } + if (!optionsCompletionResult?.isOptionArg) { + const argsCompletionResult = handleArguments(specLabel, spec, terminalContext, precedingText); + if (argsCompletionResult) { + items.push(...argsCompletionResult.items); + filesRequested ||= argsCompletionResult.filesRequested; + foldersRequested ||= argsCompletionResult.foldersRequested; + specificItemsProvided ||= argsCompletionResult.items.length > 0; + } } } } } - - if (tokenType === TokenType.Command) { // Include builitin/available commands in the results const labels = new Set(items.map((i) => i.label)); From 6b8ccd0aa2848df06b46d636998a3e92fa65e041 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 6 Feb 2025 15:51:04 -0600 Subject: [PATCH 18/51] Refactor --- .../src/terminalSuggestMain.ts | 77 +++++++------------ 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index e3d4fe8be6ff1..fa7792c01af78 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -236,48 +236,29 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } -function addSuggestionsFromParsedArguments(parsedArguments: ArgumentParserResult, prefix: string, terminalContext: any, items: vscode.TerminalCompletionItem[]) { - const addSuggestions = (parsedArguments: ArgumentParserResult, kind: vscode.TerminalCompletionItemKind) => { - switch (kind) { - case vscode.TerminalCompletionItemKind.Argument: - if (parsedArguments.currentArg?.suggestions) { - for (const item of parsedArguments.currentArg.suggestions) { - const suggestionLabels = getLabel(item); - if (!suggestionLabels) { - continue; - } - for (const suggestionLabel of suggestionLabels) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: suggestionLabel }, typeof item === 'string' ? item : item.description, undefined, kind)); - } - } - } - break; - case vscode.TerminalCompletionItemKind.Flag: - if (parsedArguments.completionObj.options) { - for (const item of Object.values(parsedArguments.completionObj.options)) { - const suggestionLabels = getLabel(item); - if (!suggestionLabels) { - continue; - } - for (const suggestionLabel of suggestionLabels) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: suggestionLabel }, item.description, undefined, kind)); - } - } - } - break; - case vscode.TerminalCompletionItemKind.Method: { - if (parsedArguments.completionObj.subcommands) { - for (const item of Object.values(parsedArguments.completionObj.subcommands)) { - const suggestionLabels = getLabel(item); - if (!suggestionLabels) { - continue; - } - for (const suggestionLabel of suggestionLabels) { - items.push(createCompletionItem(terminalContext.cursorPosition, prefix, { label: suggestionLabel }, item.description, undefined, kind)); - } - } - } - break; +export type SpecArg = Fig.Arg | Fig.Suggestion | Fig.Option | string; + +export function addSuggestionsFromParsedArguments(parsedArguments: ArgumentParserResult, prefix: string, terminalContext: any, items: vscode.TerminalCompletionItem[]) { + const addSuggestions = (specArgs: SpecArg[] | undefined, kind: vscode.TerminalCompletionItemKind) => { + if (!specArgs) { + return; + } + for (const item of specArgs) { + const suggestionLabels = getLabel(item); + if (!suggestionLabels) { + continue; + } + for (const label of suggestionLabels) { + items.push( + createCompletionItem( + terminalContext.cursorPosition, + prefix, + { label }, + typeof item === 'string' ? item : item.description, + undefined, + kind + ) + ); } } }; @@ -286,18 +267,18 @@ function addSuggestionsFromParsedArguments(parsedArguments: ArgumentParserResult case SuggestionFlag.None: break; case SuggestionFlag.Args: - addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Argument); + addSuggestions(parsedArguments.currentArg?.suggestions, vscode.TerminalCompletionItemKind.Argument); break; case SuggestionFlag.Subcommands: - addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Method); + addSuggestions(Object.values(parsedArguments.completionObj.subcommands), vscode.TerminalCompletionItemKind.Method); break; case SuggestionFlag.Options: - addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Flag); + addSuggestions(Object.values(parsedArguments.completionObj.options), vscode.TerminalCompletionItemKind.Flag); break; case SuggestionFlag.Any: - addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Method); - addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Flag); - addSuggestions(parsedArguments, vscode.TerminalCompletionItemKind.Argument); + addSuggestions(Object.values(parsedArguments.completionObj.subcommands), vscode.TerminalCompletionItemKind.Method); + addSuggestions(Object.values(parsedArguments.completionObj.options), vscode.TerminalCompletionItemKind.Flag); + addSuggestions(parsedArguments.currentArg?.suggestions, vscode.TerminalCompletionItemKind.Argument); break; } } From 1725186fe45d709f7421b2ca7f8eb8cb46411edd Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 6 Feb 2025 16:30:32 -0600 Subject: [PATCH 19/51] add git for testing purposes --- .../src/completions/index.d.ts | 2486 ++--- .../src/completions/upstream/git.ts | 9813 +++++++++++++++++ .../src/terminalSuggestMain.ts | 3 + 3 files changed, 11059 insertions(+), 1243 deletions(-) create mode 100644 extensions/terminal-suggest/src/completions/upstream/git.ts diff --git a/extensions/terminal-suggest/src/completions/index.d.ts b/extensions/terminal-suggest/src/completions/index.d.ts index de76233ecb475..52aac4f42e9ce 100644 --- a/extensions/terminal-suggest/src/completions/index.d.ts +++ b/extensions/terminal-suggest/src/completions/index.d.ts @@ -1,1300 +1,1300 @@ /* eslint-disable @typescript-eslint/ban-types */ declare namespace Fig { - /** - * Templates are generators prebuilt by Fig. - * @remarks - * Here are the three templates: - * - filepaths: show folders and filepaths. Allow autoexecute on filepaths - * - folders: show folders only. Allow autoexecute on folders - * - history: show suggestions for all items in history matching this pattern - * - help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand - */ - type TemplateStrings = "filepaths" | "folders" | "history" | "help"; + /** + * Templates are generators prebuilt by Fig. + * @remarks + * Here are the three templates: + * - filepaths: show folders and filepaths. Allow autoexecute on filepaths + * - folders: show folders only. Allow autoexecute on folders + * - history: show suggestions for all items in history matching this pattern + * - help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand + */ + type TemplateStrings = "filepaths" | "folders" | "history" | "help"; - /** - * A template which is a single TemplateString or an array of TemplateStrings - * - * @remarks - * Templates are generators prebuilt by Fig. Here are the three templates: - * - filepaths: show folders and filepaths. Allow autoexecute on filepaths - * - folders: show folders only. Allow autoexecute on folders - * - history: show suggestions for all items in history matching this pattern - * - help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand - * - * @example - * `cd` uses the "folders" template - * `ls` used ["filepaths", "folders"]. Why both? Because if I `ls` a directory, we want to enable a user to autoexecute on this directory. If we just did "filepaths" they couldn't autoexecute. - * - */ - type Template = TemplateStrings | TemplateStrings[]; + /** + * A template which is a single TemplateString or an array of TemplateStrings + * + * @remarks + * Templates are generators prebuilt by Fig. Here are the three templates: + * - filepaths: show folders and filepaths. Allow autoexecute on filepaths + * - folders: show folders only. Allow autoexecute on folders + * - history: show suggestions for all items in history matching this pattern + * - help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand + * + * @example + * `cd` uses the "folders" template + * `ls` used ["filepaths", "folders"]. Why both? Because if I `ls` a directory, we want to enable a user to autoexecute on this directory. If we just did "filepaths" they couldn't autoexecute. + * + */ + type Template = TemplateStrings | TemplateStrings[]; - type HistoryContext = { - currentWorkingDirectory: string; - time: number; - exitCode: number; - shell: string; - }; + type HistoryContext = { + currentWorkingDirectory: string; + time: number; + exitCode: number; + shell: string; + }; - type TemplateSuggestionContext = - | { templateType: "filepaths" } - | { templateType: "folders" } - | { templateType: "help" } - | ({ templateType: "history" } & Partial); + type TemplateSuggestionContext = + | { templateType: "filepaths" } + | { templateType: "folders" } + | { templateType: "help" } + | ({ templateType: "history" } & Partial); - type TemplateSuggestion = Modify< - Suggestion, - { name?: string; context: TemplateSuggestionContext } - >; + type TemplateSuggestion = Modify< + Suggestion, + { name?: string; context: TemplateSuggestionContext } + >; - /** - * - * The SpecLocation object defines well... the location of the completion spec we want to load. - * Specs can be "global" (ie hosted by Fig's cloud) or "local" (ie stored on your local machine) - * - * @remarks - * **The `SpecLocation` Object** - * - * The SpecLocation object defines well... the location of the completion spec we want to load. - * Specs can be "global" (ie hosted by Fig's cloud) or "local" (ie stored on your local machine). - * - * - Global `SpecLocation`: - * Load specs hosted in Fig's Cloud. Assume the current working directory is here: https://github.com/withfig/autocomplete/tree/master/src. Now set the value for the "name" prop to the relative location of your spec (without the .js file extension) - * ```js - * // e.g. - * { type: "global", name: "aws/s3" } // Loads up the aws s3 completion spec - * { type: "global", name: "python/http.server" } // Loads up the http.server completion spec - * ``` - * - * - Local `SpecLocation`: - * Load specs saved on your local system / machine. Assume the current working directory is the user's current working directory. - * The `name` prop should take the name of the spec (without the .js file extension) e.g. my_cli_tool - * The `path` prop should take an absolute path OR a relative path (relative to the user's current working directory). The path should be to the directory that contains the `.fig` folder. Fig will then assume your spec is located in `.fig/autocomplete/build/` - * ```js - * // e.g. - * { type: "global", path: "node_modules/cowsay", name: "cowsay_cli" } // will look for `cwd/node_modules/cowsay/.fig/autocomplete/build/cowsay_cli.js` - * { type: "global", path: "~", name: "my_cli" } // will look for `~/.fig/autocomplete/build/my_cli.js` - * ``` - * @irreplaceable - */ - type SpecLocation = - | { type: "local"; path?: string; name: string } - | { type: "global"; name: string }; + /** + * + * The SpecLocation object defines well... the location of the completion spec we want to load. + * Specs can be "global" (ie hosted by Fig's cloud) or "local" (ie stored on your local machine) + * + * @remarks + * **The `SpecLocation` Object** + * + * The SpecLocation object defines well... the location of the completion spec we want to load. + * Specs can be "global" (ie hosted by Fig's cloud) or "local" (ie stored on your local machine). + * + * - Global `SpecLocation`: + * Load specs hosted in Fig's Cloud. Assume the current working directory is here: https://github.com/withfig/autocomplete/tree/master/src. Now set the value for the "name" prop to the relative location of your spec (without the .js file extension) + * ```js + * // e.g. + * { type: "global", name: "aws/s3" } // Loads up the aws s3 completion spec + * { type: "global", name: "python/http.server" } // Loads up the http.server completion spec + * ``` + * + * - Local `SpecLocation`: + * Load specs saved on your local system / machine. Assume the current working directory is the user's current working directory. + * The `name` prop should take the name of the spec (without the .js file extension) e.g. my_cli_tool + * The `path` prop should take an absolute path OR a relative path (relative to the user's current working directory). The path should be to the directory that contains the `.fig` folder. Fig will then assume your spec is located in `.fig/autocomplete/build/` + * ```js + * // e.g. + * { type: "global", path: "node_modules/cowsay", name: "cowsay_cli" } // will look for `cwd/node_modules/cowsay/.fig/autocomplete/build/cowsay_cli.js` + * { type: "global", path: "~", name: "my_cli" } // will look for `~/.fig/autocomplete/build/my_cli.js` + * ``` + * @irreplaceable + */ + type SpecLocation = + | { type: "local"; path?: string; name: string } + | { type: "global"; name: string }; - /** - * Dynamically load up another completion spec at runtime. - * - * See [`loadSpec` property in Subcommand Object](https://fig.io/docs/reference/subcommand#loadspec). - */ - type LoadSpec = - | string - | Subcommand - | (( - token: string, - executeCommand: ExecuteCommandFunction - ) => Promise); + /** + * Dynamically load up another completion spec at runtime. + * + * See [`loadSpec` property in Subcommand Object](https://fig.io/docs/reference/subcommand#loadspec). + */ + type LoadSpec = + | string + | Subcommand + | (( + token: string, + executeCommand: ExecuteCommandFunction + ) => Promise); - /** - * The type of a suggestion object. - * @remarks - * The type determines: - * - the default icon Fig uses (e.g. a file or folder searches for the system icon, a subcommand has a specific icon etc) - * - whether we allow users to auto-execute a command - */ - type SuggestionType = - | "folder" - | "file" - | "arg" - | "subcommand" - | "option" - | "special" - | "mixin" - | "shortcut"; + /** + * The type of a suggestion object. + * @remarks + * The type determines: + * - the default icon Fig uses (e.g. a file or folder searches for the system icon, a subcommand has a specific icon etc) + * - whether we allow users to auto-execute a command + */ + type SuggestionType = + | "folder" + | "file" + | "arg" + | "subcommand" + | "option" + | "special" + | "mixin" + | "shortcut"; - /** - * A single object of type `T` or an array of objects of type `T`. - */ - type SingleOrArray = T | T[]; + /** + * A single object of type `T` or an array of objects of type `T`. + */ + type SingleOrArray = T | T[]; - /** - * An async function that returns the version of a given CLI tool. - * @remarks - * This is used in completion specs that want to version themselves the same way CLI tools are versioned. See fig.io/docs - * - * @param executeCommand -an async function that allows you to execute a shell command on the user's system and get the output as a string. - * @returns The version of a CLI tool - * - * @example - * `1.0.22` - * - * @example - * `v26` - * - */ - type GetVersionCommand = (executeCommand: ExecuteCommandFunction) => Promise; + /** + * An async function that returns the version of a given CLI tool. + * @remarks + * This is used in completion specs that want to version themselves the same way CLI tools are versioned. See fig.io/docs + * + * @param executeCommand -an async function that allows you to execute a shell command on the user's system and get the output as a string. + * @returns The version of a CLI tool + * + * @example + * `1.0.22` + * + * @example + * `v26` + * + */ + type GetVersionCommand = (executeCommand: ExecuteCommandFunction) => Promise; - /** - * Context about a current shell session. - */ - type ShellContext = { - /** - * The current directory the shell is in - */ - currentWorkingDirectory: string; - /** - * Exported environment variables from the shell - */ - environmentVariables: Record; - /** - * The name of the current process - */ - currentProcess: string; - /** - * @hidden - * @deprecated - */ - sshPrefix: string; - }; + /** + * Context about a current shell session. + */ + type ShellContext = { + /** + * The current directory the shell is in + */ + currentWorkingDirectory: string; + /** + * Exported environment variables from the shell + */ + environmentVariables: Record; + /** + * The name of the current process + */ + currentProcess: string; + /** + * @hidden + * @deprecated + */ + sshPrefix: string; + }; - type GeneratorContext = ShellContext & { - isDangerous?: boolean; - searchTerm: string; - }; + type GeneratorContext = ShellContext & { + isDangerous?: boolean; + searchTerm: string; + }; - /** - * A function which can have a `T` argument and a `R` result. - * @param param - A param of type `R` - * @returns Something of type `R` - */ - type Function = (param: T) => R; + /** + * A function which can have a `T` argument and a `R` result. + * @param param - A param of type `R` + * @returns Something of type `R` + */ + type Function = (param: T) => R; - /** - * A utility type to modify a property type - * @irreplaceable - */ - type Modify = Omit & R; + /** + * A utility type to modify a property type + * @irreplaceable + */ + type Modify = Omit & R; - /** - * A `string` OR a `function` which can have a `T` argument and a `R` result. - * @param param - A param of type `R` - * @returns Something of type `R` - */ - type StringOrFunction = string | Function; + /** + * A `string` OR a `function` which can have a `T` argument and a `R` result. + * @param param - A param of type `R` + * @returns Something of type `R` + */ + type StringOrFunction = string | Function; - /** - * @excluded - * @irreplaceable - */ - type ArgDiff = Modify; + /** + * @excluded + * @irreplaceable + */ + type ArgDiff = Modify; - /** - * @excluded - * @irreplaceable - */ - type OptionDiff = Modify< - Fig.Option, - { - args?: ArgDiff | ArgDiff[]; - remove?: true; - } - >; + /** + * @excluded + * @irreplaceable + */ + type OptionDiff = Modify< + Fig.Option, + { + args?: ArgDiff | ArgDiff[]; + remove?: true; + } + >; - /** - * @excluded - * @irreplaceable - */ - type SubcommandDiff = Modify< - Fig.Subcommand, - { - subcommands?: SubcommandDiff[]; - options?: OptionDiff[]; - args?: ArgDiff | ArgDiff[]; - remove?: true; - } - >; + /** + * @excluded + * @irreplaceable + */ + type SubcommandDiff = Modify< + Fig.Subcommand, + { + subcommands?: SubcommandDiff[]; + options?: OptionDiff[]; + args?: ArgDiff | ArgDiff[]; + remove?: true; + } + >; - /** - * @excluded - * @irreplaceable - */ - type SpecDiff = Omit; + /** + * @excluded + * @irreplaceable + */ + type SpecDiff = Omit; - /** - * @excluded - * @irreplaceable - */ - type VersionDiffMap = Record; + /** + * @excluded + * @irreplaceable + */ + type VersionDiffMap = Record; - /** - * A spec object. - * Can be one of - * 1. A subcommand - * 2. A function that dynamically computes a subcommand - * 3. A function that returns the path to a versioned spec files (that exports a base subcommand and { versions: VersionDiffMap } - */ - type Spec = - | Subcommand - | ((version?: string) => Subcommand) - | ((version?: string) => { - versionedSpecPath: string; - version?: string; - }); + /** + * A spec object. + * Can be one of + * 1. A subcommand + * 2. A function that dynamically computes a subcommand + * 3. A function that returns the path to a versioned spec files (that exports a base subcommand and { versions: VersionDiffMap } + */ + type Spec = + | Subcommand + | ((version?: string) => Subcommand) + | ((version?: string) => { + versionedSpecPath: string; + version?: string; + }); - type ExecuteCommandInput = { - /** - * The command to execute - */ - command: string; - /** - * The arguments to the command to be run - */ - args: string[]; - /** - * The directory to run the command in - */ - cwd?: string; - /** - * The environment variables to set when executing the command, `undefined` will unset the variable if it set - */ - env?: Record; - /** - * Duration of timeout in milliseconds, if the command takes longer than the timeout a error will be thrown. - * @defaultValue 5000 - */ - timeout?: number; - }; + type ExecuteCommandInput = { + /** + * The command to execute + */ + command: string; + /** + * The arguments to the command to be run + */ + args: string[]; + /** + * The directory to run the command in + */ + cwd?: string; + /** + * The environment variables to set when executing the command, `undefined` will unset the variable if it set + */ + env?: Record; + /** + * Duration of timeout in milliseconds, if the command takes longer than the timeout a error will be thrown. + * @defaultValue 5000 + */ + timeout?: number; + }; - /** - * The output of running a command - */ - type ExecuteCommandOutput = { - /** - * The stdout (1) of running a command - */ - stdout: string; - /** - * The stderr (2) of running a command - */ - stderr: string; - /** - * The exit status of running a command - */ - status: number; - }; + /** + * The output of running a command + */ + type ExecuteCommandOutput = { + /** + * The stdout (1) of running a command + */ + stdout: string; + /** + * The stderr (2) of running a command + */ + stderr: string; + /** + * The exit status of running a command + */ + status: number; + }; - /** - * An async function to execute a command - * @returns The output of the command - */ - type ExecuteCommandFunction = (args: ExecuteCommandInput) => Promise; + /** + * An async function to execute a command + * @returns The output of the command + */ + type ExecuteCommandFunction = (args: ExecuteCommandInput) => Promise; - type CacheMaxAge = { - strategy: "max-age"; - /** - * The time to live for the cache in milliseconds. - * @example - * 3600 - */ - ttl: number; - }; + type CacheMaxAge = { + strategy: "max-age"; + /** + * The time to live for the cache in milliseconds. + * @example + * 3600 + */ + ttl: number; + }; - type CacheStaleWhileRevalidate = { - strategy?: "stale-while-revalidate"; - /** - * The time to live for the cache in milliseconds. - * @example - * 3600 - */ - ttl?: number; - }; + type CacheStaleWhileRevalidate = { + strategy?: "stale-while-revalidate"; + /** + * The time to live for the cache in milliseconds. + * @example + * 3600 + */ + ttl?: number; + }; - type Cache = (CacheMaxAge | CacheStaleWhileRevalidate) & { - /** - * Whether the cache should be based on the directory the user was currently in or not. - * @defaultValue false - */ - cacheByDirectory?: boolean; + type Cache = (CacheMaxAge | CacheStaleWhileRevalidate) & { + /** + * Whether the cache should be based on the directory the user was currently in or not. + * @defaultValue false + */ + cacheByDirectory?: boolean; - /** - * Hardcoded cache key that can be used to cache a single generator across - * multiple argument locations in a spec. - */ - cacheKey?: string; - }; + /** + * Hardcoded cache key that can be used to cache a single generator across + * multiple argument locations in a spec. + */ + cacheKey?: string; + }; - type TriggerOnChange = { - /** Trigger on any change to the token */ - on: "change"; - }; + type TriggerOnChange = { + /** Trigger on any change to the token */ + on: "change"; + }; - type TriggerOnThreshold = { - /** Trigger when the length of the token changes past a threshold */ - on: "threshold"; - length: number; - }; + type TriggerOnThreshold = { + /** Trigger when the length of the token changes past a threshold */ + on: "threshold"; + length: number; + }; - type TriggerOnMatch = { - /** Trigger when the index of a string changes */ - on: "match"; - string: string | string[]; - }; + type TriggerOnMatch = { + /** Trigger when the index of a string changes */ + on: "match"; + string: string | string[]; + }; - type Trigger = - | string - | ((newToken: string, oldToken: string) => boolean) - | TriggerOnChange - | TriggerOnThreshold - | TriggerOnMatch; + type Trigger = + | string + | ((newToken: string, oldToken: string) => boolean) + | TriggerOnChange + | TriggerOnThreshold + | TriggerOnMatch; - /** - * The BaseSuggestion object is the root of the Suggestion, Subcommand, and Option objects. - * It is where key properties like description, icon, and displayName are found - * @excluded - */ - interface BaseSuggestion { - /** - * The string that is displayed in the UI for a given suggestion. - * @defaultValue the name prop - * - * @example - * The npm CLI has a subcommand called `install`. If we wanted - * to display some custom text like `Install an NPM package 📦` we would set - * `name: "install"` and `displayName: "Install an NPM package 📦"` - */ - displayName?: string; - /** - * The value that's inserted into the terminal when a user presses enter/tab or clicks on a menu item. - * - * @remarks - * You can use `\n` to insert a newline or `\b` to insert a backspace. - * You can also optionally specify {cursor} in the string and Fig will automatically place the cursor there after insert. - * - * @defaultValue The value of the name prop. - * - * @example - * For the `git commit` subcommand, the `-m` option has an insert value of `-m '{cursor}'` - */ - insertValue?: string; - /** - * When the suggestion is inserted, replace the command with this string - * - * @remarks - * You can use `\n` to insert a newline or `\b` to insert a backspace. - * You can also optionally specify {cursor} in the string and Fig will automatically place the cursor there after insert. - * Note that currently the entire edit buffer will be replaced. Eventually, only the root command will be replaced, preserving pipes and continuations. - */ - replaceValue?: string; - /** - * The text that gets rendered at the bottom of the autocomplete box (or the side if you hit ⌘i) - * - * @example - * "Your commit message" - */ - description?: string; - /** - * The icon that is rendered is based on the type. - * - * @remarks - * Icons can be a 1 character string, a URL, or Fig's [icon protocol](https://fig.io/docs/reference/suggestion/icon-api) (fig://) which lets you generate - * colorful and fun systems icons. - * - * @defaultValue related to the type of the object (e.g. `Suggestion`, `Subcommand`, `Option`, `Arg`) - * - * @example - * `A` - * @example - * `😊` - * @example - * `https://www.herokucdn.com/favicon.ico` - * @example - * `fig://icon?type=file` - * - */ - icon?: string; - /** - * Specifies whether the suggestion is "dangerous". - * - * @remarks - * If true, Fig will not enable its autoexecute functionality. Autoexecute means if a user selects a suggestion it will insert the text and run the command. We signal this by changing the icon to red. - * Setting `isDangerous` to `true` will make it harder for a user to accidentally run a dangerous command. - * - * @defaultValue false - * - * @example - * This is used in the `rm` spec. Why? Because we don't want users to accidentally delete their files so we make it just a little bit harder... - */ - isDangerous?: boolean; - /** - * The number used to rank suggestions in autocomplete. Number must be from 0-100. Higher priorities rank higher. - * - * @defaultValue 50 - * @remarks - * Fig ranks suggestions by recency. To do this, we check if a suggestion has been selected before. If yes and the suggestions has: - * - a priority between 50-75, the priority will be replaced with 75, then we will add the timestamp of when that suggestion was selected as a decimal. - * - a priority outside of 50-75, the priority will be increased by the timestamp of when that suggestion was selected as a decimal. - * If it has not been selected before, Fig will keep the same priority as was set in the completion spec - * If it was not set in the spec, it will default to 50. - * - * @example - * Let's say a user has previously selected a suggestion at unix timestamp 1634087677: - * - If completion spec did not set a priority (Fig treats this as priority 50), its priority would change to 75 + 0.1634087677 = 75.1634087677; - * - If completion spec set a priority of 49 or less, its priority would change to 49 + 0.1634087677 = 49.1634087677; - * - If completion spec set a priority of 76 or more, its priority would change to 76 + 0.1634087677 = 76.1634087677; - * - If a user had never selected a suggestion, then its priority would just stay as is (or if not set, default to 50). - * - * @example - * If you want your suggestions to always be: - * - at the top order, rank them 76 or above. - * - at the bottom, rank them 49 or below - */ - priority?: number; - /** - * Specifies whether a suggestion should be hidden from results. - * @remarks - * Fig will only show it if the user exactly types the name. - * @defaultValue false - * @example - * The "-" suggestion is hidden in the `cd` spec. You will only see it if you type exactly `cd -` - */ - hidden?: boolean; - /** - * - * Specifies whether a suggestion is deprecated. - * @remarks - * It is possible to specify a suggestion to replace the deprecated one. - * - The `description` of the deprecated object (e.g `deprecated: { description: 'The --no-ansi option has been deprecated in v2' }`) is used to provide infos about the deprecation. - * - `deprecated: true` and `deprecated: { }` behave the same and will just display the suggestion as deprecated. - * @example - * ```js - * deprecated: { insertValue: '--ansi never', description: 'The --no-ansi option has been deprecated in v2' } - * ``` - */ - deprecated?: boolean | Omit; + /** + * The BaseSuggestion object is the root of the Suggestion, Subcommand, and Option objects. + * It is where key properties like description, icon, and displayName are found + * @excluded + */ + interface BaseSuggestion { + /** + * The string that is displayed in the UI for a given suggestion. + * @defaultValue the name prop + * + * @example + * The npm CLI has a subcommand called `install`. If we wanted + * to display some custom text like `Install an NPM package 📦` we would set + * `name: "install"` and `displayName: "Install an NPM package 📦"` + */ + displayName?: string; + /** + * The value that's inserted into the terminal when a user presses enter/tab or clicks on a menu item. + * + * @remarks + * You can use `\n` to insert a newline or `\b` to insert a backspace. + * You can also optionally specify {cursor} in the string and Fig will automatically place the cursor there after insert. + * + * @defaultValue The value of the name prop. + * + * @example + * For the `git commit` subcommand, the `-m` option has an insert value of `-m '{cursor}'` + */ + insertValue?: string; + /** + * When the suggestion is inserted, replace the command with this string + * + * @remarks + * You can use `\n` to insert a newline or `\b` to insert a backspace. + * You can also optionally specify {cursor} in the string and Fig will automatically place the cursor there after insert. + * Note that currently the entire edit buffer will be replaced. Eventually, only the root command will be replaced, preserving pipes and continuations. + */ + replaceValue?: string; + /** + * The text that gets rendered at the bottom of the autocomplete box (or the side if you hit ⌘i) + * + * @example + * "Your commit message" + */ + description?: string; + /** + * The icon that is rendered is based on the type. + * + * @remarks + * Icons can be a 1 character string, a URL, or Fig's [icon protocol](https://fig.io/docs/reference/suggestion/icon-api) (fig://) which lets you generate + * colorful and fun systems icons. + * + * @defaultValue related to the type of the object (e.g. `Suggestion`, `Subcommand`, `Option`, `Arg`) + * + * @example + * `A` + * @example + * `😊` + * @example + * `https://www.herokucdn.com/favicon.ico` + * @example + * `fig://icon?type=file` + * + */ + icon?: string; + /** + * Specifies whether the suggestion is "dangerous". + * + * @remarks + * If true, Fig will not enable its autoexecute functionality. Autoexecute means if a user selects a suggestion it will insert the text and run the command. We signal this by changing the icon to red. + * Setting `isDangerous` to `true` will make it harder for a user to accidentally run a dangerous command. + * + * @defaultValue false + * + * @example + * This is used in the `rm` spec. Why? Because we don't want users to accidentally delete their files so we make it just a little bit harder... + */ + isDangerous?: boolean; + /** + * The number used to rank suggestions in autocomplete. Number must be from 0-100. Higher priorities rank higher. + * + * @defaultValue 50 + * @remarks + * Fig ranks suggestions by recency. To do this, we check if a suggestion has been selected before. If yes and the suggestions has: + * - a priority between 50-75, the priority will be replaced with 75, then we will add the timestamp of when that suggestion was selected as a decimal. + * - a priority outside of 50-75, the priority will be increased by the timestamp of when that suggestion was selected as a decimal. + * If it has not been selected before, Fig will keep the same priority as was set in the completion spec + * If it was not set in the spec, it will default to 50. + * + * @example + * Let's say a user has previously selected a suggestion at unix timestamp 1634087677: + * - If completion spec did not set a priority (Fig treats this as priority 50), its priority would change to 75 + 0.1634087677 = 75.1634087677; + * - If completion spec set a priority of 49 or less, its priority would change to 49 + 0.1634087677 = 49.1634087677; + * - If completion spec set a priority of 76 or more, its priority would change to 76 + 0.1634087677 = 76.1634087677; + * - If a user had never selected a suggestion, then its priority would just stay as is (or if not set, default to 50). + * + * @example + * If you want your suggestions to always be: + * - at the top order, rank them 76 or above. + * - at the bottom, rank them 49 or below + */ + priority?: number; + /** + * Specifies whether a suggestion should be hidden from results. + * @remarks + * Fig will only show it if the user exactly types the name. + * @defaultValue false + * @example + * The "-" suggestion is hidden in the `cd` spec. You will only see it if you type exactly `cd -` + */ + hidden?: boolean; + /** + * + * Specifies whether a suggestion is deprecated. + * @remarks + * It is possible to specify a suggestion to replace the deprecated one. + * - The `description` of the deprecated object (e.g `deprecated: { description: 'The --no-ansi option has been deprecated in v2' }`) is used to provide infos about the deprecation. + * - `deprecated: true` and `deprecated: { }` behave the same and will just display the suggestion as deprecated. + * @example + * ```js + * deprecated: { insertValue: '--ansi never', description: 'The --no-ansi option has been deprecated in v2' } + * ``` + */ + deprecated?: boolean | Omit; - /** - * Specifies which component to use to render the preview window. - * - * @remarks This should be the path within the `src` directory to the component without the extension. - * - * @example 'ls/filepathPreview' - */ - previewComponent?: string; + /** + * Specifies which component to use to render the preview window. + * + * @remarks This should be the path within the `src` directory to the component without the extension. + * + * @example 'ls/filepathPreview' + */ + previewComponent?: string; - /** - * This is a way to pass data to the Autocomplete Engine that is not formalized in the spec, do not use this in specs as it may change at any time - * - * @ignore - */ - _internal?: Record; - } + /** + * This is a way to pass data to the Autocomplete Engine that is not formalized in the spec, do not use this in specs as it may change at any time + * + * @ignore + */ + _internal?: Record; + } - /** - * Each item in Fig's autocomplete popup window is a Suggestion object. It is probably the most important object in Fig. - * Subcommand and Option objects compile down to Suggestion objects. Generators return Suggestion objects. - * The main things you can customize in your suggestion object is the text that's displayed, the icon, and what's inserted after being selected. In saying that, most of these have very sane defaults. - */ - interface Suggestion extends BaseSuggestion { - /** - * The string Fig uses when filtering over a list of suggestions to check for a match. - * @remarks - * When a a user is typing in the terminal, the query term (the token they are currently typing) filters over all suggestions in a list by checking if the queryTerm matches the prefix of the name. - * The `displayName` prop also defaults to the value of name. - * - * The `name` props of suggestion, subcommand, option, and arg objects are all different. It's important to read them all carefully. - * - * @example - * If a user types git `c`, any Suggestion objects with a name prop that has a value starting with "c" will match. - * - */ - name?: SingleOrArray; - /** - * The type of a suggestion object. - * @remarks - * The type determines - * - the default icon Fig uses (e.g. a file or folder searches for the system icon, a subcommand has a specific icon etc) - * - whether we allow users to auto-execute a command - */ - type?: SuggestionType; - } + /** + * Each item in Fig's autocomplete popup window is a Suggestion object. It is probably the most important object in Fig. + * Subcommand and Option objects compile down to Suggestion objects. Generators return Suggestion objects. + * The main things you can customize in your suggestion object is the text that's displayed, the icon, and what's inserted after being selected. In saying that, most of these have very sane defaults. + */ + interface Suggestion extends BaseSuggestion { + /** + * The string Fig uses when filtering over a list of suggestions to check for a match. + * @remarks + * When a a user is typing in the terminal, the query term (the token they are currently typing) filters over all suggestions in a list by checking if the queryTerm matches the prefix of the name. + * The `displayName` prop also defaults to the value of name. + * + * The `name` props of suggestion, subcommand, option, and arg objects are all different. It's important to read them all carefully. + * + * @example + * If a user types git `c`, any Suggestion objects with a name prop that has a value starting with "c" will match. + * + */ + name?: SingleOrArray; + /** + * The type of a suggestion object. + * @remarks + * The type determines + * - the default icon Fig uses (e.g. a file or folder searches for the system icon, a subcommand has a specific icon etc) + * - whether we allow users to auto-execute a command + */ + type?: SuggestionType; + } - /** - * The subcommand object represent the tree structure of a completion spec. We sometimes also call it the skeleton. - * - * A subcommand can nest options, arguments, and more subcommands (it's recursive) - */ - interface Subcommand extends BaseSuggestion { - /** - * The name of the subcommand. Should exactly match the name defined by the CLI tool. - * - * @remarks - * If a subcommand has multiple aliases, they should be included as an array. - * - * Note that Fig's autocomplete engine requires this `name` to match the text typed by the user in the shell. - * - * To customize the title that is displayed to the user, use `displayName`. - * - * - * @example - * For `git checkout`, the subcommand `checkout` would have `name: "checkout"` - * @example - * For `npm install`, the subcommand `install` would have `name: ["install", "i"]` as these two values both represent the same subcommand. - */ - name: SingleOrArray; + /** + * The subcommand object represent the tree structure of a completion spec. We sometimes also call it the skeleton. + * + * A subcommand can nest options, arguments, and more subcommands (it's recursive) + */ + interface Subcommand extends BaseSuggestion { + /** + * The name of the subcommand. Should exactly match the name defined by the CLI tool. + * + * @remarks + * If a subcommand has multiple aliases, they should be included as an array. + * + * Note that Fig's autocomplete engine requires this `name` to match the text typed by the user in the shell. + * + * To customize the title that is displayed to the user, use `displayName`. + * + * + * @example + * For `git checkout`, the subcommand `checkout` would have `name: "checkout"` + * @example + * For `npm install`, the subcommand `install` would have `name: ["install", "i"]` as these two values both represent the same subcommand. + */ + name: SingleOrArray; - /** - * An array of `Subcommand` objects representing all the subcommands that exist beneath the current command. - * * - * To support large CLI tools, `Subcommands` can be nested recursively. - * - * @example - * A CLI tool like `aws` is composed of many top-level subcommands (`s3`, `ec2`, `eks`...), each of which include child subcommands of their own. - */ - subcommands?: Subcommand[]; + /** + * An array of `Subcommand` objects representing all the subcommands that exist beneath the current command. + * * + * To support large CLI tools, `Subcommands` can be nested recursively. + * + * @example + * A CLI tool like `aws` is composed of many top-level subcommands (`s3`, `ec2`, `eks`...), each of which include child subcommands of their own. + */ + subcommands?: Subcommand[]; - /** - * Specifies whether the command requires a subcommand. This is false by default. - * - * A space will always be inserted after this command if `requiresSubcommand` is true. - * If the property is omitted, a space will be inserted if there is at least one required argument. - */ - requiresSubcommand?: boolean; + /** + * Specifies whether the command requires a subcommand. This is false by default. + * + * A space will always be inserted after this command if `requiresSubcommand` is true. + * If the property is omitted, a space will be inserted if there is at least one required argument. + */ + requiresSubcommand?: boolean; - /** - * An array of `Option` objects representing the options that are available on this subcommand. - * - * @example - * A command like `git commit` accepts various flags and options, such as `--message` and `--all`. These `Option` objects would be included in the `options` field. - */ - options?: Option[]; + /** + * An array of `Option` objects representing the options that are available on this subcommand. + * + * @example + * A command like `git commit` accepts various flags and options, such as `--message` and `--all`. These `Option` objects would be included in the `options` field. + */ + options?: Option[]; - /** - * An array of `Arg` objects representing the various parameters or "arguments" that can be passed to this subcommand. - * - */ - args?: SingleOrArray; - /** - * This option allows to enforce the suggestion filtering strategy for a specific subcommand. - * @remarks - * Users always want to have the most accurate results at the top of the suggestions list. - * For example we can enable fuzzy search on a subcommand that always requires fuzzy search to show the best suggestions. - * This property is also useful when subcommands or options have a prefix (e.g. the npm package scope) because enabling fuzzy search users can omit that part (see the second example below) - * @example - * yarn workspace [name] with fuzzy search is way more useful since we can omit the npm package scope - * @example - * fig settings uses fuzzy search to prevent having to add the `autocomplete.` prefix to each searched setting - * ```typescript - * const figSpec: Fig.Spec { - * name: "fig", - * subcommands: [ - * { - * name: "settings", - * filterStrategy: "fuzzy", - * subcommands: [ - * { - * name: "autocomplete.theme", // if a user writes `fig settings theme` it gets the correct suggestions - * }, - * // ... other settings - * ] - * }, - * // ... other fig subcommands - * ] - * } - * ``` - */ - filterStrategy?: "fuzzy" | "prefix" | "default"; - /** - * A list of Suggestion objects that are appended to the suggestions shown beneath a subcommand. - * - * @remarks - * You can use this field to suggest common workflows. - * - */ - additionalSuggestions?: (string | Suggestion)[]; - /** - * Dynamically load another completion spec at runtime. - * - * @param tokens - a tokenized array of the text the user has typed in the shell. - * @param executeCommand - an async function that can execute a shell command on behalf of the user. The output is a string. - * @returns A `SpecLocation` object or an array of `SpecLocation` objects. - * - * @remarks - * `loadSpec` can be invoked as string (recommended) or a function (advanced). - * - * The API tells the autocomplete engine where to look for a completion spec. If you pass a string, the engine will attempt to locate a matching spec that is hosted by Fig. - * - * @example - * Suppose you have an internal CLI tool that wraps `kubectl`. Instead of copying the `kubectl` completion spec, you can include the spec at runtime. - * ```typescript - * { - * name: "kube", - * description: "a wrapper around kubectl" - * loadSpec: "kubectl" - * } - * ``` - * @example - * In the `aws` completion spec, `loadSpec` is used to optimize performance. The completion spec is split into multiple files, each of which can be loaded separately. - * ```typescript - * { - * name: "s3", - * loadSpec: "aws/s3" - * } - * ``` - */ - loadSpec?: LoadSpec; - /** - * Dynamically *generate* a `Subcommand` object a runtime. The generated `Subcommand` is merged with the current subcommand. - * - * @remarks - * This API is often used by CLI tools where the structure of the CLI tool is not *static*. For instance, if the tool can be extended by plugins or otherwise shows different subcommands or options depending on the environment. - * - * @param tokens - a tokenized array of the text the user has typed in the shell. - * @param executeCommand - an async function that can execute a shell command on behalf of the user. The output is a string. - * @returns a `Fig.Spec` object - * - * @example - * The `python` spec uses `generateSpec` to include the`django-admin` spec if `django manage.py` exists. - * ```typescript - * generateSpec: async (tokens, executeCommand) => { - * // Load the contents of manage.py - * const managePyContents = await executeCommand("cat manage.py"); - * // Heuristic to determine if project uses django - * if (managePyContents.contains("django")) { - * return { - * name: "python", - * subcommands: [{ name: "manage.py", loadSpec: "django-admin" }], - * }; - * } - * }, - * ``` - */ - generateSpec?: (tokens: string[], executeCommand: ExecuteCommandFunction) => Promise; + /** + * An array of `Arg` objects representing the various parameters or "arguments" that can be passed to this subcommand. + * + */ + args?: SingleOrArray; + /** + * This option allows to enforce the suggestion filtering strategy for a specific subcommand. + * @remarks + * Users always want to have the most accurate results at the top of the suggestions list. + * For example we can enable fuzzy search on a subcommand that always requires fuzzy search to show the best suggestions. + * This property is also useful when subcommands or options have a prefix (e.g. the npm package scope) because enabling fuzzy search users can omit that part (see the second example below) + * @example + * yarn workspace [name] with fuzzy search is way more useful since we can omit the npm package scope + * @example + * fig settings uses fuzzy search to prevent having to add the `autocomplete.` prefix to each searched setting + * ```typescript + * const figSpec: Fig.Spec { + * name: "fig", + * subcommands: [ + * { + * name: "settings", + * filterStrategy: "fuzzy", + * subcommands: [ + * { + * name: "autocomplete.theme", // if a user writes `fig settings theme` it gets the correct suggestions + * }, + * // ... other settings + * ] + * }, + * // ... other fig subcommands + * ] + * } + * ``` + */ + filterStrategy?: "fuzzy" | "prefix" | "default"; + /** + * A list of Suggestion objects that are appended to the suggestions shown beneath a subcommand. + * + * @remarks + * You can use this field to suggest common workflows. + * + */ + additionalSuggestions?: (string | Suggestion)[]; + /** + * Dynamically load another completion spec at runtime. + * + * @param tokens - a tokenized array of the text the user has typed in the shell. + * @param executeCommand - an async function that can execute a shell command on behalf of the user. The output is a string. + * @returns A `SpecLocation` object or an array of `SpecLocation` objects. + * + * @remarks + * `loadSpec` can be invoked as string (recommended) or a function (advanced). + * + * The API tells the autocomplete engine where to look for a completion spec. If you pass a string, the engine will attempt to locate a matching spec that is hosted by Fig. + * + * @example + * Suppose you have an internal CLI tool that wraps `kubectl`. Instead of copying the `kubectl` completion spec, you can include the spec at runtime. + * ```typescript + * { + * name: "kube", + * description: "a wrapper around kubectl" + * loadSpec: "kubectl" + * } + * ``` + * @example + * In the `aws` completion spec, `loadSpec` is used to optimize performance. The completion spec is split into multiple files, each of which can be loaded separately. + * ```typescript + * { + * name: "s3", + * loadSpec: "aws/s3" + * } + * ``` + */ + loadSpec?: LoadSpec; + /** + * Dynamically *generate* a `Subcommand` object a runtime. The generated `Subcommand` is merged with the current subcommand. + * + * @remarks + * This API is often used by CLI tools where the structure of the CLI tool is not *static*. For instance, if the tool can be extended by plugins or otherwise shows different subcommands or options depending on the environment. + * + * @param tokens - a tokenized array of the text the user has typed in the shell. + * @param executeCommand - an async function that can execute a shell command on behalf of the user. The output is a string. + * @returns a `Fig.Spec` object + * + * @example + * The `python` spec uses `generateSpec` to include the`django-admin` spec if `django manage.py` exists. + * ```typescript + * generateSpec: async (tokens, executeCommand) => { + * // Load the contents of manage.py + * const managePyContents = await executeCommand("cat manage.py"); + * // Heuristic to determine if project uses django + * if (managePyContents.contains("django")) { + * return { + * name: "python", + * subcommands: [{ name: "manage.py", loadSpec: "django-admin" }], + * }; + * } + * }, + * ``` + */ + generateSpec?: (tokens: string[], executeCommand: ExecuteCommandFunction) => Promise; - /** - * Generating a spec can be expensive, but due to current guarantees they are not cached. - * This function generates a cache key which is used to cache the result of generateSpec. - * If `undefined` is returned, the cache will not be used. - */ - generateSpecCacheKey?: Function<{ tokens: string[] }, string | undefined> | string; + /** + * Generating a spec can be expensive, but due to current guarantees they are not cached. + * This function generates a cache key which is used to cache the result of generateSpec. + * If `undefined` is returned, the cache will not be used. + */ + generateSpecCacheKey?: Function<{ tokens: string[] }, string | undefined> | string; - /** - * Configure how the autocomplete engine will map the raw tokens to a given completion spec. - * - * @param flagsArePosixNoncompliant - Indicates that flags with one hyphen may have *more* than one character. Enabling this directive, turns off support for option chaining. - * @param optionsMustPrecedeArguments - Options will not be suggested after any argument of the Subcommand has been typed. - * @param optionArgSeparators - Indicate that options which take arguments will require one of the specified separators between the 'verbose' option name and the argument. - * - * @example - * The `-work` option from the `go` spec is parsed as a single flag when `parserDirectives.flagsArePosixNoncompliant` is set to true. Normally, this would be chained and parsed as `-w -o -r -k` if `flagsArePosixNoncompliant` is not set to true. - */ - parserDirectives?: { - flagsArePosixNoncompliant?: boolean; - optionsMustPrecedeArguments?: boolean; - optionArgSeparators?: SingleOrArray; - }; + /** + * Configure how the autocomplete engine will map the raw tokens to a given completion spec. + * + * @param flagsArePosixNoncompliant - Indicates that flags with one hyphen may have *more* than one character. Enabling this directive, turns off support for option chaining. + * @param optionsMustPrecedeArguments - Options will not be suggested after any argument of the Subcommand has been typed. + * @param optionArgSeparators - Indicate that options which take arguments will require one of the specified separators between the 'verbose' option name and the argument. + * + * @example + * The `-work` option from the `go` spec is parsed as a single flag when `parserDirectives.flagsArePosixNoncompliant` is set to true. Normally, this would be chained and parsed as `-w -o -r -k` if `flagsArePosixNoncompliant` is not set to true. + */ + parserDirectives?: { + flagsArePosixNoncompliant?: boolean; + optionsMustPrecedeArguments?: boolean; + optionArgSeparators?: SingleOrArray; + }; - /** - * Specifies whether or not to cache the result of loadSpec and generateSpec - * - * @remarks - * Caching is good because it reduces the time to completion on subsequent calls to a dynamic subcommand, but when the data does not outlive the cache this allows a mechanism for opting out of it. - */ - cache?: boolean; - } + /** + * Specifies whether or not to cache the result of loadSpec and generateSpec + * + * @remarks + * Caching is good because it reduces the time to completion on subsequent calls to a dynamic subcommand, but when the data does not outlive the cache this allows a mechanism for opting out of it. + */ + cache?: boolean; + } - /** - * The option object represent CLI options (sometimes called flags). - * - * A option can have an argument. An option can NOT have subcommands or other option - */ - interface Option extends BaseSuggestion { - /** - * The exact name of the subcommand as defined in the CLI tool. - * - * @remarks - * Fig's parser relies on your option name being exactly what the user would type. (e.g. if the user types `git "-m"`, you must have `name: "-m"` and not something like `name: "your message"` or even with an `=` sign like`name: "-m="`) - * - * If you want to customize what the text the popup says, use `displayName`. - * - * The name prop in an Option object compiles down to the name prop in a Suggestion object - * - * Final note: the name prop can be a string (most common) or an array of strings - * - * - * @example - * For `git commit -m` in the, message option nested beneath `commit` would have `name: ["-m", "--message"]` - * @example - * For `ls -l` the `-l` option would have `name: "-l"` - */ - name: SingleOrArray; + /** + * The option object represent CLI options (sometimes called flags). + * + * A option can have an argument. An option can NOT have subcommands or other option + */ + interface Option extends BaseSuggestion { + /** + * The exact name of the subcommand as defined in the CLI tool. + * + * @remarks + * Fig's parser relies on your option name being exactly what the user would type. (e.g. if the user types `git "-m"`, you must have `name: "-m"` and not something like `name: "your message"` or even with an `=` sign like`name: "-m="`) + * + * If you want to customize what the text the popup says, use `displayName`. + * + * The name prop in an Option object compiles down to the name prop in a Suggestion object + * + * Final note: the name prop can be a string (most common) or an array of strings + * + * + * @example + * For `git commit -m` in the, message option nested beneath `commit` would have `name: ["-m", "--message"]` + * @example + * For `ls -l` the `-l` option would have `name: "-l"` + */ + name: SingleOrArray; - /** - * An array of arg objects or a single arg object - * - * @remarks - * If a subcommand takes an argument, please at least include an empty Arg Object. (e.g. `{ }`). Why? If you don't, Fig will assume the subcommand does not take an argument. When the user types their argument - * If the argument is optional, signal this by saying `isOptional: true`. - * - * @example - * `npm run` takes one mandatory argument. This can be represented by `args: { }` - * @example - * `git push` takes two optional arguments. This can be represented by: `args: [{ isOptional: true }, { isOptional: true }]` - * @example - * `git clone` takes one mandatory argument and one optional argument. This can be represented by: `args: [{ }, { isOptional: true }]` - */ - args?: SingleOrArray; - /** - * - * Signals whether an option is persistent, meaning that it will still be available - * as an option for all child subcommands. - * - * @remarks - * As of now there is no way to disable this - * persistence for certain children. Also see - * https://github.com/spf13/cobra/blob/master/user_guide.md#persistent-flags. - * - * @defaultValue false - * - * @example - * Say the `git` spec had an option at the top level with `{ name: "--help", isPersistent: true }`. - * Then the spec would recognize both `git --help` and `git commit --help` - * as a valid as we are passing the `--help` option to all `git` subcommands. - * - */ - isPersistent?: boolean; - /** - * Signals whether an option is required. - * - * @defaultValue false (option is NOT required) - * @example - * The `-m` option of `git commit` is required - * - */ - isRequired?: boolean; - /** - * - * Signals whether an equals sign is required to pass an argument to an option (e.g. `git commit --message="msg"`) - * @defaultValue false (does NOT require an equal) - * - * @example - * When `requiresEqual: true` the user MUST do `--opt=value` and cannot do `--opt value` - * - * @deprecated use `requiresSeparator` instead - * - */ - requiresEquals?: boolean; - /** - * - * Signals whether one of the separators specified in parserDirectives is required to pass an argument to an option (e.g. `git commit --message[separator]"msg"`) - * If set to true this will automatically insert an equal after the option name. - * If set to a separator (string) this will automatically insert the separator specified after the option name. - * @defaultValue false (does NOT require a separator) - * - * @example - * When `requiresSeparator: true` the user MUST do `--opt=value` and cannot do `--opt value` - * @example - * When `requiresSeparator: ':'` the user MUST do `--opt:value` and cannot do `--opt value` - */ - requiresSeparator?: boolean | string; - /** - * - * Signals whether an option can be passed multiple times. - * - * @defaultValue false (option is NOT repeatable) - * - * @remarks - * Passing `isRepeatable: true` will allow an option to be passed any number - * of times, while passing `isRepeatable: 2` will allow it to be passed - * twice, etc. Passing `isRepeatable: false` is the same as passing - * `isRepeatable: 1`. - * - * If you explicitly specify the isRepeatable option in a spec, this - * constraint will be enforced at the parser level, meaning after the option - * (say `-o`) has been passed the maximum number of times, Fig's parser will - * not recognize `-o` as an option if the user types it again. - * - * @example - * In `npm install` doesn't specify `isRepeatable` for `{ name: ["-D", "--save-dev"] }`. - * When the user types `npm install -D`, Fig will no longer suggest `-D`. - * If the user types `npm install -D -D`. Fig will still parse the second - * `-D` as an option. - * - * Suppose `npm install` explicitly specified `{ name: ["-D", "--save-dev"], isRepeatable: false }`. - * Now if the user types `npm install -D -D`, Fig will instead parse the second - * `-D` as the argument to the `install` subcommand instead of as an option. - * - * @example - * SSH has `{ name: "-v", isRepeatable: 3 }`. When the user types `ssh -vv`, Fig - * will still suggest `-v`, when the user types `ssh -vvv` Fig will stop - * suggesting `-v` as an option. Finally if the user types `ssh -vvvv` Fig's - * parser will recognize that this is not a valid string of chained options - * and will treat this as an argument to `ssh`. - * - */ - isRepeatable?: boolean | number; - /** - * - * Signals whether an option is mutually exclusive with other options (ie if the user has this option, Fig should not show the options specified). - * @defaultValue false - * - * @remarks - * Options that are mutually exclusive with flags the user has already passed will not be shown in the suggestions list. - * - * @example - * You might see `[-a | --interactive | --patch]` in a man page. This means each of these options are mutually exclusive on each other. - * If we were defining the exclusive prop of the "-a" option, then we would have `exclusive: ["--interactive", "--patch"]` - * - */ - exclusiveOn?: string[]; - /** - * - * - * Signals whether an option depends on other options (ie if the user has this option, Fig should only show these options until they are all inserted). - * - * @defaultValue false - * - * @remarks - * If the user has an unmet dependency for a flag they've already typed, this dependency will have boosted priority in the suggestion list. - * - * @example - * In a tool like firebase, we may want to delete a specific extension. The command might be `firebase delete --project ABC --extension 123` This would mean we delete the 123 extension from the ABC project. - * In this case, `--extension` dependsOn `--project` - * - */ - dependsOn?: string[]; - } + /** + * An array of arg objects or a single arg object + * + * @remarks + * If a subcommand takes an argument, please at least include an empty Arg Object. (e.g. `{ }`). Why? If you don't, Fig will assume the subcommand does not take an argument. When the user types their argument + * If the argument is optional, signal this by saying `isOptional: true`. + * + * @example + * `npm run` takes one mandatory argument. This can be represented by `args: { }` + * @example + * `git push` takes two optional arguments. This can be represented by: `args: [{ isOptional: true }, { isOptional: true }]` + * @example + * `git clone` takes one mandatory argument and one optional argument. This can be represented by: `args: [{ }, { isOptional: true }]` + */ + args?: SingleOrArray; + /** + * + * Signals whether an option is persistent, meaning that it will still be available + * as an option for all child subcommands. + * + * @remarks + * As of now there is no way to disable this + * persistence for certain children. Also see + * https://github.com/spf13/cobra/blob/master/user_guide.md#persistent-flags. + * + * @defaultValue false + * + * @example + * Say the `git` spec had an option at the top level with `{ name: "--help", isPersistent: true }`. + * Then the spec would recognize both `git --help` and `git commit --help` + * as a valid as we are passing the `--help` option to all `git` subcommands. + * + */ + isPersistent?: boolean; + /** + * Signals whether an option is required. + * + * @defaultValue false (option is NOT required) + * @example + * The `-m` option of `git commit` is required + * + */ + isRequired?: boolean; + /** + * + * Signals whether an equals sign is required to pass an argument to an option (e.g. `git commit --message="msg"`) + * @defaultValue false (does NOT require an equal) + * + * @example + * When `requiresEqual: true` the user MUST do `--opt=value` and cannot do `--opt value` + * + * @deprecated use `requiresSeparator` instead + * + */ + requiresEquals?: boolean; + /** + * + * Signals whether one of the separators specified in parserDirectives is required to pass an argument to an option (e.g. `git commit --message[separator]"msg"`) + * If set to true this will automatically insert an equal after the option name. + * If set to a separator (string) this will automatically insert the separator specified after the option name. + * @defaultValue false (does NOT require a separator) + * + * @example + * When `requiresSeparator: true` the user MUST do `--opt=value` and cannot do `--opt value` + * @example + * When `requiresSeparator: ':'` the user MUST do `--opt:value` and cannot do `--opt value` + */ + requiresSeparator?: boolean | string; + /** + * + * Signals whether an option can be passed multiple times. + * + * @defaultValue false (option is NOT repeatable) + * + * @remarks + * Passing `isRepeatable: true` will allow an option to be passed any number + * of times, while passing `isRepeatable: 2` will allow it to be passed + * twice, etc. Passing `isRepeatable: false` is the same as passing + * `isRepeatable: 1`. + * + * If you explicitly specify the isRepeatable option in a spec, this + * constraint will be enforced at the parser level, meaning after the option + * (say `-o`) has been passed the maximum number of times, Fig's parser will + * not recognize `-o` as an option if the user types it again. + * + * @example + * In `npm install` doesn't specify `isRepeatable` for `{ name: ["-D", "--save-dev"] }`. + * When the user types `npm install -D`, Fig will no longer suggest `-D`. + * If the user types `npm install -D -D`. Fig will still parse the second + * `-D` as an option. + * + * Suppose `npm install` explicitly specified `{ name: ["-D", "--save-dev"], isRepeatable: false }`. + * Now if the user types `npm install -D -D`, Fig will instead parse the second + * `-D` as the argument to the `install` subcommand instead of as an option. + * + * @example + * SSH has `{ name: "-v", isRepeatable: 3 }`. When the user types `ssh -vv`, Fig + * will still suggest `-v`, when the user types `ssh -vvv` Fig will stop + * suggesting `-v` as an option. Finally if the user types `ssh -vvvv` Fig's + * parser will recognize that this is not a valid string of chained options + * and will treat this as an argument to `ssh`. + * + */ + isRepeatable?: boolean | number; + /** + * + * Signals whether an option is mutually exclusive with other options (ie if the user has this option, Fig should not show the options specified). + * @defaultValue false + * + * @remarks + * Options that are mutually exclusive with flags the user has already passed will not be shown in the suggestions list. + * + * @example + * You might see `[-a | --interactive | --patch]` in a man page. This means each of these options are mutually exclusive on each other. + * If we were defining the exclusive prop of the "-a" option, then we would have `exclusive: ["--interactive", "--patch"]` + * + */ + exclusiveOn?: string[]; + /** + * + * + * Signals whether an option depends on other options (ie if the user has this option, Fig should only show these options until they are all inserted). + * + * @defaultValue false + * + * @remarks + * If the user has an unmet dependency for a flag they've already typed, this dependency will have boosted priority in the suggestion list. + * + * @example + * In a tool like firebase, we may want to delete a specific extension. The command might be `firebase delete --project ABC --extension 123` This would mean we delete the 123 extension from the ABC project. + * In this case, `--extension` dependsOn `--project` + * + */ + dependsOn?: string[]; + } - /** - * The arg object represent CLI arguments (sometimes called positional arguments). - * - * An argument is different to a subcommand object and option object. It does not compile down to a suggestion object. Rather, it represents custom user input. If you want to generate suggestions for this custom user input, you should use the generator prop nested beneath an Arg object - */ - interface Arg { - /** - * The name of an argument. This is different to the `name` prop for subcommands, options, and suggestion objects so please read carefully. - * This `name` prop signals a normal, human readable string. It usually signals to the user the type of argument they are inserting if there are no available suggestions. - * Unlike subcommands and options, Fig does NOT use this value for parsing. Therefore, it can be whatever you want. - * - * @example - * The name prop for the `git commit -m ` arg object is "msg". But you could also make it "message" or "your message". It is only used for description purposes (you see it when you type the message), not for parsing! - */ - name?: string; + /** + * The arg object represent CLI arguments (sometimes called positional arguments). + * + * An argument is different to a subcommand object and option object. It does not compile down to a suggestion object. Rather, it represents custom user input. If you want to generate suggestions for this custom user input, you should use the generator prop nested beneath an Arg object + */ + interface Arg { + /** + * The name of an argument. This is different to the `name` prop for subcommands, options, and suggestion objects so please read carefully. + * This `name` prop signals a normal, human readable string. It usually signals to the user the type of argument they are inserting if there are no available suggestions. + * Unlike subcommands and options, Fig does NOT use this value for parsing. Therefore, it can be whatever you want. + * + * @example + * The name prop for the `git commit -m ` arg object is "msg". But you could also make it "message" or "your message". It is only used for description purposes (you see it when you type the message), not for parsing! + */ + name?: string; - /** - * The text that gets rendered at the bottom of the autocomplete box a) when the user is inputting an argument and there are no suggestions and b) for all generated suggestions for an argument - * Keep it short and direct! - * - * @example - * "Your commit message" - */ - description?: string; + /** + * The text that gets rendered at the bottom of the autocomplete box a) when the user is inputting an argument and there are no suggestions and b) for all generated suggestions for an argument + * Keep it short and direct! + * + * @example + * "Your commit message" + */ + description?: string; - /** - * Specifies whether the suggestions generated for this argument are "dangerous". - * - * @remarks - * If true, Fig will not enable its autoexecute functionality. Autoexecute means if a user selects a suggestion it will insert the text and run the command. We signal this by changing the icon to red. - * Turning on isDangerous will make it harder for a user to accidentally run a dangerous command. - * - * @defaultValue false - * - * @example - * This is used for all arguments in the `rm` spec. - */ - isDangerous?: boolean; + /** + * Specifies whether the suggestions generated for this argument are "dangerous". + * + * @remarks + * If true, Fig will not enable its autoexecute functionality. Autoexecute means if a user selects a suggestion it will insert the text and run the command. We signal this by changing the icon to red. + * Turning on isDangerous will make it harder for a user to accidentally run a dangerous command. + * + * @defaultValue false + * + * @example + * This is used for all arguments in the `rm` spec. + */ + isDangerous?: boolean; - /** - * A list of Suggestion objects that are shown when a user is typing an argument. - * - * @remarks - * These suggestions are static meaning you know them beforehand and they are not generated at runtime. If you want to generate suggestions at runtime, use a generator - * - * @example - * For `git reset `, a two common arguments to pass are "head" and "head^". Therefore, the spec suggests both of these by using the suggestion prop - */ - suggestions?: (string | Suggestion)[]; - /** - * A template which is a single TemplateString or an array of TemplateStrings - * - * @remarks - * Templates are generators prebuilt by Fig. Here are the three templates: - * - filepaths: show folders and filepaths. Allow autoexecute on filepaths - * - folders: show folders only. Allow autoexecute on folders - * - history: show suggestions for all items in history matching this pattern - * - help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand - * - * @example - * `cd` uses the "folders" template - * @example - * `ls` used ["filepaths", "folders"]. Why both? Because if I `ls` a directory, we want to enable a user to autoexecute on this directory. If we just did "filepaths" they couldn't autoexecute. - * - */ - template?: Template; - /** - * - * Generators let you dynamically generate suggestions for arguments by running shell commands on a user's device. - * - * This takes a single generator or an array of generators - */ - generators?: SingleOrArray; - /** - * This option allows to enforce the suggestion filtering strategy for a specific argument suggestions. - * @remarks - * Users always want to have the most accurate results at the top of the suggestions list. - * For example we can enable fuzzy search on an argument that always requires fuzzy search to show the best suggestions. - * This property is also useful when argument suggestions have a prefix (e.g. the npm package scope) because enabling fuzzy search users can omit that part (see the second example below) - * @example - * npm uninstall [packages...] uses fuzzy search to allow searching for installed packages ignoring the package scope - * ```typescript - * const figSpec: Fig.Spec { - * name: "npm", - * subcommands: [ - * { - * args: { - * name: "packages", - * filterStrategy: "fuzzy", // search in suggestions provided by the generator (in this case) using fuzzy search - * generators: generateNpmDeps, - * isVariadic: true, - * }, - * }, - * // ... other npm commands - * ], - * } - * ``` - */ - filterStrategy?: "fuzzy" | "prefix" | "default"; - /** - * Provide a suggestion at the top of the list with the current token that is being typed by the user. - */ - suggestCurrentToken?: boolean; - /** - * Specifies that the argument is variadic and therefore repeats infinitely. - * - * @remarks - * Man pages represent variadic arguments with an ellipsis e.g. `git add ` - * - * @example - * `echo` takes a variadic argument (`echo hello world ...`) - * @example - * `git add` also takes a variadic argument - */ - isVariadic?: boolean; + /** + * A list of Suggestion objects that are shown when a user is typing an argument. + * + * @remarks + * These suggestions are static meaning you know them beforehand and they are not generated at runtime. If you want to generate suggestions at runtime, use a generator + * + * @example + * For `git reset `, a two common arguments to pass are "head" and "head^". Therefore, the spec suggests both of these by using the suggestion prop + */ + suggestions?: (string | Suggestion)[]; + /** + * A template which is a single TemplateString or an array of TemplateStrings + * + * @remarks + * Templates are generators prebuilt by Fig. Here are the three templates: + * - filepaths: show folders and filepaths. Allow autoexecute on filepaths + * - folders: show folders only. Allow autoexecute on folders + * - history: show suggestions for all items in history matching this pattern + * - help: show subcommands. Only includes the 'siblings' of the nearest 'parent' subcommand + * + * @example + * `cd` uses the "folders" template + * @example + * `ls` used ["filepaths", "folders"]. Why both? Because if I `ls` a directory, we want to enable a user to autoexecute on this directory. If we just did "filepaths" they couldn't autoexecute. + * + */ + template?: Template; + /** + * + * Generators let you dynamically generate suggestions for arguments by running shell commands on a user's device. + * + * This takes a single generator or an array of generators + */ + generators?: SingleOrArray; + /** + * This option allows to enforce the suggestion filtering strategy for a specific argument suggestions. + * @remarks + * Users always want to have the most accurate results at the top of the suggestions list. + * For example we can enable fuzzy search on an argument that always requires fuzzy search to show the best suggestions. + * This property is also useful when argument suggestions have a prefix (e.g. the npm package scope) because enabling fuzzy search users can omit that part (see the second example below) + * @example + * npm uninstall [packages...] uses fuzzy search to allow searching for installed packages ignoring the package scope + * ```typescript + * const figSpec: Fig.Spec { + * name: "npm", + * subcommands: [ + * { + * args: { + * name: "packages", + * filterStrategy: "fuzzy", // search in suggestions provided by the generator (in this case) using fuzzy search + * generators: generateNpmDeps, + * isVariadic: true, + * }, + * }, + * // ... other npm commands + * ], + * } + * ``` + */ + filterStrategy?: "fuzzy" | "prefix" | "default"; + /** + * Provide a suggestion at the top of the list with the current token that is being typed by the user. + */ + suggestCurrentToken?: boolean; + /** + * Specifies that the argument is variadic and therefore repeats infinitely. + * + * @remarks + * Man pages represent variadic arguments with an ellipsis e.g. `git add ` + * + * @example + * `echo` takes a variadic argument (`echo hello world ...`) + * @example + * `git add` also takes a variadic argument + */ + isVariadic?: boolean; - /** - * Specifies whether options can interrupt variadic arguments. There is - * slightly different behavior when this is used on an option argument and - * on a subcommand argument: - * - * - When an option breaks a *variadic subcommand argument*, after the option - * and any arguments are parsed, the parser will continue parsing variadic - * arguments to the subcommand - * - When an option breaks a *variadic option argument*, after the breaking - * option and any arguments are parsed, the original variadic options - * arguments will be terminated. See the second examples below for details. - * - * - * @defaultValue true - * - * @example - * When true for git add's argument: - * `git add file1 -v file2` will interpret `-v` as an option NOT an - * argument, and will continue interpreting file2 as a variadic argument to - * add after - * - * @example - * When true for -T's argument, where -T is a variadic list of tags: - * `cmd -T tag1 tag2 -p project tag3` will interpret `-p` as an option, but - * will then terminate the list of tags. So tag3 is not parsed as an - * argument to `-T`, but rather as a subcommand argument to `cmd` if `cmd` - * takes any arguments. - * - * @example - * When false: - * `echo hello -n world` will treat -n as an argument NOT an option. - * However, in `echo -n hello world` it will treat -n as an option as - * variadic arguments haven't started yet - * - */ - optionsCanBreakVariadicArg?: boolean; + /** + * Specifies whether options can interrupt variadic arguments. There is + * slightly different behavior when this is used on an option argument and + * on a subcommand argument: + * + * - When an option breaks a *variadic subcommand argument*, after the option + * and any arguments are parsed, the parser will continue parsing variadic + * arguments to the subcommand + * - When an option breaks a *variadic option argument*, after the breaking + * option and any arguments are parsed, the original variadic options + * arguments will be terminated. See the second examples below for details. + * + * + * @defaultValue true + * + * @example + * When true for git add's argument: + * `git add file1 -v file2` will interpret `-v` as an option NOT an + * argument, and will continue interpreting file2 as a variadic argument to + * add after + * + * @example + * When true for -T's argument, where -T is a variadic list of tags: + * `cmd -T tag1 tag2 -p project tag3` will interpret `-p` as an option, but + * will then terminate the list of tags. So tag3 is not parsed as an + * argument to `-T`, but rather as a subcommand argument to `cmd` if `cmd` + * takes any arguments. + * + * @example + * When false: + * `echo hello -n world` will treat -n as an argument NOT an option. + * However, in `echo -n hello world` it will treat -n as an option as + * variadic arguments haven't started yet + * + */ + optionsCanBreakVariadicArg?: boolean; - /** - * `true` if an argument is optional (ie the CLI spec says it is not mandatory to include an argument, but you can if you want to). - * - * @remarks - * NOTE: It is important you include this for our parsing. If you don't, Fig will assume the argument is mandatory. When we assume an argument is mandatory, we force the user to input the argument and hide all other suggestions. - * - * @example - * `git push [remote] [branch]` takes two optional args. - */ - isOptional?: boolean; - /** - * Syntactic sugar over the `loadSpec` prop. - * - * @remarks - * Specifies that the argument is an entirely new command which Fig should start completing on from scratch. - * - * @example - * `time` and `builtin` have only one argument and this argument has the `isCommand` property. If I type `time git`, Fig will load up the git completion spec because the isCommand property is set. - */ - isCommand?: boolean; - /** - * The same as the `isCommand` prop, except Fig will look for a completion spec in the `.fig/autocomplete/build` folder in the user's current working directory. - * - * @remarks - * See our docs for more on building completion specs for local scripts [Fig for Teams](https://fig.io/docs/) - * @example - * `python` take one argument which is a `.py` file. If I have a `main.py` file on my desktop and my current working directory is my desktop, if I type `python main.py[space]` Fig will look for a completion spec in `~/Desktop/.fig/autocomplete/build/main.py.js` - */ - isScript?: boolean; - /** - * The same as the `isCommand` prop, except you specify a string to prepend to what the user inputs and fig will load the completion spec accordingly. - * @remarks - * If isModule: "python/", Fig would load up the `python/USER_INPUT.js` completion spec from the `~/.fig/autocomplete` folder. - * @example - * For `python -m`, the user can input a specific module such as http.server. Each module is effectively a mini CLI tool that should have its own completions. Therefore the argument object for -m has `isModule: "python/"`. Whatever the modules user inputs, Fig will look under the `~/.fig/autocomplete/python/` directory for completion spec. - * - * @deprecated use `loadSpec` instead - */ - isModule?: string; + /** + * `true` if an argument is optional (ie the CLI spec says it is not mandatory to include an argument, but you can if you want to). + * + * @remarks + * NOTE: It is important you include this for our parsing. If you don't, Fig will assume the argument is mandatory. When we assume an argument is mandatory, we force the user to input the argument and hide all other suggestions. + * + * @example + * `git push [remote] [branch]` takes two optional args. + */ + isOptional?: boolean; + /** + * Syntactic sugar over the `loadSpec` prop. + * + * @remarks + * Specifies that the argument is an entirely new command which Fig should start completing on from scratch. + * + * @example + * `time` and `builtin` have only one argument and this argument has the `isCommand` property. If I type `time git`, Fig will load up the git completion spec because the isCommand property is set. + */ + isCommand?: boolean; + /** + * The same as the `isCommand` prop, except Fig will look for a completion spec in the `.fig/autocomplete/build` folder in the user's current working directory. + * + * @remarks + * See our docs for more on building completion specs for local scripts [Fig for Teams](https://fig.io/docs/) + * @example + * `python` take one argument which is a `.py` file. If I have a `main.py` file on my desktop and my current working directory is my desktop, if I type `python main.py[space]` Fig will look for a completion spec in `~/Desktop/.fig/autocomplete/build/main.py.js` + */ + isScript?: boolean; + /** + * The same as the `isCommand` prop, except you specify a string to prepend to what the user inputs and fig will load the completion spec accordingly. + * @remarks + * If isModule: "python/", Fig would load up the `python/USER_INPUT.js` completion spec from the `~/.fig/autocomplete` folder. + * @example + * For `python -m`, the user can input a specific module such as http.server. Each module is effectively a mini CLI tool that should have its own completions. Therefore the argument object for -m has `isModule: "python/"`. Whatever the modules user inputs, Fig will look under the `~/.fig/autocomplete/python/` directory for completion spec. + * + * @deprecated use `loadSpec` instead + */ + isModule?: string; - /** - * This will debounce every keystroke event for this particular arg. - * @remarks - * If there are no keystroke events after 100ms, Fig will execute all the generators in this arg and return the suggestions. - * - * @example - * `npm install` and `pip install` send debounced network requests after inactive typing from users. - */ - debounce?: boolean; - /** - * The default value for an optional argument. - * - * @remarks - * Note: This is currently not used anywhere in Fig's autocomplete popup, but will be soon. - * - */ - default?: string; - /** - * See [`loadSpec` in Subcommand Object](https://fig.io/docs/reference/subcommand#loadspec). - * - * @remarks - * There is a very high chance you want to use one of the following: - * 1. `isCommand` (See [Arg Object](https://fig.io/docs/reference/arg#iscommand)) - * 2. `isScript` (See [Arg Object](https://fig.io/docs/reference/arg#isscript)) - * - */ - loadSpec?: LoadSpec; + /** + * This will debounce every keystroke event for this particular arg. + * @remarks + * If there are no keystroke events after 100ms, Fig will execute all the generators in this arg and return the suggestions. + * + * @example + * `npm install` and `pip install` send debounced network requests after inactive typing from users. + */ + debounce?: boolean; + /** + * The default value for an optional argument. + * + * @remarks + * Note: This is currently not used anywhere in Fig's autocomplete popup, but will be soon. + * + */ + default?: string; + /** + * See [`loadSpec` in Subcommand Object](https://fig.io/docs/reference/subcommand#loadspec). + * + * @remarks + * There is a very high chance you want to use one of the following: + * 1. `isCommand` (See [Arg Object](https://fig.io/docs/reference/arg#iscommand)) + * 2. `isScript` (See [Arg Object](https://fig.io/docs/reference/arg#isscript)) + * + */ + loadSpec?: LoadSpec; - /** - * The `arg.parserDirective.alias` prop defines whether Fig's tokenizer should expand out an alias into separate tokens then offer completions accordingly. - * - * @remarks - * This is similar to how Fig is able to offer autocomplete for user defined shell aliases, but occurs at the completion spec level. - * - * @param token - The token that the user has just typed that is an alias for something else - * @param executeCommand -an async function that allows you to execute a shell command on the user's system and get the output as a string. - * @returns The expansion of the alias that Fig's bash parser will reparse as if it were typed out in full, rather than the alias. - * - * If for some reason you know exactly what it will be, you may also just pass in the expanded alias, not a function that returns the expanded alias. - * - * @example - * git takes git aliases. These aliases are defined in a user's gitconfig file. Let's say a user has an alias for `p=push`, then if a user typed `git p[space]`, this function would take the `p` token, return `push` and then offer suggestions as if the user had typed `git push[space]` - * - * @example - * `npm run