Skip to content

Commit

Permalink
feat: add proper tilda expansion (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
grant0417 authored Nov 11, 2023
1 parent d9cd128 commit 4b8fd73
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 61 deletions.
27 changes: 7 additions & 20 deletions generators/src/filepaths.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ensureTrailingSlash, shellExpand } from "./resolve";

export interface FilepathsOptions {
/**
* Show suggestions with any of these extensions. Do not include the leading dot.
Expand Down Expand Up @@ -48,8 +50,6 @@ export function sortFilesAlphabetically(array: string[], skip: string[] = []): s
];
}

const ensureTrailingSlash = (str: string) => (str.endsWith("/") ? str : `${str}/`);

/**
* @param cwd - The current working directory when the user started typing the new path
* @param searchTerm - The path inserted by the user, it can be relative to cwd or absolute
Expand All @@ -58,31 +58,19 @@ const ensureTrailingSlash = (str: string) => (str.endsWith("/") ? str : `${str}/
export const getCurrentInsertedDirectory = (
cwd: string | null,
searchTerm: string,
environmentVariables: Record<string, string> = {}
context: Fig.ShellContext
): string => {
if (cwd === null) return "/";

// Replace simple $VAR variables
const resolvedSimpleVariables = searchTerm.replace(/\$([A-Za-z0-9_]+)/g, (key) => {
const envKey = key.slice(1);
return environmentVariables[envKey] || "";
});

// Replace complex ${VAR} variables
const resolvedComplexVariables = resolvedSimpleVariables.replace(
/\$\{([A-Za-z0-9_]+)(?::-([^}]+))?\}/g,
(_, envKey, defaultValue) => environmentVariables[envKey] ?? defaultValue ?? ""
);
const resolvedPath = shellExpand(searchTerm, context);

const dirname = resolvedComplexVariables.slice(0, resolvedComplexVariables.lastIndexOf("/") + 1);
const dirname = resolvedPath.slice(0, resolvedPath.lastIndexOf("/") + 1);

if (dirname === "") {
return ensureTrailingSlash(cwd);
}

return dirname.startsWith("~/") || dirname.startsWith("/")
? dirname
: `${ensureTrailingSlash(cwd)}${dirname}`;
return dirname.startsWith("/") ? dirname : `${ensureTrailingSlash(cwd)}${dirname}`;
};

/**
Expand Down Expand Up @@ -182,11 +170,10 @@ function filepathsFn(options: FilepathsOptions = {}): Fig.Generator {
getCurrentInsertedDirectory(
rootDirectory ?? currentWorkingDirectory,
searchTerm,
generatorContext.environmentVariables
generatorContext
) ?? "/";

try {
// Use \ls command to avoid any aliases set for ls.
const data = await executeCommand({
command: "ls",
args: ["-1ApL"],
Expand Down
32 changes: 32 additions & 0 deletions generators/src/resolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const ensureTrailingSlash = (str: string) => (str.endsWith("/") ? str : `${str}/`);

const replaceTilde = (path: string, homeDir: string) => {
if (path.startsWith("~") && (path.length === 1 || path.charAt(1) === "/")) {
return path.replace("~", homeDir);
}
return path;
};

const replaceVariables = (path: string, environmentVariables: Record<string, string>) => {
// Replace simple $VAR variables
const resolvedSimpleVariables = path.replace(/\$([A-Za-z0-9_]+)/g, (key) => {
const envKey = key.slice(1);
return environmentVariables[envKey] ?? key;
});

// Replace complex ${VAR} variables
const resolvedComplexVariables = resolvedSimpleVariables.replace(
/\$\{([A-Za-z0-9_]+)(?::-([^}]+))?\}/g,
(match, envKey, defaultValue) => environmentVariables[envKey] ?? defaultValue ?? match
);

return resolvedComplexVariables;
};

export const shellExpand = (path: string, context: Fig.ShellContext): string => {
const { environmentVariables } = context;
return replaceVariables(
replaceTilde(path, environmentVariables?.HOME ?? "~"),
environmentVariables
);
};
103 changes: 62 additions & 41 deletions generators/test/filepaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,103 +12,124 @@ function toName(suggestion: Fig.Suggestion): string {
return suggestion.name as string;
}

const defaultHome = "/home/user";
const defaultCwd = `${defaultHome}/current_cwd`;

const defaultContext: Fig.GeneratorContext = {
searchTerm: "",
currentWorkingDirectory: "~/current_cwd/",
currentWorkingDirectory: defaultCwd,
currentProcess: "zsh",
sshPrefix: "",
environmentVariables: {
HOME: "/home/user",
HOME: defaultHome,
},
};

const defaultContextWithEnv = (env: Record<string, string>) => ({
...defaultContext,
environmentVariables: {
...defaultContext.environmentVariables,
...env,
},
});

describe("Test getCurrentInsertedDirectory", () => {
it("returns root for null cwd", () => {
expect(getCurrentInsertedDirectory(null, "foo/")).to.equal("/");
expect(getCurrentInsertedDirectory(null, "foo/", defaultContext)).to.equal("/");
});

it("returns merged path when both cwd and search term are specified", () => {
expect(getCurrentInsertedDirectory("~/current_cwd", "test/")).to.equal("~/current_cwd/test/");
expect(getCurrentInsertedDirectory(defaultCwd, "test/", defaultContext)).to.equal(
`${defaultCwd}/test/`
);
});

it("returns partial path when trailing slash is missing (1)", () => {
expect(getCurrentInsertedDirectory("~/current_cwd", "src/packages")).to.equal(
"~/current_cwd/src/"
expect(getCurrentInsertedDirectory(defaultCwd, "src/packages", defaultContext)).to.equal(
`${defaultCwd}/src/`
);
});

it("returns partial path when trailing slash is missing (2)", () => {
expect(getCurrentInsertedDirectory("~/current_cwd", "src")).to.equal("~/current_cwd/");
expect(getCurrentInsertedDirectory(defaultCwd, "src", defaultContext)).to.equal(
`${defaultCwd}/`
);
});

it("returns the entire search term if it is an absolute path relative to ~", () => {
expect(getCurrentInsertedDirectory("~/current_cwd", "~/some_dir/src/test/")).to.equal(
"~/some_dir/src/test/"
);
expect(
getCurrentInsertedDirectory(defaultCwd, "~/some_dir/src/test/", defaultContext)
).to.equal(`${defaultHome}/some_dir/src/test/`);
});

it("returns the entire search term if it is an absolute path relative to /", () => {
expect(getCurrentInsertedDirectory("~/current_cwd", "/etc/bin/tool")).to.equal("/etc/bin/");
expect(getCurrentInsertedDirectory(defaultCwd, "/etc/bin/tool", defaultContext)).to.equal(
"/etc/bin/"
);
});

it("returns the path with $HOME resolved", () => {
expect(
getCurrentInsertedDirectory("~/current_cwd", "$HOME/src/test/", {
HOME: "/home/user",
})
).to.equal("/home/user/src/test/");
expect(getCurrentInsertedDirectory(defaultCwd, "$HOME/src/test/", defaultContext)).to.equal(
`${defaultHome}/src/test/`
);
});

it("returns the path with $DIR resolved", () => {
expect(
getCurrentInsertedDirectory("~/current_cwd", "$DIR/src/test/", {
DIR: "/tmp/folder",
})
getCurrentInsertedDirectory(
defaultCwd,
"$DIR/src/test/",
defaultContextWithEnv({
DIR: "/tmp/folder",
})
)
).to.equal("/tmp/folder/src/test/");
});

it("returns the path with $DIR and $HOME resolved", () => {
expect(
getCurrentInsertedDirectory("~/current_cwd", "$HOME/src/$DIR/test/", {
DIR: "tmp/folder",
HOME: "/home/user",
})
).to.equal("/home/user/src/tmp/folder/test/");
getCurrentInsertedDirectory(
defaultCwd,
"$HOME/src/$DIR/test/",
defaultContextWithEnv({
DIR: "tmp/folder",
})
)
).to.equal(`${defaultHome}/src/tmp/folder/test/`);
});

it("returns the path when just $HOME is specified", () => {
expect(
getCurrentInsertedDirectory("~/current_cwd", "$HOME", {
HOME: "/home/user",
})
).to.equal("/home/");
expect(getCurrentInsertedDirectory(defaultHome, "$HOME", defaultContext)).to.equal("/home/");
});

it("returns the path when $DIR is not specified", () => {
expect(getCurrentInsertedDirectory("~/current_cwd", "$DIR")).to.equal("~/current_cwd/");
expect(getCurrentInsertedDirectory(defaultCwd, "$DIR", defaultContext)).to.equal(
`${defaultCwd}/`
);
});

it("returns the path with the complex $DIR and $HOME resolved", () => {
expect(
// eslint-disable-next-line no-template-curly-in-string
getCurrentInsertedDirectory("~/current_cwd", "${HOME}/src/${DIR}/test/", {
HOME: "/home/user",
DIR: "tmp/folder",
})
).to.equal("/home/user/src/tmp/folder/test/");
getCurrentInsertedDirectory(
defaultCwd,
// eslint-disable-next-line no-template-curly-in-string
"${HOME}/src/${DIR}/test/",
defaultContextWithEnv({
DIR: "tmp/folder",
})
)
).to.equal(`${defaultHome}/src/tmp/folder/test/`);
});

it("returns the path with the complex $DIR and $HOME resolved with default", () => {
expect(
getCurrentInsertedDirectory(
"~/current_cwd",
defaultCwd,
// eslint-disable-next-line no-template-curly-in-string
"${HOME:-/fallback}/src/${DIR:-fallback}/test/",
{
HOME: "/home/user",
}
defaultContext
)
).to.equal("/home/user/src/fallback/test/");
).to.equal(`${defaultHome}/src/fallback/test/`);
});
});

Expand Down

0 comments on commit 4b8fd73

Please sign in to comment.