дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
132 рядки
5.0 KiB
TypeScript
132 рядки
5.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { EditorState } from "@codemirror/state";
|
|
import { EditorView } from "@codemirror/view";
|
|
import { ensureSyntaxTree } from "@codemirror/language";
|
|
import { languages } from "@codemirror/language-data";
|
|
import type { FileMatches } from "@/platform/ipc";
|
|
import {
|
|
buildTodoQuery,
|
|
countItems,
|
|
DEFAULT_TODO_SETTINGS,
|
|
groupTodos,
|
|
itemsFromMatches,
|
|
mergeLive,
|
|
scanText,
|
|
todoRegexSource,
|
|
type TodoItem,
|
|
type TodoSettings,
|
|
} from "./model";
|
|
import { todoDecorations } from "./highlight";
|
|
|
|
const S = DEFAULT_TODO_SETTINGS;
|
|
|
|
describe("query", () => {
|
|
it("builds the keyword regex from enabled patterns", () => {
|
|
expect(todoRegexSource(S)).toBe("\\b(TODO|FIXME|HACK|XXX)\\b.*");
|
|
const q = buildTodoQuery({ ...S, caseSensitive: false }, { kind: "files", paths: ["/a"] })!;
|
|
expect(q).toMatchObject({ isRegex: true, caseSensitive: false, scope: { kind: "files", paths: ["/a"] } });
|
|
const none: TodoSettings = { ...S, patterns: S.patterns.map((p) => ({ ...p, enabled: false })) };
|
|
expect(buildTodoQuery(none)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("matches → items", () => {
|
|
const fm: FileMatches = {
|
|
path: "/p/src/a.ts",
|
|
relPath: "src/a.ts",
|
|
matches: [
|
|
{ line: 3, text: " // TODO: fix this", ranges: [[5, 19]], textOffset: 0 },
|
|
{ line: 9, text: "const TODO_LIST = 1; /* FIXME later */", ranges: [[24, 38]], textOffset: 0 },
|
|
{ line: 12, text: 'const s = "TODO nope";', ranges: [[11, 21]], textOffset: 0 },
|
|
{ line: 20, text: "…x # HACK around", ranges: [[5, 16]], textOffset: 100 },
|
|
],
|
|
};
|
|
|
|
it("keeps comment matches with keyword, position and clean text", () => {
|
|
const items = itemsFromMatches(fm, S);
|
|
expect(items).toEqual([
|
|
{ path: "/p/src/a.ts", line: 3, col: 5, keyword: "TODO", text: "TODO: fix this" },
|
|
{ path: "/p/src/a.ts", line: 9, col: 24, keyword: "FIXME", text: "FIXME later" },
|
|
{ path: "/p/src/a.ts", line: 20, col: 105, keyword: "HACK", text: "HACK around" },
|
|
]);
|
|
});
|
|
|
|
it("includes non-comment matches when configured", () => {
|
|
expect(itemsFromMatches(fm, { ...S, commentsOnly: false })).toHaveLength(4);
|
|
});
|
|
});
|
|
|
|
describe("local scan", () => {
|
|
it("finds items with 0-based lines and columns", () => {
|
|
const text = "fn a() {}\n// todo lower\n // TODO: one\n/* XXX two */\nlet TODO = 1;";
|
|
expect(scanText("/f.rs", text, S)).toEqual([
|
|
{ path: "/f.rs", line: 2, col: 7, keyword: "TODO", text: "TODO: one" },
|
|
{ path: "/f.rs", line: 3, col: 3, keyword: "XXX", text: "XXX two" },
|
|
]);
|
|
const ci = scanText("/f.rs", text, { ...S, caseSensitive: false });
|
|
expect(ci.map((i) => [i.line, i.keyword])).toEqual([
|
|
[1, "TODO"],
|
|
[2, "TODO"],
|
|
[3, "XXX"],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("grouping and filters", () => {
|
|
const item = (path: string, line: number, keyword: string): TodoItem => ({
|
|
path,
|
|
line,
|
|
col: 0,
|
|
keyword,
|
|
text: keyword,
|
|
});
|
|
const map = new Map<string, TodoItem[]>([
|
|
["/p/b.ts", [item("/p/b.ts", 5, "TODO"), item("/p/b.ts", 1, "FIXME")]],
|
|
["/p/a.ts", [item("/p/a.ts", 2, "HACK")]],
|
|
["/p/c.ts", []],
|
|
]);
|
|
|
|
it("groups by file sorted by path, items by line", () => {
|
|
const groups = groupTodos(map, { keywords: null });
|
|
expect(groups.map((g) => g.path)).toEqual(["/p/a.ts", "/p/b.ts"]);
|
|
expect(groups[1]!.items.map((i) => i.line)).toEqual([1, 5]);
|
|
expect(countItems(groups)).toBe(3);
|
|
});
|
|
|
|
it("filters by keyword and current file", () => {
|
|
expect(countItems(groupTodos(map, { keywords: new Set(["TODO"]) }))).toBe(1);
|
|
expect(groupTodos(map, { keywords: null, path: "/p/b.ts" }).map((g) => g.path)).toEqual(["/p/b.ts"]);
|
|
expect(groupTodos(map, { keywords: null, path: null })).toEqual([]);
|
|
expect(groupTodos(map, { keywords: new Set(["HACK"]), path: "/p/b.ts" })).toEqual([]);
|
|
});
|
|
|
|
it("layers live editor results over the scan", () => {
|
|
const merged = mergeLive(map, new Map([["/p/a.ts", []]]));
|
|
expect(countItems(groupTodos(merged, { keywords: null }))).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("editor highlighting", () => {
|
|
it("decorates TODO comments only", async () => {
|
|
const desc = languages.find((l) => l.name === "JavaScript")!;
|
|
const doc = 'const TODO = "TODO";\n// TODO: refactor\n/* FIXME now */';
|
|
const state = EditorState.create({ doc, extensions: [await desc.load()] });
|
|
ensureSyntaxTree(state, doc.length, 5000);
|
|
const view = new EditorView({ state, parent: document.body });
|
|
const ranges: Array<[number, number, string]> = [];
|
|
todoDecorations(view, S).between(0, doc.length, (from, to, d) => {
|
|
ranges.push([from, to, String(d.spec.class)]);
|
|
});
|
|
const line2 = doc.indexOf("TODO: refactor");
|
|
const fixme = doc.indexOf("FIXME");
|
|
ranges.sort((a, b) => a[0] - b[0] || a[2].localeCompare(b[2]));
|
|
expect(ranges).toEqual([
|
|
[line2, line2 + "TODO: refactor".length, "cm-openidea-todo"],
|
|
[line2, line2 + 4, "cm-openidea-todo-keyword"],
|
|
[fixme, fixme + "FIXME now".length, "cm-openidea-todo"],
|
|
[fixme, fixme + 5, "cm-openidea-todo-keyword"],
|
|
]);
|
|
view.destroy();
|
|
});
|
|
});
|