mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
505 lines
21 KiB
TypeScript
505 lines
21 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||
import { EditorState, Text } from "@codemirror/state";
|
||
import { EditorView } from "@codemirror/view";
|
||
import { history, undo } from "@codemirror/commands";
|
||
import type { MessageRequest } from "@/workbench/messages";
|
||
import type { LocalSymbol, Location, Locals, OutlineItem, Pos, Range, StructuralRanges, SyntaxNode } from "@/bindings";
|
||
|
||
// --- backend fakes ---------------------------------------------------------
|
||
|
||
type Answer<T> = () => { status: "ok"; data: T } | { status: "error"; error: { kind: string; message: string } };
|
||
const ok =
|
||
<T>(data: T) =>
|
||
() => ({ status: "ok" as const, data });
|
||
const unavailable = () => ({ status: "error" as const, error: { kind: "unavailable", message: "no server" } });
|
||
|
||
let goto: Answer<Location[]> = ok([]);
|
||
let outline: Answer<OutlineItem[]> = ok([]);
|
||
/** Called with the position searched from. */
|
||
let references: (pos: Pos) => ReturnType<Answer<Location[]>> = ok([]);
|
||
const lspGoto = vi.fn(async () => goto());
|
||
const docOutline = vi.fn(async () => outline());
|
||
const lspReferences = vi.fn(async (_request: number, _doc: number, pos: Pos) => {
|
||
const r = references(pos);
|
||
return r.status === "ok" ? { status: "ok" as const, data: { locations: r.data, incomplete: null } } : r;
|
||
});
|
||
const lspCodeActions = vi.fn(async () => ({ status: "ok", data: [] }));
|
||
const lspApplyCodeAction = vi.fn(async () => ({ status: "ok", data: null }));
|
||
const docOpen = vi.fn(async (path: string) => ({ status: "ok", data: { id: 99, path } }));
|
||
const docClose = vi.fn(async () => ({ status: "ok", data: null }));
|
||
const languages = vi.fn(async () => [
|
||
{ id: "rust", name: "Rust", lineComment: "//", blockComment: ["/*", "*/"] },
|
||
{ id: "python", name: "Python", lineComment: "#", blockComment: null },
|
||
]);
|
||
|
||
vi.mock("@/platform/ipc", async (orig) => {
|
||
const actual = await orig<typeof import("@/platform/ipc")>();
|
||
return {
|
||
...actual,
|
||
isTauri: () => true,
|
||
commands: {
|
||
...actual.commands,
|
||
lspGoto,
|
||
docOutline,
|
||
lspReferences,
|
||
lspCodeActions,
|
||
lspApplyCodeAction,
|
||
docOpen,
|
||
docClose,
|
||
languages,
|
||
},
|
||
};
|
||
});
|
||
|
||
let locals: Locals = { status: "noIdentifier" };
|
||
let statement: SyntaxNode | null = null;
|
||
let identifiers: { range: Range; qualified: boolean }[] = [];
|
||
vi.mock("@/editor/structure", () => ({
|
||
structure: {
|
||
locals: vi.fn(async () => locals),
|
||
structuralRanges: vi.fn(async (): Promise<Partial<StructuralRanges>> => ({ statement })),
|
||
identifiers: vi.fn(async () => identifiers),
|
||
},
|
||
}));
|
||
const showHint = vi.fn();
|
||
vi.mock("@/editor/lsp/hint", () => ({ showHint }));
|
||
const executeAction = vi.fn();
|
||
vi.mock("@/platform/actions", () => ({ executeAction }));
|
||
vi.mock("@tauri-apps/api/core", async (orig) => ({
|
||
...(await orig<object>()),
|
||
Channel: class {
|
||
onmessage = () => {};
|
||
},
|
||
}));
|
||
let answer = "cancel";
|
||
const ask = vi.fn(async (_request: MessageRequest) => ({ button: answer, checked: false }));
|
||
let confirmAnswer: { checked: boolean } | null = { checked: false };
|
||
const confirmChecked = vi.fn(
|
||
async (_opts: { title: string; message: string; checkbox?: string; checked?: boolean }) => confirmAnswer,
|
||
);
|
||
vi.mock("@/workbench/messages", () => ({ ask, confirmChecked }));
|
||
|
||
const { deletionRange, localDeclaration, outlineItemAt, safeDelete, fileUsages, usagesMessage, textOccurrences } =
|
||
await import("./safeDelete");
|
||
const { closePopup, useLspPopup } = await import("@/editor/lsp/popupStore");
|
||
const { lspDoc } = await import("@/editor/lsp/lsp");
|
||
|
||
// --- helpers ----------------------------------------------------------------
|
||
|
||
const doc = (s: string) => Text.of(s.split("\n"));
|
||
const posOf = (text: string, off: number) => {
|
||
const line = doc(text).lineAt(off);
|
||
return { line: line.number - 1, col: off - line.from };
|
||
};
|
||
const rangeOf = (text: string, from: number, to: number): Range => ({ start: posOf(text, from), end: posOf(text, to) });
|
||
/** Removes `deletionRange` of the `needle` span from `text`. */
|
||
function cut(text: string, needle: string, comments?: { line: string | null; block: [string, string] | null }) {
|
||
const from = text.indexOf(needle);
|
||
const r = deletionRange(doc(text), from, from + needle.length, comments);
|
||
return r && text.slice(0, r.from) + text.slice(r.to);
|
||
}
|
||
|
||
let view: EditorView | null = null;
|
||
function editor(text: string, caretNeedle: string, language = "rust") {
|
||
view = new EditorView({
|
||
state: EditorState.create({
|
||
doc: text,
|
||
selection: { anchor: text.indexOf(caretNeedle) + 1 },
|
||
extensions: [history(), lspDoc.of({ doc: 1, path: "/p/a.rs", language, uiMode: null, readOnly: false })],
|
||
}),
|
||
});
|
||
return view;
|
||
}
|
||
|
||
function item(text: string, name: string, decl: string, kind: OutlineItem["kind"] = "function"): OutlineItem {
|
||
const from = text.indexOf(decl);
|
||
const at = text.indexOf(name, from);
|
||
return {
|
||
name,
|
||
detail: null,
|
||
kind,
|
||
range: rangeOf(text, from, from + decl.length),
|
||
selection_range: rangeOf(text, at, at + name.length),
|
||
children: [],
|
||
};
|
||
}
|
||
|
||
beforeEach(() => {
|
||
goto = ok([]);
|
||
outline = ok([]);
|
||
references = ok([]);
|
||
locals = { status: "noIdentifier" };
|
||
statement = null;
|
||
identifiers = [];
|
||
confirmAnswer = { checked: false };
|
||
});
|
||
|
||
afterEach(() => {
|
||
view?.destroy();
|
||
view = null;
|
||
closePopup();
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
// --- tests --------------------------------------------------------------------
|
||
|
||
describe("deletionRange", () => {
|
||
it("removes whole lines plus the trailing blank line", () => {
|
||
expect(cut("fn a() {}\n\nfn unused() {}\n\nfn b() {}\n", "fn unused() {}")).toBe("fn a() {}\n\nfn b() {}\n");
|
||
});
|
||
|
||
it("takes the doc comment and annotations directly above", () => {
|
||
const text = "fn a() {}\n\n/**\n * Docs.\n */\n#[inline]\nfn unused() {}\n\nfn b() {}\n";
|
||
expect(cut(text, "fn unused() {}")).toBe("fn a() {}\n\nfn b() {}\n");
|
||
const java = "class A {\n // keep\n\n /// doc\n @Deprecated\n void unused() {}\n\n void b() {}\n}\n";
|
||
expect(cut(java, "void unused() {}")).toBe("class A {\n // keep\n\n void b() {}\n}\n");
|
||
});
|
||
|
||
it("takes the blank line above when the declaration ends its block", () => {
|
||
expect(cut("fn f() {\n a();\n\n let x = 1;\n}\n", "let x = 1;")).toBe("fn f() {\n a();\n}\n");
|
||
});
|
||
|
||
it("handles the last line of the document", () => {
|
||
expect(cut("fn a() {}\n\nfn unused() {}", "fn unused() {}")).toBe("fn a() {}");
|
||
expect(cut("fn a() {}\nfn unused() {}", "fn unused() {}")).toBe("fn a() {}");
|
||
});
|
||
|
||
it("uses the language's comment syntax (Python comments and decorators)", () => {
|
||
const py = "import x\n\n# helper\n@cache\ndef unused():\n pass\n\ndef b():\n pass\n";
|
||
expect(cut(py, "def unused():\n pass", { line: "#", block: null })).toBe("import x\n\ndef b():\n pass\n");
|
||
});
|
||
|
||
it("accepts a trailing separator or comment but refuses shared lines", () => {
|
||
expect(cut("let a = 1;\nlet b = 2; // note\nlet c = 3;\n", "let b = 2")).toBe("let a = 1;\nlet c = 3;\n");
|
||
expect(cut("struct S {\n a: i32,\n b: i32,\n}\n", "a: i32")).toBe("struct S {\n b: i32,\n}\n");
|
||
expect(cut("let a = 1; let b = 2;\n", "let b = 2;")).toBeNull();
|
||
expect(cut("f(a, b);\n", "a")).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("outlineItemAt", () => {
|
||
it("finds the innermost item whose name contains the position", () => {
|
||
const text = "impl S {\n fn m() {}\n}\n";
|
||
const m = item(text, "m", "fn m() {}", "method");
|
||
const s = { ...item(text, "S", "impl S {\n fn m() {}\n}", "struct"), children: [m] };
|
||
expect(outlineItemAt([s], posOf(text, text.indexOf("m()")))).toBe(m);
|
||
expect(outlineItemAt([s], posOf(text, text.indexOf("S")))).toBe(s);
|
||
expect(outlineItemAt([s], posOf(text, text.indexOf("{}")))).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("localDeclaration", () => {
|
||
const text =
|
||
"fn f(p: i32) {\n let x = 1;\n let (a, b) = t;\n let m: HashMap<K, V> = m();\n for i in v {}\n}\n";
|
||
const sym = (name: string, kind: LocalSymbol["kind"] = "var", after = "{"): LocalSymbol => {
|
||
const at = text.indexOf(` ${name}`, text.indexOf(after)) + 1;
|
||
return {
|
||
name,
|
||
kind,
|
||
definition: rangeOf(text, at, at + name.length),
|
||
scope: rangeOf(text, 0, text.length),
|
||
references: [],
|
||
certain: true,
|
||
conflicts: [],
|
||
};
|
||
};
|
||
const stmt = (kind: string, needle: string): SyntaxNode => {
|
||
const at = text.indexOf(needle);
|
||
return {
|
||
kind,
|
||
range: rangeOf(text, at, at + needle.length),
|
||
field: null,
|
||
named: true,
|
||
error: false,
|
||
missing: false,
|
||
hasError: false,
|
||
};
|
||
};
|
||
|
||
it("returns the declaring statement of a single local", () => {
|
||
expect(localDeclaration(doc(text), sym("x"), stmt("let_declaration", "let x = 1;"))).toEqual(
|
||
stmt("let_declaration", "let x = 1;").range,
|
||
);
|
||
expect(
|
||
localDeclaration(doc(text), sym("m"), stmt("let_declaration", "let m: HashMap<K, V> = m();")),
|
||
).not.toBeTypeOf("string");
|
||
});
|
||
|
||
it("refuses parameters, destructuring, loop variables and ambiguous locals", () => {
|
||
expect(localDeclaration(doc(text), sym("p", "parameter", "f"), null)).toMatch(/Change Signature/);
|
||
expect(localDeclaration(doc(text), sym("b"), stmt("let_declaration", "let (a, b) = t;"))).toMatch(/other names/);
|
||
expect(localDeclaration(doc(text), sym("i"), stmt("for_expression", "for i in v {}"))).toMatch(
|
||
/separate statement/,
|
||
);
|
||
expect(localDeclaration(doc(text), { ...sym("x"), certain: false }, stmt("let_declaration", "let x = 1;"))).toMatch(
|
||
/more than once/,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("textOccurrences", () => {
|
||
it("checks Unicode identifier boundaries without shifting UTF-16 offsets", () => {
|
||
const text = "𐐀foo αfoo $foo _foo foo 𐐀 foo foo𐐀";
|
||
const hits = textOccurrences(text, "foo", [], { from: -1, to: -1 });
|
||
expect(hits).toEqual([21, 28].map((from) => ({ from, to: from + 3 })));
|
||
});
|
||
|
||
it("finds whole-word mentions that are not identifier tokens, outside the declaration", () => {
|
||
const text = 'foo(); // foo and food\nlet s = "foo";\nfn foo() {}';
|
||
const ids = [0, text.lastIndexOf("foo")].map((from) => ({ from, to: from + 3 }));
|
||
const decl = text.indexOf("fn foo");
|
||
const hits = textOccurrences(text, "foo", ids, { from: decl, to: text.length });
|
||
expect(hits.map((h) => text.slice(h.from - 3, h.to))).toEqual(["// foo", '= "foo']);
|
||
});
|
||
});
|
||
|
||
describe("usagesMessage", () => {
|
||
it("says how many usages and where", () => {
|
||
const loc = (path: string): Location => ({ path, range: rangeOf("x", 0, 1) });
|
||
expect(usagesMessage("Function 'f'", [loc("/a")])).toEqual({
|
||
message: "1 usage found",
|
||
detail: "Function 'f' has 1 usage that is not safe to delete.",
|
||
});
|
||
expect(usagesMessage("The 2 files", [loc("/a"), loc("/b")], true).detail).toBe(
|
||
"The 2 files have 2 usages that are not safe to delete (in 2 files).",
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("safeDelete in the editor", () => {
|
||
const text = "fn used() {}\n\nfn unused() {\n unused();\n}\n\nfn main() {}\n";
|
||
|
||
it("deletes an unused declaration as one undo step, ignoring recursive calls", async () => {
|
||
const v = editor(text, "unused() {");
|
||
outline = ok([item(text, "used", "fn used() {}"), item(text, "unused", "fn unused() {\n unused();\n}")]);
|
||
const self = text.indexOf("unused();");
|
||
references = ok([{ path: "/p/a.rs", range: rangeOf(text, self, self + 6) }]);
|
||
await safeDelete(v);
|
||
expect(v.state.doc.toString()).toBe("fn used() {}\n\nfn main() {}\n");
|
||
expect(lspCodeActions).not.toHaveBeenCalled();
|
||
expect(lspApplyCodeAction).not.toHaveBeenCalled();
|
||
undo(v);
|
||
expect(v.state.doc.toString()).toBe(text);
|
||
});
|
||
|
||
it("asks when usages exist: View Usages / Delete Anyway / Cancel", async () => {
|
||
const usage: Location = { path: "/p/b.rs", range: { start: { line: 3, col: 4 }, end: { line: 3, col: 8 } } };
|
||
outline = ok([item(text, "used", "fn used() {}")]);
|
||
references = ok([usage]);
|
||
|
||
const run = async (button: string) => {
|
||
const v = editor(text, "used() {}");
|
||
answer = button;
|
||
ask.mockClear();
|
||
await safeDelete(v);
|
||
const request = ask.mock.calls[0]![0];
|
||
expect(request.title).toBe("Usages Detected");
|
||
expect(request.body).toBe("1 usage found\nFunction 'used' has 1 usage that is not safe to delete.");
|
||
expect(request.buttons.map((b) => b.label)).toEqual(["View Usages", "Delete Anyway"]);
|
||
return v;
|
||
};
|
||
|
||
expect((await run("cancel")).state.doc.toString()).toBe(text);
|
||
expect(executeAction).not.toHaveBeenCalled();
|
||
expect((await run("view")).state.doc.toString()).toBe(text);
|
||
expect(executeAction).toHaveBeenCalledWith("find.showUsageResults", { symbol: "used", locations: [usage] });
|
||
expect((await run("delete")).state.doc.toString()).toBe("fn unused() {\n unused();\n}\n\nfn main() {}\n");
|
||
});
|
||
|
||
it("confirms first ('Delete Function …?') and searches nothing when cancelled", async () => {
|
||
const v = editor(text, "unused() {");
|
||
outline = ok([item(text, "unused", "fn unused() {\n unused();\n}")]);
|
||
confirmAnswer = null;
|
||
await safeDelete(v);
|
||
expect(confirmChecked).toHaveBeenCalledTimes(1);
|
||
const opts = confirmChecked.mock.calls[0]![0];
|
||
expect(opts.title).toBe("Safe Delete");
|
||
expect(opts.message).toBe("Delete Function 'unused'?");
|
||
expect(opts.checkbox).toBe("Search in comments and strings");
|
||
expect(lspReferences).not.toHaveBeenCalled();
|
||
expect(v.state.doc.toString()).toBe(text);
|
||
});
|
||
|
||
it("with 'Search in comments and strings' previews mentions before deleting", async () => {
|
||
const src = "// calls unused\nfn unused() {}\n\nfn main() {}\n";
|
||
const v = editor(src, "unused() {");
|
||
outline = ok([item(src, "unused", "fn unused() {}")]);
|
||
const decl = src.indexOf("unused() {");
|
||
identifiers = [{ range: rangeOf(src, decl, decl + 6), qualified: false }];
|
||
confirmAnswer = { checked: true };
|
||
await safeDelete(v);
|
||
const popup = useLspPopup.getState().popup;
|
||
expect(popup?.kind).toBe("preview");
|
||
if (popup?.kind !== "preview") return;
|
||
expect(popup.occurrences).toEqual([{ line: 0, text: "// calls unused", from: 9, to: 15 }]);
|
||
expect(v.state.doc.toString()).toBe(src);
|
||
popup.onConfirm();
|
||
expect(v.state.doc.toString()).not.toContain("fn unused");
|
||
});
|
||
|
||
it("refuses a declaration in another file instead of doing something else", async () => {
|
||
const v = editor(text, "main");
|
||
goto = ok([{ path: "/p/other.rs", range: rangeOf(text, 0, 1) }]);
|
||
await safeDelete(v);
|
||
expect(showHint).toHaveBeenCalledWith(
|
||
v,
|
||
"Cannot perform refactoring. 'main' is declared in other.rs; Safe Delete it there",
|
||
);
|
||
expect(v.state.doc.toString()).toBe(text);
|
||
});
|
||
|
||
it("without a language server deletes only locals, using the syntax references", async () => {
|
||
const body = "fn f() {\n let x = 1;\n let y = 2;\n g(y);\n}\n";
|
||
goto = unavailable;
|
||
references = unavailable;
|
||
const x = body.indexOf("x");
|
||
locals = {
|
||
status: "local",
|
||
symbol: {
|
||
name: "x",
|
||
kind: "var",
|
||
definition: rangeOf(body, x, x + 1),
|
||
scope: rangeOf(body, 0, body.length),
|
||
references: [],
|
||
certain: true,
|
||
conflicts: [],
|
||
},
|
||
};
|
||
const at = body.indexOf("let x");
|
||
statement = {
|
||
kind: "let_declaration",
|
||
range: rangeOf(body, at, at + 10),
|
||
field: null,
|
||
named: true,
|
||
error: false,
|
||
missing: false,
|
||
hasError: false,
|
||
};
|
||
const v = editor(body, " x =");
|
||
v.dispatch({ selection: { anchor: x } });
|
||
await safeDelete(v);
|
||
expect(v.state.doc.toString()).toBe("fn f() {\n let y = 2;\n g(y);\n}\n");
|
||
expect(lspReferences).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it("without a language server refuses top-level declarations with the reason", async () => {
|
||
goto = unavailable;
|
||
outline = ok([item(text, "used", "fn used() {}")]);
|
||
locals = { status: "notLocal", name: "used", range: rangeOf(text, 3, 7), reason: "topLevel" };
|
||
const v = editor(text, "used() {}");
|
||
await safeDelete(v);
|
||
expect(showHint).toHaveBeenCalledWith(
|
||
v,
|
||
"Cannot perform refactoring. The language server for Rust is not available; only local declarations can be deleted safely without it",
|
||
);
|
||
expect(v.state.doc.toString()).toBe(text);
|
||
});
|
||
|
||
it("needs a symbol at the caret", async () => {
|
||
const v = editor("fn a() {}\n\n\n", "\n\n");
|
||
v.dispatch({ selection: { anchor: 11 } });
|
||
await safeDelete(v);
|
||
expect(showHint).toHaveBeenCalledWith(v, "Cannot perform refactoring. Caret should be on a symbol");
|
||
});
|
||
});
|
||
|
||
describe("fileUsages", () => {
|
||
const text = "pub fn a() {}\npub struct B;\n";
|
||
|
||
it("collects references to top-level symbols from other files", async () => {
|
||
outline = ok([item(text, "a", "pub fn a() {}"), item(text, "B", "pub struct B;", "struct")]);
|
||
const r = (path: string, line: number): Location => ({
|
||
path,
|
||
range: { start: { line, col: 0 }, end: { line, col: 1 } },
|
||
});
|
||
references = ok([r("/p/a.rs", 0), r("/p/main.rs", 4), r("/p/main.rs", 4), r("/p/gone.rs", 1)]);
|
||
const res = await fileUsages(["/p/a.rs", "/p/gone.rs"]);
|
||
expect(res).toEqual({ status: "ok", usages: [r("/p/main.rs", 4)] });
|
||
expect(docClose).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
// #1656: packages and namespaces are shared with other files; their members are what the file declares.
|
||
describe("searches members of packages and namespaces, not the container", () => {
|
||
const at = (path: string, line: number): Location => ({
|
||
path,
|
||
range: { start: { line, col: 0 }, end: { line, col: 1 } },
|
||
});
|
||
const within = (parent: OutlineItem, ...children: OutlineItem[]): OutlineItem => ({ ...parent, children });
|
||
/** References per searched name; `searched` records the order. */
|
||
function serve(text: string, refs: Record<string, Location[]>) {
|
||
const searched: string[] = [];
|
||
references = (pos) => {
|
||
const off = doc(text).line(pos.line + 1).from + pos.col;
|
||
const name = Object.keys(refs).find((n) => text.startsWith(n, off)) ?? "?";
|
||
searched.push(name);
|
||
return { status: "ok", data: refs[name] ?? [] };
|
||
};
|
||
return searched;
|
||
}
|
||
|
||
it("Java: the package's other classes imported elsewhere are not usages", async () => {
|
||
const text = "package com.example.config;\n\npublic class MessageSourceConfig {}\n";
|
||
outline = ok([
|
||
item(text, "com.example.config", "package com.example.config;", "package"),
|
||
item(text, "MessageSourceConfig", "public class MessageSourceConfig {}", "class"),
|
||
]);
|
||
// References to the package: an import of its sibling class CommunicationConfig.
|
||
const searched = serve(text, {
|
||
"com.example.config": [at("/p/server/CommandSender.java", 2)],
|
||
MessageSourceConfig: [],
|
||
});
|
||
expect(await fileUsages(["/p/config/MessageSourceConfig.java"])).toEqual({ status: "ok", usages: [] });
|
||
expect(searched).toEqual(["MessageSourceConfig"]);
|
||
});
|
||
|
||
it("C++: a class inside a namespace is searched", async () => {
|
||
const text = "namespace config {\nclass Used {};\nnamespace detail {\nint helper();\n}\n}\n";
|
||
const ns = item(
|
||
text,
|
||
"config",
|
||
"namespace config {\nclass Used {};\nnamespace detail {\nint helper();\n}\n}",
|
||
"namespace",
|
||
);
|
||
const detail = item(text, "detail", "namespace detail {\nint helper();\n}", "namespace");
|
||
outline = ok([
|
||
within(
|
||
ns,
|
||
item(text, "Used", "class Used {};", "class"),
|
||
within(detail, item(text, "helper", "int helper();")),
|
||
),
|
||
]);
|
||
const searched = serve(text, { Used: [at("/p/main.cpp", 4)], helper: [at("/p/main.cpp", 5)] });
|
||
expect(await fileUsages(["/p/config/used.h"])).toEqual({
|
||
status: "ok",
|
||
usages: [at("/p/main.cpp", 4), at("/p/main.cpp", 5)],
|
||
});
|
||
expect(searched).toEqual(["Used", "helper"]);
|
||
});
|
||
|
||
it("modules a file declares (TS namespace, Rust mod) are still searched", async () => {
|
||
const text = "export namespace Utils {\n export const version = 1;\n}\n";
|
||
outline = ok([
|
||
within(
|
||
item(text, "Utils", "export namespace Utils {\n export const version = 1;\n}", "module"),
|
||
item(text, "version", "version = 1", "variable"),
|
||
),
|
||
]);
|
||
const searched = serve(text, { Utils: [at("/p/main.ts", 5)] });
|
||
expect(await fileUsages(["/p/lib/ns.ts"])).toEqual({ status: "ok", usages: [at("/p/main.ts", 5)] });
|
||
expect(searched).toEqual(["Utils"]);
|
||
});
|
||
});
|
||
|
||
it("is unsupported when the language server can't search", async () => {
|
||
outline = ok([item(text, "a", "pub fn a() {}")]);
|
||
references = unavailable;
|
||
expect((await fileUsages(["/p/a.rs"])).status).toBe("unsupported");
|
||
});
|
||
|
||
it("is cancellable", async () => {
|
||
const abort = new AbortController();
|
||
abort.abort();
|
||
await expect(fileUsages(["/p/a.rs"], abort.signal)).rejects.toMatchObject({ kind: "cancelled" });
|
||
});
|
||
});
|