@assistant-ui/react-ink 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/primitives/composer/ComposerSend.js +1 -1
  7. package/dist/primitives/composer/ComposerSend.js.map +1 -1
  8. package/dist/primitives/diff/DiffContent.d.ts +22 -0
  9. package/dist/primitives/diff/DiffContent.d.ts.map +1 -0
  10. package/dist/primitives/diff/DiffContent.js +40 -0
  11. package/dist/primitives/diff/DiffContent.js.map +1 -0
  12. package/dist/primitives/diff/DiffContext.d.ts +8 -0
  13. package/dist/primitives/diff/DiffContext.d.ts.map +1 -0
  14. package/dist/primitives/diff/DiffContext.js +11 -0
  15. package/dist/primitives/diff/DiffContext.js.map +1 -0
  16. package/dist/primitives/diff/DiffHeader.d.ts +10 -0
  17. package/dist/primitives/diff/DiffHeader.d.ts.map +1 -0
  18. package/dist/primitives/diff/DiffHeader.js +18 -0
  19. package/dist/primitives/diff/DiffHeader.js.map +1 -0
  20. package/dist/primitives/diff/DiffLine.d.ts +13 -0
  21. package/dist/primitives/diff/DiffLine.d.ts.map +1 -0
  22. package/dist/primitives/diff/DiffLine.js +20 -0
  23. package/dist/primitives/diff/DiffLine.js.map +1 -0
  24. package/dist/primitives/diff/DiffRoot.d.ts +12 -0
  25. package/dist/primitives/diff/DiffRoot.d.ts.map +1 -0
  26. package/dist/primitives/diff/DiffRoot.js +8 -0
  27. package/dist/primitives/diff/DiffRoot.js.map +1 -0
  28. package/dist/primitives/diff/DiffStats.d.ts +10 -0
  29. package/dist/primitives/diff/DiffStats.d.ts.map +1 -0
  30. package/dist/primitives/diff/DiffStats.js +12 -0
  31. package/dist/primitives/diff/DiffStats.js.map +1 -0
  32. package/dist/primitives/diff/DiffView.d.ts +13 -0
  33. package/dist/primitives/diff/DiffView.d.ts.map +1 -0
  34. package/dist/primitives/diff/DiffView.js +76 -0
  35. package/dist/primitives/diff/DiffView.js.map +1 -0
  36. package/dist/primitives/diff/diff-utils.d.ts +9 -0
  37. package/dist/primitives/diff/diff-utils.d.ts.map +1 -0
  38. package/dist/primitives/diff/diff-utils.js +125 -0
  39. package/dist/primitives/diff/diff-utils.js.map +1 -0
  40. package/dist/primitives/diff/index.d.ts +7 -0
  41. package/dist/primitives/diff/index.d.ts.map +1 -0
  42. package/dist/primitives/diff/index.js +6 -0
  43. package/dist/primitives/diff/index.js.map +1 -0
  44. package/dist/primitives/diff/types.d.ts +24 -0
  45. package/dist/primitives/diff/types.d.ts.map +1 -0
  46. package/dist/primitives/diff/types.js +2 -0
  47. package/dist/primitives/diff/types.js.map +1 -0
  48. package/dist/primitives/diff.d.ts +2 -0
  49. package/dist/primitives/diff.d.ts.map +1 -0
  50. package/dist/primitives/diff.js +2 -0
  51. package/dist/primitives/diff.js.map +1 -0
  52. package/package.json +10 -8
  53. package/src/index.ts +9 -0
  54. package/src/primitives/composer/ComposerSend.tsx +1 -1
  55. package/src/primitives/diff/DiffContent.tsx +77 -0
  56. package/src/primitives/diff/DiffContext.tsx +18 -0
  57. package/src/primitives/diff/DiffHeader.tsx +38 -0
  58. package/src/primitives/diff/DiffLine.tsx +42 -0
  59. package/src/primitives/diff/DiffRoot.tsx +25 -0
  60. package/src/primitives/diff/DiffStats.tsx +22 -0
  61. package/src/primitives/diff/DiffView.test.tsx +340 -0
  62. package/src/primitives/diff/DiffView.tsx +204 -0
  63. package/src/primitives/diff/diff-utils.ts +149 -0
  64. package/src/primitives/diff/index.ts +25 -0
  65. package/src/primitives/diff/types.ts +28 -0
  66. package/src/primitives/diff.ts +1 -0
@@ -0,0 +1,340 @@
1
+ import type { ReactElement } from "react";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import { cleanup, render } from "ink-testing-library";
4
+ import { parsePatch, computeDiff, foldContext } from "./diff-utils";
5
+ import { DiffContent } from "./DiffContent";
6
+ import { DiffHeader } from "./DiffHeader";
7
+ import { DiffRoot } from "./DiffRoot";
8
+ import { DiffView } from "./DiffView";
9
+
10
+ const renderFrame = async (node: ReactElement) => {
11
+ const instance = render(node);
12
+ await new Promise((resolve) => setTimeout(resolve, 0));
13
+ return instance.lastFrame() ?? "";
14
+ };
15
+
16
+ afterEach(() => {
17
+ cleanup();
18
+ });
19
+
20
+ const SAMPLE_PATCH = `diff --git a/hello.txt b/hello.txt
21
+ index 1234567..abcdefg 100644
22
+ --- a/hello.txt
23
+ +++ b/hello.txt
24
+ @@ -1,3 +1,4 @@
25
+ line 1
26
+ -line 2
27
+ +line 2 modified
28
+ +line 2.5 added
29
+ line 3
30
+ `;
31
+
32
+ describe("parsePatch", () => {
33
+ it("parses a unified diff string", () => {
34
+ const files = parsePatch(SAMPLE_PATCH);
35
+ expect(files).toHaveLength(1);
36
+ const file = files[0]!;
37
+ expect(file.oldName).toBe("hello.txt");
38
+ expect(file.newName).toBe("hello.txt");
39
+ expect(file.additions).toBe(2);
40
+ expect(file.deletions).toBe(1);
41
+
42
+ const types = file.lines.map((l) => l.type);
43
+ expect(types).toContain("add");
44
+ expect(types).toContain("del");
45
+ expect(types).toContain("normal");
46
+ });
47
+
48
+ it("strips CRLF line endings from parsed patch lines", () => {
49
+ const patch = `diff --git a/x.txt b/x.txt
50
+ --- a/x.txt
51
+ +++ b/x.txt
52
+ @@ -1,2 +1,2 @@
53
+ a\r
54
+ -b\r
55
+ +c\r
56
+ `;
57
+
58
+ expect(parsePatch(patch)).toEqual([
59
+ {
60
+ oldName: "x.txt",
61
+ newName: "x.txt",
62
+ additions: 1,
63
+ deletions: 1,
64
+ lines: [
65
+ {
66
+ type: "normal",
67
+ content: "a",
68
+ oldLineNumber: 1,
69
+ newLineNumber: 1,
70
+ },
71
+ {
72
+ type: "del",
73
+ content: "b",
74
+ oldLineNumber: 2,
75
+ },
76
+ {
77
+ type: "add",
78
+ content: "c",
79
+ newLineNumber: 2,
80
+ },
81
+ ],
82
+ },
83
+ ]);
84
+ });
85
+
86
+ it("ignores no-newline markers in unified diff patches", () => {
87
+ const patch = `diff --git a/a.txt b/a.txt
88
+ --- a/a.txt
89
+ +++ b/a.txt
90
+ @@ -1 +1 @@
91
+ -old
92
+ \
93
+ +new
94
+ \
95
+ `;
96
+
97
+ expect(parsePatch(patch)).toEqual([
98
+ {
99
+ oldName: "a.txt",
100
+ newName: "a.txt",
101
+ additions: 1,
102
+ deletions: 1,
103
+ lines: [
104
+ {
105
+ type: "del",
106
+ content: "old",
107
+ oldLineNumber: 1,
108
+ },
109
+ {
110
+ type: "add",
111
+ content: "new",
112
+ newLineNumber: 1,
113
+ },
114
+ ],
115
+ },
116
+ ]);
117
+ });
118
+ });
119
+
120
+ describe("computeDiff", () => {
121
+ it("diffs two strings", () => {
122
+ const result = computeDiff("alpha\nbeta\n", "alpha\ngamma\n");
123
+ expect(result.additions).toBeGreaterThan(0);
124
+ expect(result.deletions).toBeGreaterThan(0);
125
+ const types = result.lines.map((l) => l.type);
126
+ expect(types).toContain("add");
127
+ expect(types).toContain("del");
128
+ expect(types).toContain("normal");
129
+ });
130
+
131
+ it("preserves blank-line additions and deletions", () => {
132
+ expect(computeDiff("a\n", "a\n\n")).toEqual({
133
+ additions: 1,
134
+ deletions: 0,
135
+ lines: [
136
+ {
137
+ type: "normal",
138
+ content: "a",
139
+ oldLineNumber: 1,
140
+ newLineNumber: 1,
141
+ },
142
+ {
143
+ type: "add",
144
+ content: "",
145
+ newLineNumber: 2,
146
+ },
147
+ ],
148
+ });
149
+
150
+ expect(computeDiff("a\n\n", "a\n")).toEqual({
151
+ additions: 0,
152
+ deletions: 1,
153
+ lines: [
154
+ {
155
+ type: "normal",
156
+ content: "a",
157
+ oldLineNumber: 1,
158
+ newLineNumber: 1,
159
+ },
160
+ {
161
+ type: "del",
162
+ content: "",
163
+ oldLineNumber: 2,
164
+ },
165
+ ],
166
+ });
167
+ });
168
+
169
+ it("strips CRLF line endings from computed diffs", () => {
170
+ expect(computeDiff("a\r\nb\r\n", "a\r\nc\r\n")).toEqual({
171
+ additions: 1,
172
+ deletions: 1,
173
+ lines: [
174
+ {
175
+ type: "normal",
176
+ content: "a",
177
+ oldLineNumber: 1,
178
+ newLineNumber: 1,
179
+ },
180
+ {
181
+ type: "del",
182
+ content: "b",
183
+ oldLineNumber: 2,
184
+ },
185
+ {
186
+ type: "add",
187
+ content: "c",
188
+ newLineNumber: 2,
189
+ },
190
+ ],
191
+ });
192
+ });
193
+ });
194
+
195
+ describe("foldContext", () => {
196
+ it("folds unchanged regions beyond contextLines", () => {
197
+ const lines = [
198
+ ...Array.from({ length: 10 }, (_, i) => ({
199
+ type: "normal" as const,
200
+ content: `line ${i}`,
201
+ oldLineNumber: i + 1,
202
+ newLineNumber: i + 1,
203
+ })),
204
+ { type: "add" as const, content: "new line", newLineNumber: 11 },
205
+ ...Array.from({ length: 10 }, (_, i) => ({
206
+ type: "normal" as const,
207
+ content: `line ${i + 11}`,
208
+ oldLineNumber: i + 11,
209
+ newLineNumber: i + 12,
210
+ })),
211
+ ];
212
+
213
+ const result = foldContext(lines, 2);
214
+ const folds = result.filter((l) => l.type === "fold");
215
+ expect(folds.length).toBeGreaterThan(0);
216
+ const totalHidden = folds.reduce(
217
+ (sum, f) => sum + (f.type === "fold" ? f.hiddenCount : 0),
218
+ 0,
219
+ );
220
+ expect(totalHidden).toBe(16);
221
+ });
222
+ });
223
+
224
+ describe("DiffView", () => {
225
+ it("supports composing primitives from prepared files", async () => {
226
+ const frame = await renderFrame(
227
+ <DiffRoot
228
+ files={[
229
+ {
230
+ oldName: "before.txt",
231
+ newName: "after.txt",
232
+ additions: 1,
233
+ deletions: 1,
234
+ lines: [
235
+ {
236
+ type: "del",
237
+ content: "before",
238
+ oldLineNumber: 1,
239
+ },
240
+ {
241
+ type: "add",
242
+ content: "after",
243
+ newLineNumber: 1,
244
+ },
245
+ ],
246
+ },
247
+ ]}
248
+ >
249
+ <DiffHeader />
250
+ <DiffContent />
251
+ </DiffRoot>,
252
+ );
253
+
254
+ expect(frame).toContain("before.txt");
255
+ expect(frame).toContain("after.txt");
256
+ expect(frame).toContain("+1");
257
+ expect(frame).toContain("-1");
258
+ });
259
+
260
+ it("renders a basic patch", async () => {
261
+ const frame = await renderFrame(<DiffView patch={SAMPLE_PATCH} />);
262
+ expect(frame).toContain("hello.txt");
263
+ expect(frame).toContain("+");
264
+ expect(frame).toContain("-");
265
+ });
266
+
267
+ it("renders from oldFile/newFile", async () => {
268
+ const frame = await renderFrame(
269
+ <DiffView
270
+ oldFile={{ content: "hello\nworld\n", name: "test.txt" }}
271
+ newFile={{ content: "hello\nearth\n", name: "test.txt" }}
272
+ />,
273
+ );
274
+ expect(frame).toContain("test.txt");
275
+ expect(frame).toContain("+");
276
+ expect(frame).toContain("-");
277
+ });
278
+
279
+ it("hides line numbers when showLineNumbers=false", async () => {
280
+ const withNumbers = await renderFrame(<DiffView patch={SAMPLE_PATCH} />);
281
+ const withoutNumbers = await renderFrame(
282
+ <DiffView patch={SAMPLE_PATCH} showLineNumbers={false} />,
283
+ );
284
+ expect(withNumbers.length).toBeGreaterThan(withoutNumbers.length);
285
+ });
286
+
287
+ it("truncates with maxLines", async () => {
288
+ const manyLines = Array.from({ length: 50 }, (_, i) => `+line${i}`).join(
289
+ "\n",
290
+ );
291
+ const patch = `diff --git a/big.txt b/big.txt
292
+ --- a/big.txt
293
+ +++ b/big.txt
294
+ @@ -0,0 +1,50 @@
295
+ ${manyLines}
296
+ `;
297
+ const frame = await renderFrame(<DiffView patch={patch} maxLines={5} />);
298
+ expect(frame).toContain("more lines");
299
+ });
300
+
301
+ it("folds context lines", async () => {
302
+ const normalBefore = Array.from({ length: 10 }, (_, i) => ` line${i}`).join(
303
+ "\n",
304
+ );
305
+ const normalAfter = Array.from({ length: 10 }, (_, i) => ` after${i}`).join(
306
+ "\n",
307
+ );
308
+ const patch = `diff --git a/ctx.txt b/ctx.txt
309
+ --- a/ctx.txt
310
+ +++ b/ctx.txt
311
+ @@ -1,21 +1,22 @@
312
+ ${normalBefore}
313
+ +inserted
314
+ ${normalAfter}
315
+ `;
316
+ const frame = await renderFrame(
317
+ <DiffView patch={patch} contextLines={2} />,
318
+ );
319
+ expect(frame).toContain("lines hidden");
320
+ });
321
+
322
+ it("renders multi-file patches", async () => {
323
+ const patch = `diff --git a/a.txt b/a.txt
324
+ --- a/a.txt
325
+ +++ b/a.txt
326
+ @@ -1 +1 @@
327
+ -old a
328
+ +new a
329
+ diff --git a/b.txt b/b.txt
330
+ --- a/b.txt
331
+ +++ b/b.txt
332
+ @@ -1 +1 @@
333
+ -old b
334
+ +new b
335
+ `;
336
+ const frame = await renderFrame(<DiffView patch={patch} />);
337
+ expect(frame).toContain("a.txt");
338
+ expect(frame).toContain("b.txt");
339
+ });
340
+ });
@@ -0,0 +1,204 @@
1
+ import { type ComponentProps, useMemo } from "react";
2
+ import { Box, Text } from "ink";
3
+ import { DiffContent } from "./DiffContent";
4
+ import { useDiffContext } from "./DiffContext";
5
+ import { DiffRoot } from "./DiffRoot";
6
+ import { computeDiff, parsePatch } from "./diff-utils";
7
+ import type {
8
+ DiffFileInput,
9
+ FoldedRegion,
10
+ ParsedFile,
11
+ ParsedLine,
12
+ } from "./types";
13
+
14
+ export type DiffViewProps = Omit<ComponentProps<typeof Box>, "children"> & {
15
+ patch?: string | undefined;
16
+ oldFile?: DiffFileInput | undefined;
17
+ newFile?: DiffFileInput | undefined;
18
+ showLineNumbers?: boolean | undefined;
19
+ contextLines?: number | undefined;
20
+ maxLines?: number | undefined;
21
+ };
22
+
23
+ interface DiffViewInnerProps {
24
+ showLineNumbers: boolean | undefined;
25
+ contextLines: number | undefined;
26
+ maxLines: number | undefined;
27
+ }
28
+
29
+ const INDICATOR: Record<string, string> = {
30
+ add: "+",
31
+ del: "-",
32
+ normal: " ",
33
+ };
34
+
35
+ const isDevNull = (n: string | undefined) => !n || n === "/dev/null";
36
+
37
+ const StyledLine = ({
38
+ line,
39
+ showLineNumbers,
40
+ }: {
41
+ line: ParsedLine;
42
+ showLineNumbers: boolean;
43
+ }) => {
44
+ const lineNum =
45
+ line.type === "del"
46
+ ? line.oldLineNumber
47
+ : line.type === "add"
48
+ ? line.newLineNumber
49
+ : line.oldLineNumber;
50
+ const numStr = lineNum !== undefined ? String(lineNum) : "";
51
+ const padded = numStr.padStart(4);
52
+ const content = `${INDICATOR[line.type]} ${line.content}`;
53
+
54
+ return (
55
+ <Box>
56
+ {showLineNumbers && <Text dimColor>{padded} </Text>}
57
+ {line.type === "add" ? (
58
+ <Text color="green">{content}</Text>
59
+ ) : line.type === "del" ? (
60
+ <Text color="red">{content}</Text>
61
+ ) : (
62
+ <Text>{content}</Text>
63
+ )}
64
+ </Box>
65
+ );
66
+ };
67
+
68
+ const StyledFold = ({ region }: { region: FoldedRegion }) => (
69
+ <Text dimColor>{` --- ${region.hiddenCount} lines hidden ---`}</Text>
70
+ );
71
+
72
+ const DiffViewInner = ({
73
+ showLineNumbers,
74
+ contextLines,
75
+ maxLines,
76
+ }: DiffViewInnerProps) => {
77
+ const { files } = useDiffContext();
78
+ const shouldShowLineNumbers = showLineNumbers ?? true;
79
+
80
+ if (files.length === 0) {
81
+ return <Text dimColor>No diff content</Text>;
82
+ }
83
+
84
+ return (
85
+ <>
86
+ {files.map((file, i) => {
87
+ const renamed =
88
+ !isDevNull(file.oldName) &&
89
+ !isDevNull(file.newName) &&
90
+ file.oldName !== file.newName;
91
+ const displayName = isDevNull(file.newName)
92
+ ? file.oldName
93
+ : file.newName;
94
+
95
+ return (
96
+ <Box key={i} flexDirection="column">
97
+ <Box gap={1}>
98
+ {renamed ? (
99
+ <>
100
+ <Text bold dimColor>
101
+ {file.oldName}
102
+ </Text>
103
+ <Text dimColor>{"->"}</Text>
104
+ <Text bold>{file.newName}</Text>
105
+ </>
106
+ ) : (
107
+ <Text bold>{displayName}</Text>
108
+ )}
109
+ <Text color="green">+{file.additions}</Text>
110
+ <Text color="red">-{file.deletions}</Text>
111
+ </Box>
112
+ <DiffContent
113
+ fileIndex={i}
114
+ contextLines={contextLines}
115
+ maxLines={maxLines}
116
+ renderLine={({ line }) => (
117
+ <StyledLine
118
+ line={line}
119
+ showLineNumbers={shouldShowLineNumbers}
120
+ />
121
+ )}
122
+ renderFold={({ region }) => <StyledFold region={region} />}
123
+ />
124
+ {i < files.length - 1 && <Text> </Text>}
125
+ </Box>
126
+ );
127
+ })}
128
+ </>
129
+ );
130
+ };
131
+
132
+ const getDiffViewFiles = ({
133
+ patch,
134
+ oldFile,
135
+ newFile,
136
+ }: {
137
+ patch?: string | undefined;
138
+ oldFile?: DiffFileInput | undefined;
139
+ newFile?: DiffFileInput | undefined;
140
+ }): ParsedFile[] => {
141
+ if (patch) {
142
+ return parsePatch(patch);
143
+ }
144
+
145
+ if (!oldFile || !newFile) {
146
+ return [];
147
+ }
148
+
149
+ const { lines, additions, deletions } = computeDiff(
150
+ oldFile.content,
151
+ newFile.content,
152
+ );
153
+
154
+ return [
155
+ {
156
+ oldName: oldFile.name,
157
+ newName: newFile.name,
158
+ lines,
159
+ additions,
160
+ deletions,
161
+ },
162
+ ];
163
+ };
164
+
165
+ export const DiffView = ({
166
+ patch,
167
+ oldFile,
168
+ newFile,
169
+ showLineNumbers,
170
+ contextLines,
171
+ maxLines,
172
+ ...boxProps
173
+ }: DiffViewProps) => {
174
+ const oldContent = oldFile?.content;
175
+ const oldName = oldFile?.name;
176
+ const newContent = newFile?.content;
177
+ const newName = newFile?.name;
178
+
179
+ const files = useMemo(
180
+ () =>
181
+ getDiffViewFiles({
182
+ patch,
183
+ oldFile:
184
+ oldContent !== undefined
185
+ ? { content: oldContent, name: oldName }
186
+ : undefined,
187
+ newFile:
188
+ newContent !== undefined
189
+ ? { content: newContent, name: newName }
190
+ : undefined,
191
+ }),
192
+ [patch, oldContent, oldName, newContent, newName],
193
+ );
194
+
195
+ return (
196
+ <DiffRoot files={files} {...boxProps}>
197
+ <DiffViewInner
198
+ showLineNumbers={showLineNumbers}
199
+ contextLines={contextLines}
200
+ maxLines={maxLines}
201
+ />
202
+ </DiffRoot>
203
+ );
204
+ };
@@ -0,0 +1,149 @@
1
+ import { diffLines } from "diff";
2
+ import parseDiff from "parse-diff";
3
+ import type {
4
+ ParsedLine,
5
+ ParsedFile,
6
+ DisplayLine,
7
+ FoldedRegion,
8
+ } from "./types";
9
+
10
+ const NO_NEWLINE_MARKER = "\";
11
+
12
+ const stripTrailingCarriageReturn = (content: string) => {
13
+ return content.endsWith("\r") ? content.slice(0, -1) : content;
14
+ };
15
+
16
+ const parseChangeContent = (content: string) => {
17
+ const normalized = stripTrailingCarriageReturn(content);
18
+ if (normalized === NO_NEWLINE_MARKER) return null;
19
+ return stripTrailingCarriageReturn(normalized.slice(1));
20
+ };
21
+
22
+ const splitDiffLines = (value: string) => {
23
+ const normalized = value.endsWith("\n") ? value.slice(0, -1) : value;
24
+ if (normalized.length === 0) {
25
+ return value.length > 0 ? [""] : [];
26
+ }
27
+ return normalized.split("\n").map(stripTrailingCarriageReturn);
28
+ };
29
+
30
+ export function parsePatch(patch: string): ParsedFile[] {
31
+ const files = parseDiff(patch);
32
+ return files.map((file) => {
33
+ const lines: ParsedLine[] = [];
34
+ let additions = 0;
35
+ let deletions = 0;
36
+
37
+ for (const chunk of file.chunks) {
38
+ let oldLine = chunk.oldStart;
39
+ let newLine = chunk.newStart;
40
+
41
+ for (const change of chunk.changes) {
42
+ const content = parseChangeContent(change.content);
43
+ if (content === null) continue;
44
+
45
+ if (change.type === "add") {
46
+ additions++;
47
+ lines.push({
48
+ type: "add",
49
+ content,
50
+ newLineNumber: newLine++,
51
+ });
52
+ } else if (change.type === "del") {
53
+ deletions++;
54
+ lines.push({
55
+ type: "del",
56
+ content,
57
+ oldLineNumber: oldLine++,
58
+ });
59
+ } else {
60
+ lines.push({
61
+ type: "normal",
62
+ content,
63
+ oldLineNumber: oldLine++,
64
+ newLineNumber: newLine++,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ return {
71
+ oldName: file.from,
72
+ newName: file.to,
73
+ lines,
74
+ additions,
75
+ deletions,
76
+ };
77
+ });
78
+ }
79
+
80
+ export function computeDiff(
81
+ oldContent: string,
82
+ newContent: string,
83
+ ): { lines: ParsedLine[]; additions: number; deletions: number } {
84
+ const changes = diffLines(oldContent, newContent);
85
+ const lines: ParsedLine[] = [];
86
+ let oldLine = 1;
87
+ let newLine = 1;
88
+ let additions = 0;
89
+ let deletions = 0;
90
+
91
+ for (const change of changes) {
92
+ for (const content of splitDiffLines(change.value)) {
93
+ if (change.added) {
94
+ additions++;
95
+ lines.push({ type: "add", content, newLineNumber: newLine++ });
96
+ } else if (change.removed) {
97
+ deletions++;
98
+ lines.push({ type: "del", content, oldLineNumber: oldLine++ });
99
+ } else {
100
+ lines.push({
101
+ type: "normal",
102
+ content,
103
+ oldLineNumber: oldLine++,
104
+ newLineNumber: newLine++,
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ return { lines, additions, deletions };
111
+ }
112
+
113
+ export function foldContext(
114
+ lines: ParsedLine[],
115
+ contextLines: number,
116
+ ): DisplayLine[] {
117
+ const ctx = Math.max(0, contextLines);
118
+ const keep = new Set<number>();
119
+
120
+ for (let i = 0; i < lines.length; i++) {
121
+ if (lines[i]!.type !== "normal") {
122
+ for (
123
+ let j = Math.max(0, i - ctx);
124
+ j <= Math.min(lines.length - 1, i + ctx);
125
+ j++
126
+ ) {
127
+ keep.add(j);
128
+ }
129
+ }
130
+ }
131
+
132
+ const result: DisplayLine[] = [];
133
+ let i = 0;
134
+ while (i < lines.length) {
135
+ if (keep.has(i)) {
136
+ result.push(lines[i]!);
137
+ i++;
138
+ } else {
139
+ let hiddenCount = 0;
140
+ while (i < lines.length && !keep.has(i)) {
141
+ hiddenCount++;
142
+ i++;
143
+ }
144
+ result.push({ type: "fold", hiddenCount } satisfies FoldedRegion);
145
+ }
146
+ }
147
+
148
+ return result;
149
+ }
@@ -0,0 +1,25 @@
1
+ export { DiffRoot as Root, type DiffRootProps as RootProps } from "./DiffRoot";
2
+ export {
3
+ DiffHeader as Header,
4
+ type DiffHeaderProps as HeaderProps,
5
+ } from "./DiffHeader";
6
+ export {
7
+ DiffContent as Content,
8
+ type DiffContentProps as ContentProps,
9
+ } from "./DiffContent";
10
+ export {
11
+ DiffLine as Line,
12
+ type DiffLineProps as LineProps,
13
+ } from "./DiffLine";
14
+ export {
15
+ DiffStats as Stats,
16
+ type DiffStatsProps as StatsProps,
17
+ } from "./DiffStats";
18
+ export type {
19
+ ParsedLine,
20
+ ParsedFile,
21
+ DiffFileInput,
22
+ DiffLineType,
23
+ DisplayLine,
24
+ FoldedRegion,
25
+ } from "./types";