critique 0.1.0 → 0.1.2
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.
- package/CHANGELOG.md +11 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/cli.tsx +9 -3
- package/src/diff.tsx +0 -940
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# 0.1.2
|
|
2
|
+
|
|
3
|
+
- Branch comparison:
|
|
4
|
+
- Document two-ref comparison: `critique <base> <head>`
|
|
5
|
+
- Uses three-dot syntax (like GitHub PRs) to show what head added since diverging from base
|
|
6
|
+
- Syntax highlighting:
|
|
7
|
+
- Fix filetype detection to match available tree-sitter parsers
|
|
8
|
+
- Map all JS/TS/JSX/TSX to `typescript` parser (handles all as superset)
|
|
9
|
+
- Map JSON to `javascript` parser (JSON is valid JS)
|
|
10
|
+
- Return `undefined` for unsupported extensions instead of passing them through
|
|
11
|
+
|
|
1
12
|
# 0.1.0
|
|
2
13
|
|
|
3
14
|
- Diff view:
|
package/README.md
CHANGED
|
@@ -31,6 +31,10 @@ critique --staged
|
|
|
31
31
|
critique --commit HEAD~1
|
|
32
32
|
critique abc1234
|
|
33
33
|
|
|
34
|
+
# Compare two branches (PR-style, shows what head added since diverging from base)
|
|
35
|
+
critique main feature-branch # what feature-branch added vs main
|
|
36
|
+
critique main HEAD # what current branch added vs main
|
|
37
|
+
|
|
34
38
|
# Watch mode - auto-refresh on file changes
|
|
35
39
|
critique --watch
|
|
36
40
|
```
|
package/package.json
CHANGED
package/src/cli.tsx
CHANGED
|
@@ -60,16 +60,22 @@ function savePersistedState(state: PersistedState): void {
|
|
|
60
60
|
const persistedState = loadPersistedState();
|
|
61
61
|
|
|
62
62
|
// Detect filetype from filename for syntax highlighting
|
|
63
|
+
// Maps to tree-sitter parsers available in @opentui/core: typescript, javascript, markdown, zig
|
|
63
64
|
function detectFiletype(filePath: string): string | undefined {
|
|
64
65
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
65
66
|
switch (ext) {
|
|
67
|
+
// TypeScript parser handles TS, TSX, JS, JSX (it's a superset)
|
|
66
68
|
case "ts":
|
|
67
|
-
return "typescript";
|
|
68
69
|
case "tsx":
|
|
69
70
|
case "js":
|
|
70
71
|
case "jsx":
|
|
71
72
|
case "mjs":
|
|
72
73
|
case "cjs":
|
|
74
|
+
case "mts":
|
|
75
|
+
case "cts":
|
|
76
|
+
return "typescript";
|
|
77
|
+
// JSON uses JavaScript parser (JSON is valid JS)
|
|
78
|
+
case "json":
|
|
73
79
|
return "javascript";
|
|
74
80
|
case "md":
|
|
75
81
|
case "mdx":
|
|
@@ -77,7 +83,7 @@ function detectFiletype(filePath: string): string | undefined {
|
|
|
77
83
|
case "zig":
|
|
78
84
|
return "zig";
|
|
79
85
|
default:
|
|
80
|
-
return
|
|
86
|
+
return undefined;
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -554,7 +560,7 @@ cli
|
|
|
554
560
|
return `git diff --cached --no-prefix ${contextArg}`.trim();
|
|
555
561
|
if (options.commit)
|
|
556
562
|
return `git show ${options.commit} --no-prefix ${contextArg}`.trim();
|
|
557
|
-
// Two refs: compare base...head (three-dot, shows changes since branches diverged)
|
|
563
|
+
// Two refs: compare base...head (three-dot, shows changes since branches diverged, like GitHub PRs)
|
|
558
564
|
if (base && head)
|
|
559
565
|
return `git diff ${base}...${head} --no-prefix ${contextArg}`.trim();
|
|
560
566
|
// Single ref: show that commit's changes
|
package/src/diff.tsx
DELETED
|
@@ -1,940 +0,0 @@
|
|
|
1
|
-
import { RGBA, DiffRenderable, SyntaxStyle, parseColor, addDefaultParsers, getTreeSitterClient, type MouseEvent } from "@opentui/core";
|
|
2
|
-
import { extend } from "@opentui/react";
|
|
3
|
-
import { execSync } from "child_process";
|
|
4
|
-
import { diffWords } from "diff";
|
|
5
|
-
|
|
6
|
-
import * as React from "react";
|
|
7
|
-
|
|
8
|
-
import { type StructuredPatchHunk as Hunk } from "diff";
|
|
9
|
-
import {
|
|
10
|
-
createHighlighter,
|
|
11
|
-
type BundledLanguage,
|
|
12
|
-
type GrammarState,
|
|
13
|
-
type ThemedToken
|
|
14
|
-
} from "shiki";
|
|
15
|
-
|
|
16
|
-
// Register the diff component with opentui react
|
|
17
|
-
extend({ diff: DiffRenderable });
|
|
18
|
-
|
|
19
|
-
// Initialize tree-sitter client to ensure parsers are loaded
|
|
20
|
-
getTreeSitterClient();
|
|
21
|
-
|
|
22
|
-
// Declare the diff component type for JSX
|
|
23
|
-
declare module "@opentui/react" {
|
|
24
|
-
interface OpenTUIComponents {
|
|
25
|
-
diff: typeof DiffRenderable;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// GitHub Dark theme - copied exactly from opentui diff-demo.ts
|
|
30
|
-
export const githubDarkSyntaxTheme = {
|
|
31
|
-
keyword: { fg: parseColor("#FF7B72"), bold: true },
|
|
32
|
-
"keyword.import": { fg: parseColor("#FF7B72"), bold: true },
|
|
33
|
-
string: { fg: parseColor("#A5D6FF") },
|
|
34
|
-
comment: { fg: parseColor("#8B949E"), italic: true },
|
|
35
|
-
number: { fg: parseColor("#79C0FF") },
|
|
36
|
-
boolean: { fg: parseColor("#79C0FF") },
|
|
37
|
-
constant: { fg: parseColor("#79C0FF") },
|
|
38
|
-
function: { fg: parseColor("#D2A8FF") },
|
|
39
|
-
"function.call": { fg: parseColor("#D2A8FF") },
|
|
40
|
-
constructor: { fg: parseColor("#FFA657") },
|
|
41
|
-
type: { fg: parseColor("#FFA657") },
|
|
42
|
-
operator: { fg: parseColor("#FF7B72") },
|
|
43
|
-
variable: { fg: parseColor("#E6EDF3") },
|
|
44
|
-
property: { fg: parseColor("#79C0FF") },
|
|
45
|
-
bracket: { fg: parseColor("#F0F6FC") },
|
|
46
|
-
punctuation: { fg: parseColor("#F0F6FC") },
|
|
47
|
-
default: { fg: parseColor("#E6EDF3") },
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export { SyntaxStyle };
|
|
51
|
-
|
|
52
|
-
// Detect filetype from filename for syntax highlighting
|
|
53
|
-
// Only returns filetypes that have parsers bundled in @opentui/core:
|
|
54
|
-
// javascript, typescript, markdown, zig
|
|
55
|
-
export function detectFiletype(filePath: string): string | undefined {
|
|
56
|
-
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
57
|
-
switch (ext) {
|
|
58
|
-
case "ts": case "tsx": return "typescript";
|
|
59
|
-
case "js": case "jsx": case "mjs": case "cjs": return "javascript";
|
|
60
|
-
case "md": case "mdx": return "markdown";
|
|
61
|
-
case "zig": return "zig";
|
|
62
|
-
default: return undefined;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const UNCHANGED_CODE_BG = RGBA.fromInts(15, 15, 15, 255);
|
|
67
|
-
const ADDED_BG_LIGHT = RGBA.fromInts(100, 250, 120, 12);
|
|
68
|
-
const REMOVED_BG_LIGHT = RGBA.fromInts(255, 0, 0, 32);
|
|
69
|
-
|
|
70
|
-
const LINE_NUMBER_BG = RGBA.fromInts(5, 5, 5, 255);
|
|
71
|
-
const REMOVED_LINE_NUMBER_BG = RGBA.fromInts(60, 0, 0, 255);
|
|
72
|
-
const ADDED_LINE_NUMBER_BG = RGBA.fromInts(0, 50, 0, 255);
|
|
73
|
-
const LINE_NUMBER_FG_BRIGHT = RGBA.fromInts(255, 255, 255, 255);
|
|
74
|
-
const LINE_NUMBER_FG_DIM = "brightBlack";
|
|
75
|
-
|
|
76
|
-
function openInEditor(filePath: string, lineNumber: number) {
|
|
77
|
-
const editor = process.env.REACT_EDITOR || "zed";
|
|
78
|
-
|
|
79
|
-
execSync(`${editor} "${filePath}:${lineNumber}"`, { stdio: "ignore" });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const theme = "github-dark-default";
|
|
83
|
-
const highlighterStart = performance.now();
|
|
84
|
-
const highlighter = await createHighlighter({
|
|
85
|
-
themes: [theme],
|
|
86
|
-
langs: [
|
|
87
|
-
"javascript",
|
|
88
|
-
"typescript",
|
|
89
|
-
"tsx",
|
|
90
|
-
"jsx",
|
|
91
|
-
"json",
|
|
92
|
-
"markdown",
|
|
93
|
-
"html",
|
|
94
|
-
"css",
|
|
95
|
-
"python",
|
|
96
|
-
"rust",
|
|
97
|
-
"go",
|
|
98
|
-
"java",
|
|
99
|
-
"c",
|
|
100
|
-
"cpp",
|
|
101
|
-
"yaml",
|
|
102
|
-
"toml",
|
|
103
|
-
"bash",
|
|
104
|
-
"sh",
|
|
105
|
-
"sql",
|
|
106
|
-
],
|
|
107
|
-
});
|
|
108
|
-
const highlighterDuration = performance.now() - highlighterStart;
|
|
109
|
-
|
|
110
|
-
function detectLanguage(filePath: string): BundledLanguage {
|
|
111
|
-
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
112
|
-
switch (ext) {
|
|
113
|
-
case "ts":
|
|
114
|
-
return "typescript";
|
|
115
|
-
case "tsx":
|
|
116
|
-
return "tsx";
|
|
117
|
-
case "jsx":
|
|
118
|
-
return "jsx";
|
|
119
|
-
case "js":
|
|
120
|
-
case "mjs":
|
|
121
|
-
case "cjs":
|
|
122
|
-
return "javascript";
|
|
123
|
-
case "json":
|
|
124
|
-
return "json";
|
|
125
|
-
case "md":
|
|
126
|
-
case "mdx":
|
|
127
|
-
case "markdown":
|
|
128
|
-
return "markdown";
|
|
129
|
-
case "html":
|
|
130
|
-
case "htm":
|
|
131
|
-
return "html";
|
|
132
|
-
case "css":
|
|
133
|
-
return "css";
|
|
134
|
-
case "py":
|
|
135
|
-
return "python";
|
|
136
|
-
case "rs":
|
|
137
|
-
return "rust";
|
|
138
|
-
case "go":
|
|
139
|
-
return "go";
|
|
140
|
-
case "java":
|
|
141
|
-
return "java";
|
|
142
|
-
case "c":
|
|
143
|
-
case "h":
|
|
144
|
-
return "c";
|
|
145
|
-
case "cpp":
|
|
146
|
-
case "cc":
|
|
147
|
-
case "cxx":
|
|
148
|
-
case "hpp":
|
|
149
|
-
case "hxx":
|
|
150
|
-
return "cpp";
|
|
151
|
-
case "yaml":
|
|
152
|
-
case "yml":
|
|
153
|
-
return "yaml";
|
|
154
|
-
case "toml":
|
|
155
|
-
return "toml";
|
|
156
|
-
case "sh":
|
|
157
|
-
return "sh";
|
|
158
|
-
case "bash":
|
|
159
|
-
return "bash";
|
|
160
|
-
case "sql":
|
|
161
|
-
return "sql";
|
|
162
|
-
default:
|
|
163
|
-
return "javascript";
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function renderHighlightedTokens(tokens: ThemedToken[]) {
|
|
168
|
-
return tokens.map((token, tokenIdx) => {
|
|
169
|
-
const color = token.color;
|
|
170
|
-
const fg = color ? RGBA.fromHex(color) : undefined;
|
|
171
|
-
|
|
172
|
-
return (
|
|
173
|
-
<span key={tokenIdx} fg={fg}>
|
|
174
|
-
{token.content}
|
|
175
|
-
</span>
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Custom error boundary class
|
|
181
|
-
class ErrorBoundary extends React.Component<
|
|
182
|
-
{ children: React.ReactNode },
|
|
183
|
-
{ hasError: boolean; error: Error | null }
|
|
184
|
-
> {
|
|
185
|
-
constructor(props: { children: React.ReactNode }) {
|
|
186
|
-
super(props);
|
|
187
|
-
this.state = { hasError: false, error: null };
|
|
188
|
-
|
|
189
|
-
// Bind methods
|
|
190
|
-
this.componentDidCatch = this.componentDidCatch.bind(this);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
static getDerivedStateFromError(error: Error): {
|
|
194
|
-
hasError: boolean;
|
|
195
|
-
error: Error;
|
|
196
|
-
} {
|
|
197
|
-
return { hasError: true, error };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
201
|
-
console.error("Error caught by boundary:", error);
|
|
202
|
-
console.error("Component stack:", errorInfo.componentStack);
|
|
203
|
-
|
|
204
|
-
// Copy stack trace to clipboard
|
|
205
|
-
const stackTrace = `${error.message}\n\nStack trace:\n${error.stack}\n\nComponent stack:\n${errorInfo.componentStack}`;
|
|
206
|
-
const { execSync } = require("child_process");
|
|
207
|
-
try {
|
|
208
|
-
execSync("pbcopy", { input: stackTrace });
|
|
209
|
-
} catch (copyError) {
|
|
210
|
-
console.error("Failed to copy to clipboard:", copyError);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
override render(): any {
|
|
215
|
-
if (this.state.hasError && this.state.error) {
|
|
216
|
-
return (
|
|
217
|
-
<box style={{ flexDirection: "column", padding: 2 }}>
|
|
218
|
-
<text fg="red">
|
|
219
|
-
<strong>Error occurred:</strong>
|
|
220
|
-
</text>
|
|
221
|
-
<text>{this.state.error.message}</text>
|
|
222
|
-
<text fg="brightBlack">Stack trace (copied to clipboard):</text>
|
|
223
|
-
<text fg="white">{this.state.error.stack}</text>
|
|
224
|
-
</box>
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return this.props.children;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export const FileEditPreviewTitle = ({
|
|
233
|
-
filePath,
|
|
234
|
-
hunks,
|
|
235
|
-
}: {
|
|
236
|
-
filePath: string;
|
|
237
|
-
hunks: Hunk[];
|
|
238
|
-
}) => {
|
|
239
|
-
const numAdditions = hunks.reduce(
|
|
240
|
-
(count, hunk) => count + hunk.lines.filter((_) => _.startsWith("+")).length,
|
|
241
|
-
0,
|
|
242
|
-
);
|
|
243
|
-
const numRemovals = hunks.reduce(
|
|
244
|
-
(count, hunk) => count + hunk.lines.filter((_) => _.startsWith("-")).length,
|
|
245
|
-
0,
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const isNewFile = numAdditions > 0 && numRemovals === 0;
|
|
249
|
-
const isDeleted = numRemovals > 0 && numAdditions === 0;
|
|
250
|
-
|
|
251
|
-
return (
|
|
252
|
-
<text>
|
|
253
|
-
{isNewFile ? "Created" : isDeleted ? "Deleted" : "Updated"} <strong>{filePath}</strong>
|
|
254
|
-
{numAdditions > 0 || numRemovals > 0 ? " with " : ""}
|
|
255
|
-
{numAdditions > 0 ? (
|
|
256
|
-
<>
|
|
257
|
-
<strong>{numAdditions}</strong>{" "}
|
|
258
|
-
{numAdditions > 1 ? "additions" : "addition"}
|
|
259
|
-
</>
|
|
260
|
-
) : null}
|
|
261
|
-
{numAdditions > 0 && numRemovals > 0 ? " and " : null}
|
|
262
|
-
{numRemovals > 0 ? (
|
|
263
|
-
<>
|
|
264
|
-
<strong>{numRemovals}</strong>{" "}
|
|
265
|
-
{numRemovals > 1 ? "removals" : "removal"}
|
|
266
|
-
</>
|
|
267
|
-
) : null}
|
|
268
|
-
</text>
|
|
269
|
-
);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
export const FileEditPreview = ({
|
|
273
|
-
hunks,
|
|
274
|
-
paddingLeft = 0,
|
|
275
|
-
splitView = true,
|
|
276
|
-
filePath = "",
|
|
277
|
-
}: {
|
|
278
|
-
hunks: Hunk[];
|
|
279
|
-
paddingLeft?: number;
|
|
280
|
-
splitView?: boolean;
|
|
281
|
-
filePath?: string;
|
|
282
|
-
}) => {
|
|
283
|
-
React.useEffect(() => {
|
|
284
|
-
console.log(
|
|
285
|
-
`Highlighter initialized in ${highlighterDuration.toFixed(2)}ms`,
|
|
286
|
-
);
|
|
287
|
-
}, []);
|
|
288
|
-
|
|
289
|
-
const allLines = hunks.flatMap((h) => h.lines);
|
|
290
|
-
let oldLineNum = hunks[0]?.oldStart || 1;
|
|
291
|
-
let newLineNum = hunks[0]?.newStart || 1;
|
|
292
|
-
|
|
293
|
-
const maxOldLine = allLines.reduce((max, line) => {
|
|
294
|
-
if (line.startsWith("-")) {
|
|
295
|
-
return Math.max(max, oldLineNum++);
|
|
296
|
-
} else if (line.startsWith("+")) {
|
|
297
|
-
newLineNum++;
|
|
298
|
-
return max;
|
|
299
|
-
} else {
|
|
300
|
-
oldLineNum++;
|
|
301
|
-
newLineNum++;
|
|
302
|
-
return Math.max(max, oldLineNum - 1);
|
|
303
|
-
}
|
|
304
|
-
}, 0);
|
|
305
|
-
|
|
306
|
-
oldLineNum = hunks[0]?.oldStart || 1;
|
|
307
|
-
newLineNum = hunks[0]?.newStart || 1;
|
|
308
|
-
const maxNewLine = allLines.reduce((max, line) => {
|
|
309
|
-
if (line.startsWith("-")) {
|
|
310
|
-
oldLineNum++;
|
|
311
|
-
return max;
|
|
312
|
-
} else if (line.startsWith("+")) {
|
|
313
|
-
return Math.max(max, newLineNum++);
|
|
314
|
-
} else {
|
|
315
|
-
oldLineNum++;
|
|
316
|
-
newLineNum++;
|
|
317
|
-
return Math.max(max, newLineNum - 1);
|
|
318
|
-
}
|
|
319
|
-
}, 0);
|
|
320
|
-
|
|
321
|
-
const leftMaxWidth = maxOldLine.toString().length;
|
|
322
|
-
const rightMaxWidth = maxNewLine.toString().length;
|
|
323
|
-
|
|
324
|
-
return (
|
|
325
|
-
<box style={{ flexDirection: "column" }}>
|
|
326
|
-
{hunks.flatMap((patch, i) => {
|
|
327
|
-
const elements = [
|
|
328
|
-
<box
|
|
329
|
-
style={{ flexDirection: "column", paddingLeft }}
|
|
330
|
-
key={patch.newStart}
|
|
331
|
-
>
|
|
332
|
-
<StructuredDiff
|
|
333
|
-
patch={patch}
|
|
334
|
-
splitView={splitView}
|
|
335
|
-
leftMaxWidth={leftMaxWidth}
|
|
336
|
-
rightMaxWidth={rightMaxWidth}
|
|
337
|
-
filePath={filePath}
|
|
338
|
-
/>
|
|
339
|
-
</box>,
|
|
340
|
-
];
|
|
341
|
-
if (i < hunks.length - 1) {
|
|
342
|
-
elements.push(
|
|
343
|
-
<box style={{ paddingLeft }} key={`ellipsis-${i}`}>
|
|
344
|
-
<text fg="brightBlack">{" ".repeat(leftMaxWidth + 2)}…</text>
|
|
345
|
-
</box>,
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
return elements;
|
|
349
|
-
})}
|
|
350
|
-
</box>
|
|
351
|
-
);
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
function calculateSimilarity(str1: string, str2: string): number {
|
|
355
|
-
const longer = str1.length > str2.length ? str1 : str2;
|
|
356
|
-
const shorter = str1.length > str2.length ? str2 : str1;
|
|
357
|
-
|
|
358
|
-
if (longer.length === 0) return 1.0;
|
|
359
|
-
|
|
360
|
-
const editDistance = levenshteinDistance(longer, shorter);
|
|
361
|
-
return (longer.length - editDistance) / longer.length;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function levenshteinDistance(str1: string, str2: string): number {
|
|
365
|
-
const len1 = str1.length;
|
|
366
|
-
const len2 = str2.length;
|
|
367
|
-
const matrix: number[][] = [];
|
|
368
|
-
|
|
369
|
-
for (let i = 0; i <= len1; i++) {
|
|
370
|
-
matrix[i] = [i];
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
for (let j = 0; j <= len2; j++) {
|
|
374
|
-
matrix[0]![j] = j;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
for (let i = 1; i <= len1; i++) {
|
|
378
|
-
for (let j = 1; j <= len2; j++) {
|
|
379
|
-
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
380
|
-
matrix[i]![j] = Math.min(
|
|
381
|
-
matrix[i - 1]![j]! + 1,
|
|
382
|
-
matrix[i]![j - 1]! + 1,
|
|
383
|
-
matrix[i - 1]![j - 1]! + cost,
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return matrix[len1]![len2]!;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const StructuredDiff = ({
|
|
392
|
-
patch,
|
|
393
|
-
splitView = true,
|
|
394
|
-
leftMaxWidth = 0,
|
|
395
|
-
rightMaxWidth = 0,
|
|
396
|
-
filePath = "",
|
|
397
|
-
}: {
|
|
398
|
-
patch: Hunk;
|
|
399
|
-
splitView?: boolean;
|
|
400
|
-
leftMaxWidth?: number;
|
|
401
|
-
rightMaxWidth?: number;
|
|
402
|
-
filePath?: string;
|
|
403
|
-
}) => {
|
|
404
|
-
const formatDiff = (
|
|
405
|
-
lines: string[],
|
|
406
|
-
startingLineNumber: number,
|
|
407
|
-
isSplitView: boolean,
|
|
408
|
-
) => {
|
|
409
|
-
const processedLines = lines.map((code) => {
|
|
410
|
-
if (code.startsWith("+")) {
|
|
411
|
-
return { code: code.slice(1), type: "add", originalCode: code };
|
|
412
|
-
}
|
|
413
|
-
if (code.startsWith("-")) {
|
|
414
|
-
return {
|
|
415
|
-
code: code.slice(1),
|
|
416
|
-
type: "remove",
|
|
417
|
-
originalCode: code,
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
return { code: code.slice(1), type: "nochange", originalCode: code };
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
const lang = detectLanguage(filePath);
|
|
424
|
-
|
|
425
|
-
let beforeState: GrammarState | undefined;
|
|
426
|
-
const beforeTokens: (ThemedToken[] | null)[] = [];
|
|
427
|
-
|
|
428
|
-
for (let idx = 0; idx < processedLines.length; idx++) {
|
|
429
|
-
const line = processedLines[idx];
|
|
430
|
-
if (!line) continue;
|
|
431
|
-
|
|
432
|
-
if (line.type === "remove" || line.type === "nochange") {
|
|
433
|
-
const result = highlighter.codeToTokens(line.code, {
|
|
434
|
-
lang,
|
|
435
|
-
theme,
|
|
436
|
-
grammarState: beforeState,
|
|
437
|
-
});
|
|
438
|
-
const tokens = result.tokens[0] || null;
|
|
439
|
-
|
|
440
|
-
beforeTokens.push(tokens);
|
|
441
|
-
beforeState = highlighter.getLastGrammarState(result.tokens);
|
|
442
|
-
} else {
|
|
443
|
-
beforeTokens.push(null);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
let afterState: GrammarState | undefined;
|
|
448
|
-
const afterTokens: (ThemedToken[] | null)[] = [];
|
|
449
|
-
|
|
450
|
-
for (const line of processedLines) {
|
|
451
|
-
if (line.type === "add" || line.type === "nochange") {
|
|
452
|
-
const result = highlighter.codeToTokens(line.code, {
|
|
453
|
-
lang,
|
|
454
|
-
theme,
|
|
455
|
-
grammarState: afterState,
|
|
456
|
-
});
|
|
457
|
-
const tokens = result.tokens[0] || null;
|
|
458
|
-
afterTokens.push(tokens);
|
|
459
|
-
afterState = highlighter.getLastGrammarState(result.tokens);
|
|
460
|
-
} else {
|
|
461
|
-
afterTokens.push(null);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Check if hunk is fully additions or fully deletions
|
|
466
|
-
const hasRemovals = processedLines.some((line) => line.type === "remove");
|
|
467
|
-
const hasAdditions = processedLines.some((line) => line.type === "add");
|
|
468
|
-
const shouldShowWordDiff = hasRemovals && hasAdditions;
|
|
469
|
-
|
|
470
|
-
// Find pairs of removed/added lines for word-level diff (only if hunk has both)
|
|
471
|
-
const linePairs: Array<{ remove?: number; add?: number }> = [];
|
|
472
|
-
if (shouldShowWordDiff) {
|
|
473
|
-
let i = 0;
|
|
474
|
-
while (i < processedLines.length) {
|
|
475
|
-
if (processedLines[i]?.type === "remove") {
|
|
476
|
-
// Collect all consecutive removes
|
|
477
|
-
const removes: number[] = [];
|
|
478
|
-
let j = i;
|
|
479
|
-
while (
|
|
480
|
-
j < processedLines.length &&
|
|
481
|
-
processedLines[j]?.type === "remove"
|
|
482
|
-
) {
|
|
483
|
-
removes.push(j);
|
|
484
|
-
j++;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Collect all consecutive adds that follow
|
|
488
|
-
const adds: number[] = [];
|
|
489
|
-
while (
|
|
490
|
-
j < processedLines.length &&
|
|
491
|
-
processedLines[j]?.type === "add"
|
|
492
|
-
) {
|
|
493
|
-
adds.push(j);
|
|
494
|
-
j++;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Pair them up
|
|
498
|
-
const minLength = Math.min(removes.length, adds.length);
|
|
499
|
-
for (let k = 0; k < minLength; k++) {
|
|
500
|
-
linePairs.push({ remove: removes[k], add: adds[k] });
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
i = j;
|
|
504
|
-
} else {
|
|
505
|
-
i++;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
let oldLineNumber = startingLineNumber;
|
|
511
|
-
let newLineNumber = startingLineNumber;
|
|
512
|
-
const result: Array<{
|
|
513
|
-
code: any;
|
|
514
|
-
type: string;
|
|
515
|
-
oldLineNumber: number;
|
|
516
|
-
newLineNumber: number;
|
|
517
|
-
pairedWith?: number;
|
|
518
|
-
}> = [];
|
|
519
|
-
|
|
520
|
-
for (let i = 0; i < processedLines.length; i++) {
|
|
521
|
-
const processedLine = processedLines[i];
|
|
522
|
-
if (!processedLine) continue;
|
|
523
|
-
|
|
524
|
-
const { code, type, originalCode } = processedLine;
|
|
525
|
-
|
|
526
|
-
// Check if this line is part of a word-diff pair
|
|
527
|
-
const pair = linePairs.find((p) => p.remove === i || p.add === i);
|
|
528
|
-
|
|
529
|
-
if (pair && pair.remove === i && pair.add !== undefined) {
|
|
530
|
-
// This is a removed line with a corresponding added line
|
|
531
|
-
const removedText = processedLines[i]?.code;
|
|
532
|
-
const addedLine = processedLines[pair.add];
|
|
533
|
-
if (!removedText || !addedLine) continue;
|
|
534
|
-
|
|
535
|
-
const addedText = addedLine.code;
|
|
536
|
-
|
|
537
|
-
const similarity = calculateSimilarity(removedText, addedText);
|
|
538
|
-
const shouldSkipWordDiff = similarity < 0.5;
|
|
539
|
-
|
|
540
|
-
if (shouldSkipWordDiff) {
|
|
541
|
-
const tokens = beforeTokens[i];
|
|
542
|
-
const removedContent = tokens ? (
|
|
543
|
-
<text>{renderHighlightedTokens(tokens)}</text>
|
|
544
|
-
) : (
|
|
545
|
-
<text>{removedText}</text>
|
|
546
|
-
);
|
|
547
|
-
result.push({
|
|
548
|
-
code: removedContent,
|
|
549
|
-
type,
|
|
550
|
-
oldLineNumber,
|
|
551
|
-
newLineNumber,
|
|
552
|
-
pairedWith: pair.add,
|
|
553
|
-
});
|
|
554
|
-
oldLineNumber++;
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const wordDiff = diffWords(removedText, addedText);
|
|
559
|
-
|
|
560
|
-
const removedContent = (
|
|
561
|
-
<text>
|
|
562
|
-
{wordDiff.map((part, idx) => {
|
|
563
|
-
if (part.removed) {
|
|
564
|
-
return (
|
|
565
|
-
<span key={idx} bg={RGBA.fromInts(255, 50, 50, 100)}>
|
|
566
|
-
{part.value}
|
|
567
|
-
</span>
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
if (!part.added) {
|
|
571
|
-
return <span key={idx}>{part.value}</span>;
|
|
572
|
-
}
|
|
573
|
-
return null;
|
|
574
|
-
})}
|
|
575
|
-
</text>
|
|
576
|
-
);
|
|
577
|
-
|
|
578
|
-
result.push({
|
|
579
|
-
code: removedContent,
|
|
580
|
-
type,
|
|
581
|
-
oldLineNumber,
|
|
582
|
-
newLineNumber,
|
|
583
|
-
pairedWith: pair.add,
|
|
584
|
-
});
|
|
585
|
-
oldLineNumber++;
|
|
586
|
-
} else if (pair && pair.add === i && pair.remove !== undefined) {
|
|
587
|
-
// This is an added line with a corresponding removed line
|
|
588
|
-
const removedLine = processedLines[pair.remove];
|
|
589
|
-
const addedLine = processedLines[i];
|
|
590
|
-
if (!removedLine || !addedLine) continue;
|
|
591
|
-
|
|
592
|
-
const removedText = removedLine.code;
|
|
593
|
-
const addedText = addedLine.code;
|
|
594
|
-
|
|
595
|
-
const similarity = calculateSimilarity(removedText, addedText);
|
|
596
|
-
const shouldSkipWordDiff = similarity < 0.5;
|
|
597
|
-
|
|
598
|
-
if (shouldSkipWordDiff) {
|
|
599
|
-
const tokens = afterTokens[i];
|
|
600
|
-
const addedContent = tokens ? (
|
|
601
|
-
<text>{renderHighlightedTokens(tokens)}</text>
|
|
602
|
-
) : (
|
|
603
|
-
<text>{addedText}</text>
|
|
604
|
-
);
|
|
605
|
-
result.push({
|
|
606
|
-
code: addedContent,
|
|
607
|
-
type,
|
|
608
|
-
oldLineNumber,
|
|
609
|
-
newLineNumber,
|
|
610
|
-
pairedWith: pair.remove,
|
|
611
|
-
});
|
|
612
|
-
newLineNumber++;
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const wordDiff = diffWords(removedText, addedText);
|
|
617
|
-
|
|
618
|
-
const addedContent = (
|
|
619
|
-
<text>
|
|
620
|
-
{wordDiff.map((part, idx) => {
|
|
621
|
-
if (part.added) {
|
|
622
|
-
return (
|
|
623
|
-
<span key={idx} bg={RGBA.fromInts(0, 200, 0, 100)}>
|
|
624
|
-
{part.value}
|
|
625
|
-
</span>
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
if (!part.removed) {
|
|
629
|
-
return <span key={idx}>{part.value}</span>;
|
|
630
|
-
}
|
|
631
|
-
return null;
|
|
632
|
-
})}
|
|
633
|
-
</text>
|
|
634
|
-
);
|
|
635
|
-
|
|
636
|
-
result.push({
|
|
637
|
-
code: addedContent,
|
|
638
|
-
type,
|
|
639
|
-
oldLineNumber,
|
|
640
|
-
newLineNumber,
|
|
641
|
-
pairedWith: pair.remove,
|
|
642
|
-
});
|
|
643
|
-
newLineNumber++;
|
|
644
|
-
} else {
|
|
645
|
-
const tokens =
|
|
646
|
-
type === "remove"
|
|
647
|
-
? beforeTokens[i]
|
|
648
|
-
: type === "add"
|
|
649
|
-
? afterTokens[i]
|
|
650
|
-
: beforeTokens[i] || afterTokens[i];
|
|
651
|
-
|
|
652
|
-
const content =
|
|
653
|
-
tokens && tokens.length > 0 ? (
|
|
654
|
-
<text>{renderHighlightedTokens(tokens)}</text>
|
|
655
|
-
) : (
|
|
656
|
-
<text>{code}</text>
|
|
657
|
-
);
|
|
658
|
-
|
|
659
|
-
result.push({
|
|
660
|
-
code: content,
|
|
661
|
-
type,
|
|
662
|
-
oldLineNumber,
|
|
663
|
-
newLineNumber,
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (type === "remove") {
|
|
668
|
-
oldLineNumber++;
|
|
669
|
-
} else if (type === "add") {
|
|
670
|
-
newLineNumber++;
|
|
671
|
-
} else {
|
|
672
|
-
oldLineNumber++;
|
|
673
|
-
newLineNumber++;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
return result.map(
|
|
678
|
-
({ type, code, oldLineNumber, newLineNumber, pairedWith }, index) => {
|
|
679
|
-
return {
|
|
680
|
-
oldLineNumber: oldLineNumber.toString(),
|
|
681
|
-
newLineNumber: newLineNumber.toString(),
|
|
682
|
-
code,
|
|
683
|
-
type,
|
|
684
|
-
pairedWith,
|
|
685
|
-
key: `line-${index}`,
|
|
686
|
-
};
|
|
687
|
-
},
|
|
688
|
-
);
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
const diff = formatDiff(patch.lines, patch.oldStart, splitView);
|
|
692
|
-
|
|
693
|
-
const maxWidth = Math.max(leftMaxWidth, rightMaxWidth);
|
|
694
|
-
|
|
695
|
-
if (!splitView) {
|
|
696
|
-
const paddedDiff = diff.map((item) => ({
|
|
697
|
-
...item,
|
|
698
|
-
lineNumber:
|
|
699
|
-
item.newLineNumber && item.newLineNumber !== "0"
|
|
700
|
-
? item.newLineNumber.padStart(maxWidth)
|
|
701
|
-
: " ".repeat(maxWidth),
|
|
702
|
-
}));
|
|
703
|
-
return (
|
|
704
|
-
<>
|
|
705
|
-
{paddedDiff.map(({ lineNumber, code, type, key, newLineNumber }) => (
|
|
706
|
-
<box key={key} style={{ flexDirection: "row" }}>
|
|
707
|
-
<box
|
|
708
|
-
style={{
|
|
709
|
-
flexShrink: 0,
|
|
710
|
-
alignSelf: "stretch",
|
|
711
|
-
backgroundColor:
|
|
712
|
-
type === "add"
|
|
713
|
-
? ADDED_LINE_NUMBER_BG
|
|
714
|
-
: type === "remove"
|
|
715
|
-
? REMOVED_LINE_NUMBER_BG
|
|
716
|
-
: LINE_NUMBER_BG,
|
|
717
|
-
}}
|
|
718
|
-
onMouse={(event: MouseEvent) => {
|
|
719
|
-
if (event.type === "down") {
|
|
720
|
-
openInEditor(filePath, parseInt(newLineNumber));
|
|
721
|
-
}
|
|
722
|
-
}}
|
|
723
|
-
>
|
|
724
|
-
<text
|
|
725
|
-
selectable={false}
|
|
726
|
-
fg={
|
|
727
|
-
type === "add" || type === "remove"
|
|
728
|
-
? LINE_NUMBER_FG_BRIGHT
|
|
729
|
-
: LINE_NUMBER_FG_DIM
|
|
730
|
-
}
|
|
731
|
-
style={{ width: maxWidth + 2 }}
|
|
732
|
-
>
|
|
733
|
-
{" "}
|
|
734
|
-
{lineNumber}{" "}
|
|
735
|
-
</text>
|
|
736
|
-
</box>
|
|
737
|
-
<box
|
|
738
|
-
style={{
|
|
739
|
-
flexGrow: 1,
|
|
740
|
-
paddingLeft: 1,
|
|
741
|
-
backgroundColor:
|
|
742
|
-
type === "add"
|
|
743
|
-
? ADDED_BG_LIGHT
|
|
744
|
-
: type === "remove"
|
|
745
|
-
? REMOVED_BG_LIGHT
|
|
746
|
-
: UNCHANGED_CODE_BG,
|
|
747
|
-
}}
|
|
748
|
-
>
|
|
749
|
-
{code}
|
|
750
|
-
</box>
|
|
751
|
-
</box>
|
|
752
|
-
))}
|
|
753
|
-
</>
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Split view: separate left (removals) and right (additions)
|
|
758
|
-
// Build rows by pairing deletions with additions
|
|
759
|
-
const splitLines: Array<{
|
|
760
|
-
left: any;
|
|
761
|
-
right: any;
|
|
762
|
-
}> = [];
|
|
763
|
-
const processedIndices = new Set<number>();
|
|
764
|
-
|
|
765
|
-
for (let i = 0; i < diff.length; i++) {
|
|
766
|
-
if (processedIndices.has(i)) continue;
|
|
767
|
-
|
|
768
|
-
const line = diff[i];
|
|
769
|
-
if (!line) continue;
|
|
770
|
-
|
|
771
|
-
if (line.type === "remove" && line.pairedWith !== undefined) {
|
|
772
|
-
// This removal is paired with an addition
|
|
773
|
-
const pairedLine = diff[line.pairedWith];
|
|
774
|
-
if (pairedLine) {
|
|
775
|
-
splitLines.push({
|
|
776
|
-
left: {
|
|
777
|
-
...line,
|
|
778
|
-
lineNumber: line.oldLineNumber.padStart(leftMaxWidth),
|
|
779
|
-
},
|
|
780
|
-
right: {
|
|
781
|
-
...pairedLine,
|
|
782
|
-
lineNumber: pairedLine.newLineNumber.padStart(rightMaxWidth),
|
|
783
|
-
},
|
|
784
|
-
});
|
|
785
|
-
processedIndices.add(i);
|
|
786
|
-
processedIndices.add(line.pairedWith);
|
|
787
|
-
}
|
|
788
|
-
} else if (line.type === "add" && line.pairedWith !== undefined) {
|
|
789
|
-
// This addition is paired with a removal (already processed above)
|
|
790
|
-
continue;
|
|
791
|
-
} else if (line.type === "remove") {
|
|
792
|
-
// Unpaired removal
|
|
793
|
-
splitLines.push({
|
|
794
|
-
left: {
|
|
795
|
-
...line,
|
|
796
|
-
lineNumber: line.oldLineNumber.padStart(leftMaxWidth),
|
|
797
|
-
},
|
|
798
|
-
right: {
|
|
799
|
-
lineNumber: " ".repeat(rightMaxWidth),
|
|
800
|
-
code: <text></text>,
|
|
801
|
-
type: "empty",
|
|
802
|
-
key: `${line.key}-empty-right`,
|
|
803
|
-
},
|
|
804
|
-
});
|
|
805
|
-
processedIndices.add(i);
|
|
806
|
-
} else if (line.type === "add") {
|
|
807
|
-
// Unpaired addition
|
|
808
|
-
splitLines.push({
|
|
809
|
-
left: {
|
|
810
|
-
lineNumber: " ".repeat(leftMaxWidth),
|
|
811
|
-
code: <text></text>,
|
|
812
|
-
type: "empty",
|
|
813
|
-
key: `${line.key}-empty-left`,
|
|
814
|
-
},
|
|
815
|
-
right: {
|
|
816
|
-
...line,
|
|
817
|
-
lineNumber: line.newLineNumber.padStart(rightMaxWidth),
|
|
818
|
-
},
|
|
819
|
-
});
|
|
820
|
-
processedIndices.add(i);
|
|
821
|
-
} else {
|
|
822
|
-
// Unchanged line
|
|
823
|
-
splitLines.push({
|
|
824
|
-
left: {
|
|
825
|
-
...line,
|
|
826
|
-
lineNumber: line.oldLineNumber.padStart(leftMaxWidth),
|
|
827
|
-
},
|
|
828
|
-
right: {
|
|
829
|
-
...line,
|
|
830
|
-
lineNumber: line.newLineNumber.padStart(rightMaxWidth),
|
|
831
|
-
},
|
|
832
|
-
});
|
|
833
|
-
processedIndices.add(i);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return (
|
|
838
|
-
<>
|
|
839
|
-
{splitLines.map(({ left: leftLine, right: rightLine }) => (
|
|
840
|
-
<box key={leftLine.key} style={{ flexDirection: "row" }}>
|
|
841
|
-
{/* Left side (removals) */}
|
|
842
|
-
<box style={{ flexDirection: "row", width: "50%" }}>
|
|
843
|
-
<box
|
|
844
|
-
style={{
|
|
845
|
-
flexShrink: 0,
|
|
846
|
-
minWidth: leftMaxWidth + 2,
|
|
847
|
-
alignSelf: "stretch",
|
|
848
|
-
backgroundColor:
|
|
849
|
-
leftLine.type === "remove"
|
|
850
|
-
? REMOVED_LINE_NUMBER_BG
|
|
851
|
-
: LINE_NUMBER_BG,
|
|
852
|
-
}}
|
|
853
|
-
onMouse={(event: MouseEvent) => {
|
|
854
|
-
if (
|
|
855
|
-
event.type === "down" &&
|
|
856
|
-
leftLine.oldLineNumber &&
|
|
857
|
-
leftLine.oldLineNumber !== "0"
|
|
858
|
-
) {
|
|
859
|
-
openInEditor(filePath, parseInt(leftLine.oldLineNumber));
|
|
860
|
-
}
|
|
861
|
-
}}
|
|
862
|
-
>
|
|
863
|
-
<text
|
|
864
|
-
selectable={false}
|
|
865
|
-
fg={
|
|
866
|
-
leftLine.type === "remove"
|
|
867
|
-
? LINE_NUMBER_FG_BRIGHT
|
|
868
|
-
: LINE_NUMBER_FG_DIM
|
|
869
|
-
}
|
|
870
|
-
>
|
|
871
|
-
{" "}
|
|
872
|
-
{leftLine.lineNumber}{" "}
|
|
873
|
-
</text>
|
|
874
|
-
</box>
|
|
875
|
-
<box
|
|
876
|
-
style={{
|
|
877
|
-
flexGrow: 1,
|
|
878
|
-
paddingLeft: 1,
|
|
879
|
-
minWidth: 0,
|
|
880
|
-
backgroundColor:
|
|
881
|
-
leftLine.type === "remove"
|
|
882
|
-
? REMOVED_BG_LIGHT
|
|
883
|
-
: UNCHANGED_CODE_BG,
|
|
884
|
-
}}
|
|
885
|
-
>
|
|
886
|
-
{leftLine.code}
|
|
887
|
-
</box>
|
|
888
|
-
</box>
|
|
889
|
-
|
|
890
|
-
{/* Right side (additions) */}
|
|
891
|
-
<box style={{ flexDirection: "row", width: "50%" }}>
|
|
892
|
-
<box
|
|
893
|
-
style={{
|
|
894
|
-
flexShrink: 0,
|
|
895
|
-
minWidth: leftMaxWidth + 2,
|
|
896
|
-
alignSelf: "stretch",
|
|
897
|
-
backgroundColor:
|
|
898
|
-
rightLine.type === "add"
|
|
899
|
-
? ADDED_LINE_NUMBER_BG
|
|
900
|
-
: LINE_NUMBER_BG,
|
|
901
|
-
}}
|
|
902
|
-
onMouse={(event: MouseEvent) => {
|
|
903
|
-
if (event.type === "down") {
|
|
904
|
-
openInEditor(filePath, parseInt(rightLine.newLineNumber));
|
|
905
|
-
}
|
|
906
|
-
}}
|
|
907
|
-
>
|
|
908
|
-
<text
|
|
909
|
-
selectable={false}
|
|
910
|
-
fg={
|
|
911
|
-
rightLine.type === "add"
|
|
912
|
-
? LINE_NUMBER_FG_BRIGHT
|
|
913
|
-
: LINE_NUMBER_FG_DIM
|
|
914
|
-
}
|
|
915
|
-
>
|
|
916
|
-
{" "}
|
|
917
|
-
{rightLine.lineNumber}{" "}
|
|
918
|
-
</text>
|
|
919
|
-
</box>
|
|
920
|
-
<box
|
|
921
|
-
style={{
|
|
922
|
-
flexGrow: 1,
|
|
923
|
-
minWidth: 0,
|
|
924
|
-
paddingLeft: 1,
|
|
925
|
-
backgroundColor:
|
|
926
|
-
rightLine.type === "add"
|
|
927
|
-
? ADDED_BG_LIGHT
|
|
928
|
-
: UNCHANGED_CODE_BG,
|
|
929
|
-
}}
|
|
930
|
-
>
|
|
931
|
-
{rightLine.code}
|
|
932
|
-
</box>
|
|
933
|
-
</box>
|
|
934
|
-
</box>
|
|
935
|
-
))}
|
|
936
|
-
</>
|
|
937
|
-
);
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
export { ErrorBoundary };
|