Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/nu-bang/__tests__/nu-bang.test.ts

Raw
import { 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);
	});
});