1import type { CodeViewItem } from "@pierre/diffs";
2import { parsePatchFiles } from "@pierre/diffs/utils/parsePatchFiles";
3import { describe, expect, it } from "vite-plus/test";
4
5// Find-in-diff lives in our @pierre/diffs patch and is not a package export.
6const searchUrl = new URL("./components/CodeViewSearch.js", import.meta.resolve("@pierre/diffs"));
7const { collectMatches } = (await import(/* @vite-ignore */ searchUrl.href)) as {
8 collectMatches(
9 items: ReadonlyArray<CodeViewItem<undefined>>,
10 params: { text: string; caseSensitive: boolean; wholeWord: boolean; regex: boolean },
11 ): Array<{
12 id: string;
13 side: string;
14 isContext: boolean;
15 lineNumber: number;
16 start: number;
17 end: number;
18 }>;
19};
20
21const PATCH = `diff --git a/src/a.ts b/src/a.ts
22index 1111111..2222222 100644
23--- a/src/a.ts
24+++ b/src/a.ts
25@@ -1,4 +1,5 @@
26 const keep = 1;
27-const oldName = 2;
28+const newName = 2;
29+const added = oldName;
30 const tail = 4;
31@@ -20,3 +21,3 @@ function x() {
32 ctx20 name
33-del21 Name
34+add22 names
35 ctx22
36diff --git a/src/b.ts b/src/b.ts
37index 3333333..4444444 100644
38--- a/src/b.ts
39+++ b/src/b.ts
40@@ -1,1 +1,1 @@
41-nil
42+: nil
43`;
44
45const items: CodeViewItem<undefined>[] = parsePatchFiles(PATCH)[0]!.files.map(
46 (fileDiff, index) => ({ id: `file-${index}`, type: "diff", fileDiff, collapsed: index === 1 }),
47);
48
49function find(
50 text: string,
51 flags: { caseSensitive?: boolean; wholeWord?: boolean; regex?: boolean } = {},
52) {
53 return collectMatches(items, {
54 text,
55 caseSensitive: flags.caseSensitive ?? false,
56 wholeWord: flags.wholeWord ?? false,
57 regex: flags.regex ?? false,
58 }).map(({ id, side, lineNumber, start, end }) => `${id}:${side}:${lineNumber}:${start}-${end}`);
59}
60
61describe("collectMatches", () => {
62 it("finds removed, added, and context lines in render order, folded files included", () => {
63 expect(find("oldName")).toEqual(["file-0:deletions:2:6-13", "file-0:additions:3:14-21"]);
64 expect(find("nil")).toEqual(["file-1:deletions:1:0-3", "file-1:additions:1:2-5"]);
65 });
66
67 it("honours case, whole-word, and regex toggles", () => {
68 expect(find("Name", { caseSensitive: true })).toEqual([
69 "file-0:deletions:2:9-13",
70 "file-0:additions:2:9-13",
71 "file-0:additions:3:17-21",
72 "file-0:deletions:21:6-10",
73 ]);
74 expect(find("name", { wholeWord: true })).toEqual([
75 "file-0:additions:21:6-10",
76 "file-0:deletions:21:6-10",
77 ]);
78 expect(find("n[a-z]+e", { regex: true })).toHaveLength(6);
79 });
80
81 it("marks unchanged lines, which split view draws in both columns", () => {
82 const flags = collectMatches(items, {
83 text: "ctx",
84 caseSensitive: false,
85 wholeWord: false,
86 regex: false,
87 }).map(({ lineNumber, isContext }) => [lineNumber, isContext]);
88 expect(flags).toEqual([
89 [21, true],
90 [23, true],
91 ]);
92 expect(
93 collectMatches(items, {
94 text: "oldName",
95 caseSensitive: false,
96 wholeWord: false,
97 regex: false,
98 }).map(({ isContext }) => isContext),
99 ).toEqual([false, false]);
100 });
101
102 it("returns nothing for an empty query or an invalid pattern", () => {
103 expect(find("")).toEqual([]);
104 expect(find("(", { regex: true })).toEqual([]);
105 });
106});