Skip to content

Commit

Permalink
fix: generators by syncing types/generators & providing path field su…
Browse files Browse the repository at this point in the history
…pport (#129)

Signed-off-by: Chapman Pendery <[email protected]>
  • Loading branch information
cpendery authored Dec 18, 2023
1 parent 6928eec commit 724304c
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 89 deletions.
43 changes: 23 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion src/commands/complete.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import os from "node:os";
import { Command } from "commander";
import { getSuggestions } from "../runtime/runtime.js";
import { Shell } from "../utils/shell.js";

const action = async (input: string) => {
const suggestions = await getSuggestions(input, process.cwd());
const suggestions = await getSuggestions(input, process.cwd(), os.platform() === "win32" ? Shell.Cmd : Shell.Bash);
process.stdout.write(JSON.stringify(suggestions));
};

Expand Down
2 changes: 1 addition & 1 deletion src/isterm/pty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export class ISTerm implements IPty {
case IstermOscPt.CurrentWorkingDirectory: {
const cwd = data.split(";").at(1);
if (cwd != null) {
this.cwd = this._sanitizedCwd(this._deserializeIsMessage(cwd));
this.cwd = path.resolve(this._sanitizedCwd(this._deserializeIsMessage(cwd)));
}
break;
}
Expand Down
27 changes: 15 additions & 12 deletions src/runtime/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,22 @@ const getGeneratorContext = (cwd: string): Fig.GeneratorContext => {

// TODO: add support for caching, trigger, & getQueryTerm
export const runGenerator = async (generator: Fig.Generator, tokens: string[], cwd: string): Promise<Fig.Suggestion[]> => {
const { script, postProcess, scriptTimeout, splitOn, custom, template } = generator;
// TODO: support trigger
const { script, postProcess, scriptTimeout, splitOn, custom, template, filterTemplateSuggestions } = generator;

const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000);
const suggestions = [];
try {
if (script) {
const scriptOutput =
typeof script === "function"
? script(tokens)
: typeof script === "string"
? await executeShellCommand(script)
: script != null
? await executeShellCommand((script as string[]).join(" "))
: "";
const shellInput = typeof script === "function" ? script(tokens) : script;
const scriptOutput = Array.isArray(shellInput)
? await executeShellCommand({ command: shellInput.at(0) ?? "", args: shellInput.slice(1) })
: await executeShellCommand(shellInput);

if (postProcess) {
suggestions.push(...postProcess(scriptOutput, tokens));
suggestions.push(...postProcess(scriptOutput.stdout, tokens));
} else if (splitOn) {
suggestions.push(...scriptOutput.split(splitOn).map((s) => ({ name: s })));
suggestions.push(...scriptOutput.stdout.split(splitOn).map((s) => ({ name: s })));
}
}

Expand All @@ -44,7 +42,12 @@ export const runGenerator = async (generator: Fig.Generator, tokens: string[], c
}

if (template != null) {
suggestions.push(...(await runTemplates(template, cwd)));
const templateSuggestions = await runTemplates(template, cwd);
if (filterTemplateSuggestions) {
suggestions.push(...filterTemplateSuggestions(templateSuggestions));
} else {
suggestions.push(...templateSuggestions);
}
}
return suggestions;
} catch (e) {
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type CommandToken = {
complete: boolean;
isOption: boolean;
isPersistent?: boolean;
isPath?: boolean;
isPathComplete?: boolean;
};

const cmdDelim = /(\|\|)|(&&)|(;)|(\|)/;
Expand Down
25 changes: 18 additions & 7 deletions src/runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import speclist, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
} from "@withfig/autocomplete/build/index.js";
import path from "node:path";
import { parseCommand, CommandToken } from "./parser.js";
import { getArgDrivenRecommendation, getSubcommandDrivenRecommendation } from "./suggestion.js";
import { SuggestionBlob } from "./model.js";
import { buildExecuteShellCommand } from "./utils.js";
import { buildExecuteShellCommand, resolveCwd } from "./utils.js";
import { Shell } from "../utils/shell.js";

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- recursive type, setting as any
const specSet: any = {};
Expand Down Expand Up @@ -55,7 +57,7 @@ const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise<Fig.Spe
return; //TODO: implement spec location loading
};

export const getSuggestions = async (cmd: string, cwd: string): Promise<SuggestionBlob | undefined> => {
export const getSuggestions = async (cmd: string, cwd: string, shell: Shell): Promise<SuggestionBlob | undefined> => {
const activeCmd = parseCommand(cmd);
const rootToken = activeCmd.at(0);
if (activeCmd.length === 0 || !rootToken?.complete) {
Expand All @@ -67,10 +69,19 @@ export const getSuggestions = async (cmd: string, cwd: string): Promise<Suggesti
const subcommand = getSubcommand(spec);
if (subcommand == null) return;

const result = await runSubcommand(activeCmd.slice(1), subcommand, cwd);
if (result == null) return;
const lastCommand = activeCmd.at(-1);
const charactersToDrop = lastCommand?.complete ? 0 : lastCommand?.token.length ?? 0;
const { cwd: resolvedCwd, pathy, complete: pathyComplete } = await resolveCwd(lastCommand, cwd, shell);
if (pathy && lastCommand) {
lastCommand.isPath = true;
lastCommand.isPathComplete = pathyComplete;
}
const result = await runSubcommand(activeCmd.slice(1), subcommand, resolvedCwd);
if (result == null) return;

let charactersToDrop = lastCommand?.complete ? 0 : lastCommand?.token.length ?? 0;
if (pathy) {
charactersToDrop = pathyComplete ? 0 : path.basename(lastCommand?.token ?? "").length;
}
return { ...result, charactersToDrop };
};

Expand Down Expand Up @@ -193,7 +204,7 @@ const runArg = async (
} else if (tokens.length === 0) {
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, fromVariadic, cwd);
} else if (!tokens.at(0)?.complete) {
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0].token, acceptedTokens, fromVariadic, cwd);
return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0], acceptedTokens, fromVariadic, cwd);
}

const activeToken = tokens[0];
Expand Down Expand Up @@ -240,7 +251,7 @@ const runSubcommand = async (
if (tokens.length === 0) {
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens, cwd);
} else if (!tokens.at(0)?.complete) {
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0].token, argsDepleted, argsUsed, acceptedTokens, cwd);
return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0], argsDepleted, argsUsed, acceptedTokens, cwd);
}

const activeToken = tokens[0];
Expand Down
49 changes: 38 additions & 11 deletions src/runtime/suggestion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import path from "node:path";

import { CommandToken } from "./parser.js";
import { runGenerator } from "./generator.js";
import { runTemplates } from "./template.js";
Expand Down Expand Up @@ -178,19 +180,30 @@ const optionSuggestions = (
return filter<Fig.Option>(validOptions ?? [], filterStrategy, partialCmd, "option");
};

const removeDuplicateSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => {
const removeAcceptedSuggestions = (suggestions: Suggestion[], acceptedTokens: CommandToken[]): Suggestion[] => {
const seen = new Set<string>(acceptedTokens.map((t) => t.token));
return suggestions.filter((s) => s.allNames.every((n) => !seen.has(n)));
};

const removeDuplicateSuggestion = (suggestions: Suggestion[]): Suggestion[] => {
const seen = new Set<string>();
return suggestions
.map((s) => {
if (seen.has(s.name)) return null;
seen.add(s.name);
return s;
})
.filter((s): s is Suggestion => s != null);
};

const removeEmptySuggestion = (suggestions: Suggestion[]): Suggestion[] => {
return suggestions.filter((s) => s.name.length > 0);
};

export const getSubcommandDrivenRecommendation = async (
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
partialCmd: string | undefined,
partialToken: CommandToken | undefined,
argsDepleted: boolean,
argsFromSubcommand: boolean,
acceptedTokens: CommandToken[],
Expand All @@ -199,6 +212,11 @@ export const getSubcommandDrivenRecommendation = async (
if (argsDepleted && argsFromSubcommand) {
return;
}
let partialCmd = partialToken?.token;
if (partialToken?.isPath) {
partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? "");
}

const suggestions: Suggestion[] = [];
const argLength = subcommand.args instanceof Array ? subcommand.args.length : subcommand.args ? 1 : 0;
const allOptions = persistentOptions.concat(subcommand.options ?? []);
Expand All @@ -215,10 +233,12 @@ export const getSubcommandDrivenRecommendation = async (
}

return {
suggestions: removeEmptySuggestion(
removeDuplicateSuggestions(
suggestions.sort((a, b) => b.priority - a.priority),
acceptedTokens,
suggestions: removeDuplicateSuggestion(
removeEmptySuggestion(
removeAcceptedSuggestions(
suggestions.sort((a, b) => b.priority - a.priority),
acceptedTokens,
),
),
),
};
Expand All @@ -228,11 +248,16 @@ export const getArgDrivenRecommendation = async (
args: Fig.Arg[],
subcommand: Fig.Subcommand,
persistentOptions: Fig.Option[],
partialCmd: string | undefined,
partialToken: CommandToken | undefined,
acceptedTokens: CommandToken[],
variadicArgBound: boolean,
cwd: string,
): Promise<SuggestionBlob | undefined> => {
let partialCmd = partialToken?.token;
if (partialToken?.isPath) {
partialCmd = partialToken.isPathComplete ? "" : path.basename(partialCmd ?? "");
}

const activeArg = args[0];
const allOptions = persistentOptions.concat(subcommand.options ?? []);
const suggestions = [
Expand All @@ -247,10 +272,12 @@ export const getArgDrivenRecommendation = async (
}

return {
suggestions: removeEmptySuggestion(
removeDuplicateSuggestions(
suggestions.sort((a, b) => b.priority - a.priority),
acceptedTokens,
suggestions: removeDuplicateSuggestion(
removeEmptySuggestion(
removeAcceptedSuggestions(
suggestions.sort((a, b) => b.priority - a.priority),
acceptedTokens,
),
),
),
argumentDescription: activeArg.description ?? activeArg.name,
Expand Down
14 changes: 7 additions & 7 deletions src/runtime/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@
import fsAsync from "node:fs/promises";
import log from "../utils/log.js";

const filepathsTemplate = async (cwd: string): Promise<Fig.Suggestion[]> => {
const filepathsTemplate = async (cwd: string): Promise<Fig.TemplateSuggestion[]> => {
const files = await fsAsync.readdir(cwd, { withFileTypes: true });
return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90 }));
return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "filepaths" } }));
};

const foldersTemplate = async (cwd: string): Promise<Fig.Suggestion[]> => {
const foldersTemplate = async (cwd: string): Promise<Fig.TemplateSuggestion[]> => {
const files = await fsAsync.readdir(cwd, { withFileTypes: true });
return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90 }));
return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90, context: { templateType: "folders" } }));
};

// TODO: implement history template
const historyTemplate = (): Fig.Suggestion[] => {
const historyTemplate = (): Fig.TemplateSuggestion[] => {
return [];
};

// TODO: implement help template
const helpTemplate = (): Fig.Suggestion[] => {
const helpTemplate = (): Fig.TemplateSuggestion[] => {
return [];
};

export const runTemplates = async (template: Fig.TemplateStrings[] | Fig.Template, cwd: string): Promise<Fig.Suggestion[]> => {
export const runTemplates = async (template: Fig.TemplateStrings[] | Fig.Template, cwd: string): Promise<Fig.TemplateSuggestion[]> => {
const templates = template instanceof Array ? template : [template];
return (
await Promise.all(
Expand Down
Loading

0 comments on commit 724304c

Please sign in to comment.