diff --git a/jest.config.cjs b/jest.config.cjs index b413e10..3f98ef0 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,5 +1,18 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; \ No newline at end of file + // [...] + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` + // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` + "^.+\\.tsx?$": [ + "ts-jest", + { + useESM: true, + }, + ], + }, +}; diff --git a/package-lock.json b/package-lock.json index 0639ec8..38842bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,11 @@ "wrap-ansi": "^8.1.0" }, "devDependencies": { - "@babel/preset-typescript": "^7.23.0", "@tsconfig/node18": "^18.2.2", "@types/ink": "^2.0.3", "@types/jest": "^29.5.5", "@types/react": "^18.2.24", "@withfig/autocomplete-types": "^1.28.0", - "cross-env": "^7.0.3", "jest": "^29.7.0", "ts-jest": "^29.1.1", "typescript": "^5.2.2" @@ -185,18 +183,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", @@ -237,38 +223,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", @@ -303,18 +257,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", @@ -346,18 +288,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", @@ -367,23 +297,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/helper-simple-access": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", @@ -396,18 +309,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", @@ -720,60 +621,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz", - "integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz", - "integrity": "sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-typescript": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -2521,24 +2368,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5770,15 +5599,6 @@ "jsesc": "^2.5.1" } }, - "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, "@babel/helper-compilation-targets": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", @@ -5815,31 +5635,6 @@ } } }, - "@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, "@babel/helper-environment-visitor": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", @@ -5865,15 +5660,6 @@ "@babel/types": "^7.22.5" } }, - "@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, - "requires": { - "@babel/types": "^7.23.0" - } - }, "@babel/helper-module-imports": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", @@ -5896,32 +5682,12 @@ "@babel/helper-validator-identifier": "^7.22.20" } }, - "@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, "@babel/helper-plugin-utils": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", "dev": true }, - "@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - } - }, "@babel/helper-simple-access": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", @@ -5931,15 +5697,6 @@ "@babel/types": "^7.22.5" } }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "requires": { - "@babel/types": "^7.22.5" - } - }, "@babel/helper-split-export-declaration": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", @@ -6164,42 +5921,6 @@ "@babel/helper-plugin-utils": "^7.22.5" } }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", - "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - } - }, - "@babel/plugin-transform-typescript": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.15.tgz", - "integrity": "sha512-1uirS0TnijxvQLnlv5wQBwOX3E1wCFX7ITv+9pBV2wKEk4K+M5tqDaoNXnTH8tjEIYHLO98MwiTWO04Ggz4XuA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" - } - }, - "@babel/preset-typescript": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.0.tgz", - "integrity": "sha512-6P6VVa/NM/VlAYj5s2Aq/gdVg8FSENCg3wlZ6Qau9AcPaoF5LbN1nyGlR9DTRIw9PpxI94e+ReydsJHcjwAweg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-typescript": "^7.22.15" - } - }, "@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -7551,15 +7272,6 @@ } } }, - "cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.1" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", diff --git a/package.json b/package.json index bacd848..c973c09 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "start": "node ./build/index.js", - "test": "jest" + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "repository": { "type": "git", diff --git a/src/runtime/generator.ts b/src/runtime/generator.ts new file mode 100644 index 0000000..3bbd2e5 --- /dev/null +++ b/src/runtime/generator.ts @@ -0,0 +1,47 @@ +import { exec } from "node:child_process"; +import { runTemplates } from "./template.js"; + +const buildExecuteShellCommand = + (timeout: number) => + async (command: string, cwd?: string): Promise => { + return new Promise((resolve, reject) => { + exec(command, { timeout }, (_, stdout, stderr) => { + resolve(stdout || stderr); + }); + }); + }; + +const getGeneratorContext = (): Fig.GeneratorContext => { + return { + environmentVariables: Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] != null)), + currentWorkingDirectory: process.cwd(), + currentProcess: "", // TODO: define current process + sshPrefix: "", // deprecated, should be empty + isDangerous: false, + searchTerm: "", // TODO: define search term + }; +}; + +// TODO: add support for caching, trigger, & getQueryTerm +export const runGenerator = async (generator: Fig.Generator, tokens: string[]): Promise => { + const { script, postProcess, scriptTimeout, splitOn, custom, template } = generator; + const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000); + const suggestions = []; + if (script) { + const scriptOutput = typeof script === "function" ? script(tokens) : script != null ? await executeShellCommand(script) : ""; + if (postProcess) { + suggestions.push(...postProcess(scriptOutput, tokens)); + } else if (splitOn) { + suggestions.push(...scriptOutput.split(splitOn).map((s) => ({ name: s }))); + } + } + + if (custom) { + suggestions.push(...(await custom(tokens, executeShellCommand, getGeneratorContext()))); + } + + if (template != null) { + suggestions.push(...runTemplates(template)); + } + return suggestions; +}; diff --git a/src/runtime/model.ts b/src/runtime/model.ts new file mode 100644 index 0000000..87be41e --- /dev/null +++ b/src/runtime/model.ts @@ -0,0 +1,5 @@ +export type Suggestion = { + name: string; + description?: string; + icon: string; +}; diff --git a/src/runtime/parser.ts b/src/runtime/parser.ts index 74cf24d..f2eef11 100644 --- a/src/runtime/parser.ts +++ b/src/runtime/parser.ts @@ -1,7 +1,8 @@ -type CommandToken = { +export type CommandToken = { token: string; complete: boolean; isOption: boolean; + isPersistent?: boolean; }; const cmdDelim = /(\|\|)|(&&)|(;)/; @@ -9,10 +10,10 @@ const spaceRegex = /\s/; export const parseCommand = (command: string): CommandToken[] => { const lastCommand = command.split(cmdDelim).at(-1)?.trimStart(); - return lastCommand ? parse(lastCommand) : []; + return lastCommand ? lex(lastCommand) : []; }; -const parse = (command: string): CommandToken[] => { +const lex = (command: string): CommandToken[] => { const tokens: CommandToken[] = []; let [readingQuotedString, readingFlag, readingCmd] = [false, false, false]; let readingIdx = 0; diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 9a115a1..3fe1f81 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -2,28 +2,207 @@ import speclist, { diffVersionedCompletions as versionedSpeclist, // @ts-ignore } from "@withfig/autocomplete/build/index.js"; +import { parseCommand, CommandToken } from "./parser.js"; +import { SuggestionBlob, getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion.js"; -const specs = (await Promise.all( - speclist.map(async (spec: string) => { - const prefix = versionedSpeclist.includes(spec) ? "/index.js" : `.js`; - return (await import(`@withfig/autocomplete/build/${spec}${prefix}`)) - .default; - }) -)) as Fig.Spec[]; +const specSet: any = {}; +(speclist as string[]).forEach((s) => { + let activeSet = specSet; + const specRoutes = s.split("/"); + specRoutes.forEach((route, idx) => { + if (idx === specRoutes.length - 1) { + const prefix = versionedSpeclist.includes(s) ? "/index.js" : `.js`; + activeSet[route] = `@withfig/autocomplete/build/${s}${prefix}`; + } else { + activeSet[route] = activeSet[route] || {}; + activeSet = activeSet[route]; + } + }); +}); -export const getSuggestions = (cmd: string) => { - const suggestions: Fig.Suggestion[] = [ - { - name: "test", - description: - "test this is a very long description that i'd expect to wrap", - }, - { name: "test1", description: "test" }, - { name: "test2", description: "test" }, - { name: "test3", description: "test" }, - { name: "test4", description: "test" }, - { name: "test5", description: "test" }, - ]; +const loadedSpecs: { [key: string]: Fig.Spec } = {}; - return suggestions; +const loadSpec = async (cmd: CommandToken[]): Promise => { + const rootToken = cmd.at(0); + if (!rootToken?.complete) { + return; + } + + if (loadedSpecs[rootToken.token]) { + return loadedSpecs[rootToken.token]; + } + if (specSet[rootToken.token]) { + return (await import(specSet[rootToken.token])).default; + } +}; + +export const getSuggestions = async (cmd: string): Promise => { + const activeCmd = parseCommand(cmd); + const rootToken = activeCmd.at(0); + if (activeCmd.length === 0 || !rootToken?.complete) { + return; + } + + const spec = await loadSpec(activeCmd); + if (spec == null) return; + const subcommand = getSubcommand(spec); + if (subcommand == null) return; + + return runSubcommand(activeCmd.slice(1), subcommand); +}; + +const getPersistentOptions = (persistentOptions: Fig.Option[], options?: Fig.Option[]) => { + const persistentOptionNames = new Set(persistentOptions.map((o) => (typeof o.name === "string" ? [o.name] : o.name)).flat()); + return persistentOptions.concat( + (options ?? []).filter( + (o) => (typeof o.name == "string" ? !persistentOptionNames.has(o.name) : o.name.some((n) => !persistentOptionNames.has(n))) && o.isPersistent === true + ) + ); +}; + +// TODO: handle subcommands that are versioned +const getSubcommand = (spec: Fig.Spec): Fig.Subcommand | undefined => { + if (typeof spec === "function") { + const potentialSubcommand = spec(); + if (potentialSubcommand.hasOwnProperty("name")) { + return potentialSubcommand as Fig.Subcommand; + } + return; + } + return spec; +}; + +// TODO: implement loadspec +const genSubcommand = (command: string, parentCommand: Fig.Subcommand): Fig.Subcommand | undefined => { + switch (typeof parentCommand.loadSpec) { + case "function": + + case "string": + + case "object": + + case "undefined": + return parentCommand.subcommands?.find((s) => s.name === command); + } +}; + +const getOption = (activeToken: CommandToken, options: Fig.Option[]): Fig.Option | undefined => { + return options.find((o) => (typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token))); +}; + +const getPersistentTokens = (tokens: CommandToken[]): CommandToken[] => { + return tokens.filter((t) => t.isPersistent === true); +}; + +const getArgs = (args: Fig.SingleOrArray | undefined): Fig.Arg[] => { + return args instanceof Array ? args : args != null ? [args] : []; +}; + +const runOption = async ( + tokens: CommandToken[], + option: Fig.Option, + subcommand: Fig.Subcommand, + persistentOptions: Fig.Option[], + acceptedTokens: CommandToken[] +): Promise => { + if (tokens.length === 0) { + throw new Error("invalid state reached, option expected but no tokens found"); + } + const activeToken = tokens[0]; + const isPersistent = persistentOptions.some((o) => (typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token))); + if ((option.args instanceof Array && option.args.length > 0) || option.args != null) { + const args = option.args instanceof Array ? option.args : [option.args]; + return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), true, false); + } + return runSubcommand(tokens.slice(1), subcommand, persistentOptions, acceptedTokens.concat({ ...activeToken, isPersistent })); +}; + +const runArg = async ( + tokens: CommandToken[], + args: Fig.Arg[], + subcommand: Fig.Subcommand, + persistentOptions: Fig.Option[], + acceptedTokens: CommandToken[], + fromOption: boolean, + fromVariadic: boolean +): Promise => { + if (args.length === 0) { + return runSubcommand(tokens, subcommand, persistentOptions, acceptedTokens, true, !fromOption); + } else if (tokens.length === 0) { + return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, fromVariadic); + } else if (!tokens.at(0)?.complete) { + return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0].token, acceptedTokens, fromVariadic); + } + + const activeToken = tokens[0]; + if (args.every((a) => a.isOptional)) { + if (activeToken.isOption) { + const option = getOption(activeToken, persistentOptions.concat(subcommand.options ?? [])); + if (option != null) { + return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens); + } + return; + } + + const nextSubcommand = genSubcommand(activeToken.token, subcommand); + if (nextSubcommand != null) { + return runSubcommand(tokens.slice(1), nextSubcommand, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken))); + } + } + + const activeArg = args[0]; + if (activeArg.isVariadic) { + return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true); + } else if (activeArg.isCommand) { + if (tokens.length <= 1) { + return; + } + const activeCmd = tokens[1]; + // TODO: handle subcommands that isCommands + return; + } + return runArg(tokens.slice(1), args.slice(1), subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, false); +}; + +const runSubcommand = async ( + tokens: CommandToken[], + subcommand: Fig.Subcommand, + persistentOptions: Fig.Option[] = [], + acceptedTokens: CommandToken[] = [], + argsDepleted = false, + argsUsed = false +): Promise => { + if (tokens.length === 0) { + return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens); + } else if (!tokens.at(0)?.complete) { + return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0].token, argsDepleted, argsUsed, acceptedTokens); + } + + const activeToken = tokens[0]; + const activeArgsLength = subcommand.args instanceof Array ? subcommand.args.length : 1; + const allOptions = [...persistentOptions, ...(subcommand.options ?? [])]; + + if (activeToken.isOption) { + const option = getOption(activeToken, allOptions); + if (option != null) { + return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens); + } + return; + } + + const nextSubcommand = genSubcommand(activeToken.token, subcommand); + if (nextSubcommand != null) { + return runSubcommand( + tokens.slice(1), + nextSubcommand, + getPersistentOptions(persistentOptions, subcommand.options), + getPersistentTokens(acceptedTokens.concat(activeToken)) + ); + } + + if (activeArgsLength <= 0) { + return; // not subcommand or option & no args exist + } + + return runArg(tokens, getArgs(subcommand.args), subcommand, allOptions, acceptedTokens, false, false); }; diff --git a/src/runtime/suggestion.ts b/src/runtime/suggestion.ts new file mode 100644 index 0000000..6e5511c --- /dev/null +++ b/src/runtime/suggestion.ts @@ -0,0 +1,204 @@ +import { CommandToken } from "./parser.js"; +import { runGenerator } from "./generator.js"; +import { runTemplates } from "./template.js"; +import { Suggestion } from "./model.js"; + +// TODO: support other suggestion attributes +enum SuggestionIcons { + File = "📄", + Folder = "📁", + Subcommand = "📦", + Option = "⚙️", + Argument = "💲", + Mixin = "🏝️", + Shortcut = "🔥", + Special = "⭐", + Default = "📀", +} + +const getIcon = (suggestionType?: Fig.SuggestionType | undefined): string => { + switch (suggestionType) { + case "arg": + return SuggestionIcons.Argument; + case "file": + return SuggestionIcons.File; + case "folder": + return SuggestionIcons.Folder; + case "option": + return SuggestionIcons.Option; + case "subcommand": + return SuggestionIcons.Subcommand; + case "mixin": + return SuggestionIcons.Mixin; + case "shortcut": + return SuggestionIcons.Shortcut; + case "special": + return SuggestionIcons.Special; + } + return SuggestionIcons.Default; +}; + +const getLong = (suggestion: Fig.SingleOrArray): string => { + return suggestion instanceof Array ? suggestion.reduce((p, c) => (p.length > c.length ? p : c)) : suggestion; +}; + +const toSuggestion = (suggestion: Fig.Suggestion, name?: string, type?: Fig.SuggestionType): Suggestion | undefined => { + if (suggestion.name == null) return; + return { + name: name ?? getLong(suggestion.name), + description: suggestion.description, + icon: getIcon(type ?? suggestion.type), + }; +}; + +function filter; type?: Fig.SuggestionType | undefined }>( + suggestions: T[], + filterStrategy: FilterStrategy | undefined, + partialCmd: string | undefined, + suggestionType: Fig.SuggestionType | undefined +): Suggestion[] { + if (!partialCmd) return suggestions.map((s) => toSuggestion(s, undefined, suggestionType)).filter((s) => s != null) as Suggestion[]; + + switch (filterStrategy) { + case "fuzzy": + return suggestions + .map((s) => { + if (s.name == null) return; + if (s.name instanceof Array) { + const matchedName = s.name.find((n) => n.toLowerCase().includes(partialCmd.toLowerCase())); + return matchedName != null ? { name: matchedName, description: s.description, icon: getIcon(s.type ?? suggestionType) } : undefined; + } + return s.name.toLowerCase().includes(partialCmd.toLowerCase()) + ? { name: s.name, description: s.description, icon: getIcon(s.type ?? suggestionType) } + : undefined; + }) + .filter((s) => s != null) as Suggestion[]; + default: + return suggestions + .map((s) => { + if (s.name == null) return; + if (s.name instanceof Array) { + const matchedName = s.name.find((n) => n.toLowerCase().startsWith(partialCmd.toLowerCase())); + return matchedName != null ? { name: matchedName, description: s.description, icon: getIcon(s.type ?? suggestionType) } : undefined; + } + return s.name.toLowerCase().startsWith(partialCmd.toLowerCase()) + ? { name: s.name, description: s.description, icon: getIcon(s.type ?? suggestionType) } + : undefined; + }) + .filter((s) => s != null) as Suggestion[]; + } +} + +export type SuggestionBlob = { + suggestions: Suggestion[]; + argumentDescription?: string; +}; + +type FilterStrategy = "fuzzy" | "prefix" | "default"; + +const generatorSuggestions = async ( + generator: Fig.SingleOrArray | undefined, + acceptedTokens: CommandToken[], + filterStrategy: FilterStrategy | undefined, + partialCmd: string | undefined +): Promise => { + const generators = generator instanceof Array ? generator : generator ? [generator] : []; + const tokens = acceptedTokens.map((t) => t.token); + const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens)))).flat(); + return filter(suggestions, filterStrategy, partialCmd, undefined); +}; + +const templateSuggestions = (templates: Fig.Template | undefined, filterStrategy: FilterStrategy | undefined, partialCmd: string | undefined): Suggestion[] => { + return filter(runTemplates(templates ?? []), filterStrategy, partialCmd, undefined); +}; + +const suggestionSuggestions = ( + suggestions: (string | Fig.Suggestion)[] | undefined, + filterStrategy: FilterStrategy | undefined, + partialCmd: string | undefined +): Suggestion[] => { + const cleanedSuggestions = suggestions?.map((s) => (typeof s === "string" ? { name: s } : s)) ?? []; + return filter(cleanedSuggestions ?? [], filterStrategy, partialCmd, undefined); +}; + +const subcommandSuggestions = ( + subcommands: Fig.Subcommand[] | undefined, + filterStrategy: FilterStrategy | undefined, + partialCmd: string | undefined +): Suggestion[] => { + return filter(subcommands ?? [], filterStrategy, partialCmd, "subcommand"); +}; + +const optionSuggestions = ( + options: Fig.Option[] | undefined, + acceptedTokens: CommandToken[], + filterStrategy: FilterStrategy | undefined, + partialCmd: string | undefined +): Suggestion[] => { + const usedOptions = new Set(acceptedTokens.filter((t) => t.isOption).map((t) => t.token)); + const validOptions = options?.filter((o) => o.exclusiveOn?.every((exclusiveOption) => !usedOptions.has(exclusiveOption)) ?? true); + return filter(validOptions ?? [], filterStrategy, partialCmd, "option"); +}; + +// TODO: handle case where short option has been seen, but now we recommend long option (see: completedOptionWithArg --actor flag) +const removeDuplicateSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => { + const seen = new Set(acceptedTokens.map((t) => t.token)); + return suggestions.filter((s) => !seen.has(s.name)); +}; + +// TODO: implement re-ranking globally +export const getSubcommandDrivenRecommendation = async ( + subcommand: Fig.Subcommand, + persistentOptions: Fig.Option[], + partialCmd: string | undefined, + argsDepleted: boolean, + argsFromSubcommand: boolean, + acceptedTokens: CommandToken[] +): Promise => { + if (argsDepleted && argsFromSubcommand) { + return; + } + const suggestions: Suggestion[] = []; + const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0; + const allOptions = persistentOptions.concat(subcommand.options ?? []); + if (argLength != 0) { + const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args; + suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd))); + suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd)); + suggestions.push(...templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd)); + } + if (!argsFromSubcommand) { + suggestions.push(...optionSuggestions(allOptions, acceptedTokens, subcommand.filterStrategy, partialCmd)); + suggestions.push(...subcommandSuggestions(subcommand.subcommands, subcommand.filterStrategy, partialCmd)); + } + return { + suggestions: removeDuplicateSuggestions(suggestions, acceptedTokens), + }; +}; + +export const getArgDrivenRecommendation = async ( + args: Fig.Arg[], + subcommand: Fig.Subcommand, + persistentOptions: Fig.Option[], + partialCmd: string | undefined, + acceptedTokens: CommandToken[], + variadicArgBound: boolean +): Promise => { + const activeArg = args[0]; + const allOptions = persistentOptions.concat(subcommand.options ?? []); + const suggestions = [ + ...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)), + ...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd), + ...templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd), + ]; + + if ((activeArg.isOptional && !activeArg.isVariadic) || (activeArg.isVariadic && activeArg.isOptional && !variadicArgBound)) { + suggestions.push(...subcommandSuggestions(subcommand.subcommands, activeArg?.filterStrategy, partialCmd)); + suggestions.push(...optionSuggestions(allOptions, acceptedTokens, activeArg?.filterStrategy, partialCmd)); + } + + return { + suggestions: removeDuplicateSuggestions(suggestions, acceptedTokens), + argumentDescription: activeArg.description ?? activeArg.name, + }; +}; diff --git a/src/runtime/template.ts b/src/runtime/template.ts new file mode 100644 index 0000000..f7c5897 --- /dev/null +++ b/src/runtime/template.ts @@ -0,0 +1,33 @@ +const filepathsTemplate = (): Fig.Suggestion[] => { + return []; +}; + +const foldersTemplate = (): Fig.Suggestion[] => { + return []; +}; + +const historyTemplate = (): Fig.Suggestion[] => { + return []; +}; + +const helpTemplate = (): Fig.Suggestion[] => { + return []; +}; + +export const runTemplates = (template: Fig.TemplateStrings[] | Fig.Template): Fig.Suggestion[] => { + const templates = template instanceof Array ? template : [template]; + return templates + .map((t) => { + switch (t) { + case "filepaths": + return filepathsTemplate(); + case "folders": + return foldersTemplate(); + case "history": + return historyTemplate(); + case "help": + return helpTemplate(); + } + }) + .flat(); +}; diff --git a/src/tests/runtime/__snapshots__/runtime.test.ts.snap b/src/tests/runtime/__snapshots__/runtime.test.ts.snap new file mode 100644 index 0000000..de82b2f --- /dev/null +++ b/src/tests/runtime/__snapshots__/runtime.test.ts.snap @@ -0,0 +1,258 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseCommand alreadyUsedOption 1`] = ` +{ + "suggestions": [ + { + "description": "Display system information for bug report", + "icon": "⚙️", + "name": "--bug-report", + }, + ], +} +`; + +exports[`parseCommand alreadyUsedSuggestion 1`] = `undefined`; + +exports[`parseCommand completePrefixFilter 1`] = ` +{ + "suggestions": [ + { + "description": "Show the working tree status", + "icon": "📦", + "name": "status", + }, + ], +} +`; + +exports[`parseCommand completedOptionWithArg 1`] = ` +{ + "suggestions": [ + { + "description": "User that triggered the event", + "icon": "⚙️", + "name": "--actor", + }, + { + "description": "Defines the path where the artifact server stores uploads and retrieves downloads from. If not specified the artifact server will not start", + "icon": "⚙️", + "name": "--artifact-server-path", + }, + { + "description": "Defines the port where the artifact server listens (will only bind to localhost)", + "icon": "⚙️", + "name": "--artifact-server-port", + }, + { + "description": "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms", + "icon": "⚙️", + "name": "--container-architecture", + }, + { + "description": "Path to Docker daemon socket which will be mounted to containers", + "icon": "⚙️", + "name": "--container-daemon-socket", + }, + { + "description": "Working directory", + "icon": "⚙️", + "name": "--directory", + }, + { + "description": "Dryrun mode", + "icon": "⚙️", + "name": "--dryrun", + }, + { + "description": "Environment file to read and use as env in the containers", + "icon": "⚙️", + "name": "--env-file", + }, + { + "description": "GitHub instance to use. Don't use this if you are not using GitHub Enterprise Server", + "icon": "⚙️", + "name": "--github-instance", + }, + { + "description": "NOT RECOMMENDED! Doesn't hide secrets while printing logs", + "icon": "⚙️", + "name": "--insecure-secrets", + }, + { + "description": "Output logs in json format", + "icon": "⚙️", + "name": "--json", + }, + { + "description": "Flag to disable running workflows from subdirectories of specified path in '--workflows'/'-W' flag", + "icon": "⚙️", + "name": "--no-recurse", + }, + { + "description": "Do not skip actions/checkout", + "icon": "⚙️", + "name": "--no-skip-checkout", + }, + { + "description": "Disable logging of output from steps", + "icon": "⚙️", + "name": "--quiet", + }, + { + "description": "File with list of secrets to read from (e.g. --secret-file .secrets)", + "icon": "⚙️", + "name": "--secret-file", + }, + { + "description": "Verbose output", + "icon": "⚙️", + "name": "--verbose", + }, + { + "description": "Path to workflow file(s)", + "icon": "⚙️", + "name": "--workflows", + }, + { + "description": "Display help", + "icon": "⚙️", + "name": "--help", + }, + { + "description": "Disable completion descriptions", + "icon": "⚙️", + "name": "--no-descriptions", + }, + ], +} +`; + +exports[`parseCommand emptySuggestions 1`] = ` +{ + "suggestions": [], +} +`; + +exports[`parseCommand exclusiveOnOption 1`] = ` +{ + "suggestions": [ + { + "description": "Print a newline between matches in different files. Enabled by default", + "icon": "⚙️", + "name": "--nobreak", + }, + { + "description": "Don't print color codes in results", + "icon": "⚙️", + "name": "--nocolor", + }, + { + "description": "Don't print file names", + "icon": "⚙️", + "name": "--nofilename", + }, + { + "description": "Don't follow symlinks", + "icon": "⚙️", + "name": "--nofollow", + }, + { + "description": "Refrains from lumping matches in the same file together, and instead places the filename at the start of each match line", + "icon": "⚙️", + "name": "--nogroup", + }, + { + "description": "Don't print filenames above matching contents", + "icon": "⚙️", + "name": "--noheading", + }, + { + "description": "Don't use of memory-mapped I/O. Defaults to true on platforms where mmap() is faster than read(). (All but macOS.)", + "icon": "⚙️", + "name": "--nommap", + }, + { + "description": "Don't match regexes across newlines", + "icon": "⚙️", + "name": "--nomultiline", + }, + { + "description": "Don't recurse into directories", + "icon": "⚙️", + "name": "--norecurse", + }, + { + "description": "Don't print line numbers", + "icon": "⚙️", + "name": "--nonumbers", + }, + ], +} +`; + +exports[`parseCommand fullyTypedSuggestion 1`] = ` +{ + "suggestions": [ + { + "description": "Display whiteouts when scanning directories. (-S) flag)", + "icon": "⚙️", + "name": "-W", + }, + { + "description": "Force raw printing of non-printable characters. This is the default when output is not to a terminal", + "icon": "⚙️", + "name": "-w", + }, + ], +} +`; + +exports[`parseCommand noOptionsSuggestedAfterVariadicArg 1`] = ` +{ + "argumentDescription": undefined, + "suggestions": [], +} +`; + +exports[`parseCommand partialPrefixFilter 1`] = ` +{ + "suggestions": [ + { + "description": "Add file contents to the staging area", + "icon": "📦", + "name": "stage", + }, + { + "description": "Show the working tree status", + "icon": "📦", + "name": "status", + }, + { + "description": "Temporarily stores all the modified tracked files", + "icon": "📦", + "name": "stash", + }, + ], +} +`; + +exports[`parseCommand providedArgDescription 1`] = ` +{ + "argumentDescription": "actor", + "suggestions": [], +} +`; + +exports[`parseCommand providedSuggestion 1`] = ` +{ + "argumentDescription": "Shell to generate completions for", + "suggestions": [ + { + "description": undefined, + "icon": "📀", + "name": "zsh", + }, + ], +} +`; diff --git a/src/tests/runtime/parser.test.ts b/src/tests/runtime/parser.test.ts index 68f17e4..ed79ba4 100644 --- a/src/tests/runtime/parser.test.ts +++ b/src/tests/runtime/parser.test.ts @@ -1,4 +1,4 @@ -import { parseCommand } from "../../runtime/parser"; +import { parseCommand } from "../../runtime/parser.js"; const testData = [ { command: `cmd --flag value` }, diff --git a/src/tests/runtime/runtime.test.ts b/src/tests/runtime/runtime.test.ts new file mode 100644 index 0000000..f3c3348 --- /dev/null +++ b/src/tests/runtime/runtime.test.ts @@ -0,0 +1,27 @@ +import { getSuggestions } from "../../runtime/runtime.js"; + +const testData = [ + { name: "partialPrefixFilter", command: "git sta" }, + { name: "completePrefixFilter", command: "git stat" }, + { name: "emptySuggestions", command: "git to" }, + { name: "alreadyUsedSuggestion", command: "cd ~ " }, + { name: "alreadyUsedOption", command: "act --bind --b" }, + { name: "exclusiveOnOption", command: "ag --affinity --no" }, + { name: "providedSuggestion", command: "bw completion --shell " }, + { name: "fullyTypedSuggestion", command: "ls -W" }, + { name: "noOptionsSuggestedAfterVariadicArg", command: "ls item -" }, + { name: "providedArgDescription", command: "act completion bash -a " }, + { name: "completedOptionWithArg", command: "act completion bash -a 'actor' " }, + { name: "command", command: "sudo git sta", skip: true }, // TODO: fix skipped test + { name: "nestedNonCommands", command: "az az ", skip: true }, // TODO: fix skipped test +]; + +describe(`parseCommand`, () => { + testData.forEach(({ command, name, skip }) => { + if (skip) return; + test(name, async () => { + const suggetions = await getSuggestions(command); + expect(suggetions).toMatchSnapshot(); + }); + }); +});