repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/nu-bang/__tests__/nu-bang.test.ts
Rawimport { join } from "node:path";
import { describe, expect, test } from "vitest";
import {
classifyBangCommand,
completionPrefix,
cursorOffset,
parseCompletionsOutput,
stripBangPrefix,
wrapNuAutocompleteProvider,
} from "../index.js";
describe("nu-bang interactive classifier", () => {
function interactive(command: string): boolean {
return classifyBangCommand(command).interactive;
}
test("forces interactive mode with the i prefix and strips it", () => {
expect(classifyBangCommand("i cargo run")).toEqual({
command: "cargo run",
interactive: true,
});
expect(classifyBangCommand("i\tcargo run").command).toBe("cargo run");
expect(classifyBangCommand("i")).toEqual({
command: "i",
interactive: false,
});
expect(interactive("into int")).toBe(false);
});
test("streams ordinary commands", () => {
for (const command of [
"ls | where size > 1kb",
"git status",
"git log --oneline",
"git rebase main",
"git add -A",
'git commit -m "msg"',
'git commit -am "msg"',
"git commit --message=msg",
"git commit --amend --no-edit",
"git commit -F notes.txt",
"open people.json | get name",
"[1 2 3] | each {|less| $less }",
"lessons | first",
"",
]) {
expect(interactive(command), command).toBe(false);
}
});
test("detects editors, pagers, and terminal UIs", () => {
for (const command of [
"vim notes.md",
"^nvim .",
" hx src/main.rs",
"less README.md",
"git log | less",
"open people.json | to json | ^less -R",
"lazygit",
"htop",
"ssh host",
"fzf",
]) {
expect(interactive(command), command).toBe(true);
}
});
test("detects editor-opening git subcommands", () => {
for (const command of [
"git commit",
"git commit -a",
"git commit --amend",
"git commit -S",
"git rebase -i HEAD~3",
"git rebase --interactive main",
"git add -p",
"git add --patch src",
"git add -i",
"git stash -p",
"git reset -p HEAD~1",
"git checkout -p",
"git restore --patch .",
"git difftool",
"git mergetool",
"^git commit",
]) {
expect(interactive(command), command).toBe(true);
}
});
});
describe("nu-bang helpers", () => {
test("strips bang prefixes", () => {
expect(stripBangPrefix("!ls")?.command).toBe("ls");
expect(stripBangPrefix("!!ls")?.command).toBe("ls");
expect(stripBangPrefix("!!ls")?.prefixLength).toBe(2);
expect(stripBangPrefix("ls")).toBeUndefined();
});
test("computes cursor offset after prefix", () => {
expect(cursorOffset(["!ls | wh"], 0, 8, 1)).toBe(7);
expect(cursorOffset(["!!ls", "| where"], 1, 7, 2)).toBe(10);
expect(cursorOffset(["!"], 0, 1, 1)).toBeUndefined();
});
test("extracts completion prefix", () => {
expect(completionPrefix("ls | wh", 7)).toBe("wh");
expect(completionPrefix("$env.PA", 7)).toBe("PA");
expect(completionPrefix("$env.", 5)).toBe("");
expect(completionPrefix("open people.j", 13)).toBe("people.j");
expect(completionPrefix("open .e", 7)).toBe(".e");
expect(completionPrefix("open my\\ f", 10)).toBe("my\\ f");
expect(completionPrefix('open "my f', 10)).toBe('"my f');
expect(completionPrefix("open 'my f", 10)).toBe("'my f");
expect(completionPrefix("open `my f", 10)).toBe("`my f");
});
test("salvages malformed completion JSON", () => {
expect(
parseCompletionsOutput('{"completions": ["PATH", ""ODD_NAME"", "HOME"]}'),
).toEqual(["PATH", "ODD_NAME", "HOME"]);
expect(
parseCompletionsOutput('{"completions": ["a,b", ""ODD_NAME""]}'),
).toEqual(["a,b", "ODD_NAME"]);
});
test("delegates non-bang autocomplete suggestions", async () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions(lines, cursorLine, cursorCol) {
expect(lines).toEqual(["/re"]);
expect(cursorLine).toBe(0);
expect(cursorCol).toBe(3);
return {
prefix: "re",
items: [{ value: "/reload", label: "reload" }],
};
},
applyCompletion() {
throw new Error("not used");
},
},
process.cwd(),
);
await expect(
provider.getSuggestions(["/re"], 0, 3, {
signal: new AbortController().signal,
}),
).resolves.toEqual({
prefix: "re",
items: [{ value: "/reload", label: "reload" }],
});
});
test("applies dotted path completions without duplicating prefix", () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion() {
throw new Error("not used");
},
},
process.cwd(),
);
const prefix = completionPrefix("open people.j", 13);
expect(
provider.applyCompletion(
["!open people.j"],
0,
14,
{ value: "people.json", label: "people.json" },
prefix,
),
).toEqual({
lines: ["!open people.json"],
cursorLine: 0,
cursorCol: 17,
});
});
test("applies quoted path completions without leaving stale quotes", () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion() {
throw new Error("not used");
},
},
process.cwd(),
);
const prefix = completionPrefix('open "my f', 10);
expect(
provider.applyCompletion(
['!open "my f'],
0,
11,
{ value: "`my file.txt`", label: "`my file.txt`" },
prefix,
),
).toEqual({
lines: ["!open `my file.txt`"],
cursorLine: 0,
cursorCol: 19,
});
});
test("removes existing closing quote when applying quoted path completions", () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion() {
throw new Error("not used");
},
},
process.cwd(),
);
expect(
provider.applyCompletion(
['!open "my f"'],
0,
11,
{ value: "`my file.txt`", label: "`my file.txt`" },
completionPrefix('open "my f', 10),
),
).toEqual({
lines: ["!open `my file.txt`"],
cursorLine: 0,
cursorCol: 19,
});
expect(
provider.applyCompletion(
["!open 'my f'"],
0,
11,
{ value: "`my file.txt`", label: "`my file.txt`" },
completionPrefix("open 'my f", 10),
),
).toEqual({
lines: ["!open `my file.txt`"],
cursorLine: 0,
cursorCol: 19,
});
expect(
provider.applyCompletion(
["!open `my f`"],
0,
11,
{ value: "`my file.txt`", label: "`my file.txt`" },
completionPrefix("open `my f", 10),
),
).toEqual({
lines: ["!open `my file.txt`"],
cursorLine: 0,
cursorCol: 19,
});
});
test("applies escaped-space path completions without duplicating prefix", () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion() {
throw new Error("not used");
},
},
process.cwd(),
);
const prefix = completionPrefix("open my\\ f", 10);
expect(
provider.applyCompletion(
["!open my\\ f"],
0,
11,
{ value: "`my file.txt`", label: "`my file.txt`" },
prefix,
),
).toEqual({
lines: ["!open `my file.txt`"],
cursorLine: 0,
cursorCol: 19,
});
});
test("delegates non-bang completion application", () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
expect(lines).toEqual(["/re"]);
expect(cursorLine).toBe(0);
expect(cursorCol).toBe(3);
expect(item.value).toBe("/reload");
expect(prefix).toBe("re");
return { lines: ["/reload"], cursorLine: 0, cursorCol: 7 };
},
},
process.cwd(),
);
expect(
provider.applyCompletion(
["/re"],
0,
3,
{ value: "/reload", label: "reload" },
"re",
),
).toEqual({
lines: ["/reload"],
cursorLine: 0,
cursorCol: 7,
});
});
test("delegates fallback-origin bang completion application", async () => {
let applyCalls = 0;
const fallbackItem = { value: "/reload", label: "reload" };
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions(lines, cursorLine, cursorCol) {
expect(lines).toEqual(["!bad"]);
expect(cursorLine).toBe(0);
expect(cursorCol).toBe(4);
return { prefix: "/re", items: [fallbackItem] };
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
applyCalls++;
expect(lines).toEqual(["!bad"]);
expect(cursorLine).toBe(0);
expect(cursorCol).toBe(4);
expect(item).toBe(fallbackItem);
expect(prefix).toBe("/re");
return { lines: ["!/reload"], cursorLine: 0, cursorCol: 8 };
},
},
join(process.cwd(), "missing-nu-bang-cwd"),
);
const suggestions = await provider.getSuggestions(["!bad"], 0, 4, {
signal: new AbortController().signal,
});
expect(suggestions?.items[0]).toBe(fallbackItem);
expect(
provider.applyCompletion(
["!bad"],
0,
4,
fallbackItem,
suggestions?.prefix ?? "",
),
).toEqual({
lines: ["!/reload"],
cursorLine: 0,
cursorCol: 8,
});
expect(applyCalls).toBe(1);
});
test("does not invoke fallback suggestions for already-aborted bang completion failures", async () => {
let fallbackCalls = 0;
const controller = new AbortController();
controller.abort();
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
fallbackCalls++;
return {
prefix: "x",
items: [{ value: "fallback", label: "fallback" }],
};
},
applyCompletion() {
throw new Error("not used");
},
},
join(process.cwd(), "missing-nu-bang-cwd"),
);
await expect(
provider.getSuggestions(["!bad"], 0, 4, { signal: controller.signal }),
).resolves.toBeNull();
expect(fallbackCalls).toBe(0);
});
test("allows tab completion for bang input", () => {
const provider = wrapNuAutocompleteProvider(
{
async getSuggestions() {
return null;
},
applyCompletion() {
throw new Error("not used");
},
shouldTriggerFileCompletion(lines) {
return lines[0] === "@";
},
},
process.cwd(),
);
expect(provider.shouldTriggerFileCompletion?.(["!open "], 0, 6)).toBe(true);
expect(provider.shouldTriggerFileCompletion?.(["@"], 0, 1)).toBe(true);
expect(provider.shouldTriggerFileCompletion?.(["nope"], 0, 4)).toBe(false);
});
});