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 {
completionPrefix,
cursorOffset,
parseCompletionsOutput,
stripBangPrefix,
wrapNuAutocompleteProvider,
} from "../index.js";
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);
});
});