pauloj commited on
Commit
36872ee
·
1 Parent(s): b3ec53f

refactor: Enhance Diff View with advanced line and character-level change detection

Browse files

- Improved diff algorithm to detect more granular line and character-level changes
- Added support for character-level highlighting in diff view
- Simplified diff view mode by removing side-by-side option
- Updated component rendering to support more detailed change visualization
- Optimized line change detection with improved matching strategy

app/components/workbench/DiffView.tsx CHANGED
@@ -25,6 +25,10 @@ interface DiffBlock {
25
  content: string;
26
  type: 'added' | 'removed' | 'unchanged';
27
  correspondingLine?: number;
 
 
 
 
28
  }
29
 
30
  interface FullscreenButtonProps {
@@ -74,93 +78,211 @@ const processChanges = (beforeCode: string, afterCode: string) => {
74
  };
75
  }
76
 
77
- // Normalizar quebras de linha para evitar falsos positivos
78
- const normalizedBefore = beforeCode.replace(/\r\n/g, '\n');
79
- const normalizedAfter = afterCode.replace(/\r\n/g, '\n');
 
 
 
 
80
 
81
- // Dividir em linhas preservando linhas vazias
82
- const beforeLines = normalizedBefore.split('\n');
83
- const afterLines = normalizedAfter.split('\n');
84
 
85
- // Se os conteúdos são idênticos após normalização, não há mudanças
86
- if (normalizedBefore === normalizedAfter) {
87
  return {
88
  beforeLines,
89
  afterLines,
90
  hasChanges: false,
91
  lineChanges: { before: new Set(), after: new Set() },
92
- unifiedBlocks: []
 
93
  };
94
  }
95
 
96
- // Processar as diferenças com configurações otimizadas para detecção por linha
97
- const changes = diffLines(normalizedBefore, normalizedAfter, {
98
- newlineIsToken: false, // Não tratar quebras de linha como tokens separados
99
- ignoreWhitespace: true, // Ignorar diferenças de espaços em branco
100
- ignoreCase: false // Manter sensibilidade a maiúsculas/minúsculas
101
- });
102
-
103
  const lineChanges = {
104
  before: new Set<number>(),
105
  after: new Set<number>()
106
  };
107
 
108
- let beforeLineNumber = 0;
109
- let afterLineNumber = 0;
110
-
111
- const unifiedBlocks = changes.reduce((blocks: DiffBlock[], change) => {
112
- // Dividir o conteúdo em linhas preservando linhas vazias
113
- const lines = change.value.split('\n');
114
-
115
- if (change.added) {
116
- // Processar linhas adicionadas
117
- const addedBlocks = lines.map((line, i) => {
118
- lineChanges.after.add(afterLineNumber + i);
119
- return {
120
- lineNumber: afterLineNumber + i,
121
- content: line,
122
- type: 'added' as const
123
- };
124
  });
125
- afterLineNumber += lines.length;
126
- return [...blocks, ...addedBlocks];
127
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
- if (change.removed) {
130
- // Processar linhas removidas
131
- const removedBlocks = lines.map((line, i) => {
132
- lineChanges.before.add(beforeLineNumber + i);
133
- return {
134
- lineNumber: beforeLineNumber + i,
135
- content: line,
136
- type: 'removed' as const
137
- };
138
- });
139
- beforeLineNumber += lines.length;
140
- return [...blocks, ...removedBlocks];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
 
142
 
143
- // Processar linhas não modificadas
144
- const unchangedBlocks = lines.map((line, i) => {
145
- const block = {
146
- lineNumber: afterLineNumber + i,
147
- content: line,
148
- type: 'unchanged' as const,
149
- correspondingLine: beforeLineNumber + i
150
- };
151
- return block;
152
- });
153
- beforeLineNumber += lines.length;
154
- afterLineNumber += lines.length;
155
- return [...blocks, ...unchangedBlocks];
156
- }, []);
157
 
158
  return {
159
  beforeLines,
160
  afterLines,
161
  hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0,
162
  lineChanges,
163
- unifiedBlocks,
164
  isBinary: false
165
  };
166
  } catch (error) {
@@ -177,8 +299,14 @@ const processChanges = (beforeCode: string, afterCode: string) => {
177
  }
178
  };
179
 
180
- const lineNumberStyles = "w-12 shrink-0 pl-2 py-0.5 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1";
181
- const lineContentStyles = "px-4 py-0.5 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary";
 
 
 
 
 
 
182
 
183
  const renderContentWarning = (type: 'binary' | 'error') => (
184
  <div className="h-full flex items-center justify-center p-4">
@@ -243,13 +371,15 @@ const CodeLine = memo(({
243
  content,
244
  type,
245
  highlighter,
246
- language
 
247
  }: {
248
  lineNumber: number;
249
  content: string;
250
  type: 'added' | 'removed' | 'unchanged';
251
  highlighter: any;
252
  language: string;
 
253
  }) => {
254
  const bgColor = {
255
  added: 'bg-green-500/20 border-l-4 border-green-500',
@@ -257,13 +387,42 @@ const CodeLine = memo(({
257
  unchanged: ''
258
  }[type];
259
 
260
- const highlightedCode = useMemo(() => {
261
- if (!highlighter) return content;
262
- return highlighter.codeToHtml(content, {
263
- lang: language,
264
- theme: 'github-dark'
265
- }).replace(/<\/?pre[^>]*>/g, '').replace(/<\/?code[^>]*>/g, '');
266
- }, [content, highlighter, language]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
  return (
269
  <div className="flex group min-w-fit">
@@ -274,7 +433,7 @@ const CodeLine = memo(({
274
  {type === 'removed' && '-'}
275
  {type === 'unchanged' && ' '}
276
  </span>
277
- <span dangerouslySetInnerHTML={{ __html: highlightedCode }} />
278
  </div>
279
  </div>
280
  );
@@ -380,9 +539,9 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
380
  beforeCode={beforeCode}
381
  afterCode={afterCode}
382
  />
383
- <div className="flex-1 overflow-auto diff-panel-content">
384
  {hasChanges ? (
385
- <div className="overflow-x-auto">
386
  {unifiedBlocks.map((block, index) => (
387
  <CodeLine
388
  key={`${block.lineNumber}-${index}`}
@@ -391,6 +550,7 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
391
  type={block.type}
392
  highlighter={highlighter}
393
  language={language}
 
394
  />
395
  ))}
396
  </div>
@@ -407,103 +567,13 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language,
407
  );
408
  });
409
 
410
- const SideBySideComparison = memo(({
411
- beforeCode,
412
- afterCode,
413
- language,
414
- filename,
415
- lightTheme,
416
- darkTheme,
417
- }: CodeComparisonProps) => {
418
- const [isFullscreen, setIsFullscreen] = useState(false);
419
- const [highlighter, setHighlighter] = useState<any>(null);
420
-
421
- const toggleFullscreen = useCallback(() => {
422
- setIsFullscreen(prev => !prev);
423
- }, []);
424
-
425
- const { beforeLines, afterLines, hasChanges, lineChanges, isBinary, error } = useProcessChanges(beforeCode, afterCode);
426
-
427
- useEffect(() => {
428
- getHighlighter({
429
- themes: ['github-dark'],
430
- langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx']
431
- }).then(setHighlighter);
432
- }, []);
433
-
434
- if (isBinary || error) return renderContentWarning(isBinary ? 'binary' : 'error');
435
-
436
- const renderCode = (code: string) => {
437
- if (!highlighter) return code;
438
- const highlightedCode = highlighter.codeToHtml(code, {
439
- lang: language,
440
- theme: 'github-dark'
441
- });
442
- return highlightedCode.replace(/<\/?pre[^>]*>/g, '').replace(/<\/?code[^>]*>/g, '');
443
- };
444
-
445
- return (
446
- <FullscreenOverlay isFullscreen={isFullscreen}>
447
- <div className="w-full h-full flex flex-col">
448
- <FileInfo
449
- filename={filename}
450
- hasChanges={hasChanges}
451
- onToggleFullscreen={toggleFullscreen}
452
- isFullscreen={isFullscreen}
453
- beforeCode={beforeCode}
454
- afterCode={afterCode}
455
- />
456
- <div className="flex-1 overflow-auto diff-panel-content">
457
- {hasChanges ? (
458
- <div className="grid md:grid-cols-2 divide-x divide-bolt-elements-borderColor relative h-full">
459
- <div className="overflow-auto">
460
- {beforeLines.map((line, index) => (
461
- <div key={`before-${index}`} className="flex group min-w-fit">
462
- <div className={lineNumberStyles}>{index + 1}</div>
463
- <div className={`${lineContentStyles} ${lineChanges.before.has(index) ? 'bg-red-500/20 border-l-4 border-red-500' : ''}`}>
464
- <span className="mr-2 text-bolt-elements-textTertiary">
465
- {lineChanges.before.has(index) ? '-' : ' '}
466
- </span>
467
- <span dangerouslySetInnerHTML={{ __html: renderCode(line) }} />
468
- </div>
469
- </div>
470
- ))}
471
- </div>
472
- <div className="overflow-auto">
473
- {afterLines.map((line, index) => (
474
- <div key={`after-${index}`} className="flex group min-w-fit">
475
- <div className={lineNumberStyles}>{index + 1}</div>
476
- <div className={`${lineContentStyles} ${lineChanges.after.has(index) ? 'bg-green-500/20 border-l-4 border-green-500' : ''}`}>
477
- <span className="mr-2 text-bolt-elements-textTertiary">
478
- {lineChanges.after.has(index) ? '+' : ' '}
479
- </span>
480
- <span dangerouslySetInnerHTML={{ __html: renderCode(line) }} />
481
- </div>
482
- </div>
483
- ))}
484
- </div>
485
- </div>
486
- ) : (
487
- <NoChangesView
488
- beforeCode={beforeCode}
489
- language={language}
490
- highlighter={highlighter}
491
- />
492
- )}
493
- </div>
494
- </div>
495
- </FullscreenOverlay>
496
- );
497
- });
498
-
499
  interface DiffViewProps {
500
  fileHistory: Record<string, FileHistory>;
501
  setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
502
- diffViewMode: 'inline' | 'side';
503
  actionRunner: ActionRunner;
504
  }
505
 
506
- export const DiffView = memo(({ fileHistory, setFileHistory, diffViewMode, actionRunner }: DiffViewProps) => {
507
  const files = useStore(workbenchStore.files) as FileMap;
508
  const selectedFile = useStore(workbenchStore.selectedFile);
509
  const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument;
@@ -612,25 +682,14 @@ export const DiffView = memo(({ fileHistory, setFileHistory, diffViewMode, actio
612
  try {
613
  return (
614
  <div className="h-full overflow-hidden">
615
- {diffViewMode === 'inline' ? (
616
- <InlineDiffComparison
617
- beforeCode={effectiveOriginalContent}
618
- afterCode={currentContent}
619
- language={language}
620
- filename={selectedFile}
621
- lightTheme="github-light"
622
- darkTheme="github-dark"
623
- />
624
- ) : (
625
- <SideBySideComparison
626
- beforeCode={effectiveOriginalContent}
627
- afterCode={currentContent}
628
- language={language}
629
- filename={selectedFile}
630
- lightTheme="github-light"
631
- darkTheme="github-dark"
632
- />
633
- )}
634
  </div>
635
  );
636
  } catch (error) {
 
25
  content: string;
26
  type: 'added' | 'removed' | 'unchanged';
27
  correspondingLine?: number;
28
+ charChanges?: Array<{
29
+ value: string;
30
+ type: 'added' | 'removed' | 'unchanged';
31
+ }>;
32
  }
33
 
34
  interface FullscreenButtonProps {
 
78
  };
79
  }
80
 
81
+ // Normalize line endings and content
82
+ const normalizeContent = (content: string): string[] => {
83
+ return content
84
+ .replace(/\r\n/g, '\n')
85
+ .split('\n')
86
+ .map(line => line.trimEnd());
87
+ };
88
 
89
+ const beforeLines = normalizeContent(beforeCode);
90
+ const afterLines = normalizeContent(afterCode);
 
91
 
92
+ // Early return if files are identical
93
+ if (beforeLines.join('\n') === afterLines.join('\n')) {
94
  return {
95
  beforeLines,
96
  afterLines,
97
  hasChanges: false,
98
  lineChanges: { before: new Set(), after: new Set() },
99
+ unifiedBlocks: [],
100
+ isBinary: false
101
  };
102
  }
103
 
 
 
 
 
 
 
 
104
  const lineChanges = {
105
  before: new Set<number>(),
106
  after: new Set<number>()
107
  };
108
 
109
+ const unifiedBlocks: DiffBlock[] = [];
110
+
111
+ // Compare lines directly for more accurate diff
112
+ let i = 0, j = 0;
113
+ while (i < beforeLines.length || j < afterLines.length) {
114
+ if (i < beforeLines.length && j < afterLines.length && beforeLines[i] === afterLines[j]) {
115
+ // Unchanged line
116
+ unifiedBlocks.push({
117
+ lineNumber: j,
118
+ content: afterLines[j],
119
+ type: 'unchanged',
120
+ correspondingLine: i
 
 
 
 
121
  });
122
+ i++;
123
+ j++;
124
+ } else {
125
+ // Look ahead for potential matches
126
+ let matchFound = false;
127
+ const lookAhead = 3; // Number of lines to look ahead
128
+
129
+ // Try to find matching lines ahead
130
+ for (let k = 1; k <= lookAhead && i + k < beforeLines.length && j + k < afterLines.length; k++) {
131
+ if (beforeLines[i + k] === afterLines[j]) {
132
+ // Found match in after lines - mark lines as removed
133
+ for (let l = 0; l < k; l++) {
134
+ lineChanges.before.add(i + l);
135
+ unifiedBlocks.push({
136
+ lineNumber: i + l,
137
+ content: beforeLines[i + l],
138
+ type: 'removed',
139
+ correspondingLine: j,
140
+ charChanges: [{ value: beforeLines[i + l], type: 'removed' }]
141
+ });
142
+ }
143
+ i += k;
144
+ matchFound = true;
145
+ break;
146
+ } else if (beforeLines[i] === afterLines[j + k]) {
147
+ // Found match in before lines - mark lines as added
148
+ for (let l = 0; l < k; l++) {
149
+ lineChanges.after.add(j + l);
150
+ unifiedBlocks.push({
151
+ lineNumber: j + l,
152
+ content: afterLines[j + l],
153
+ type: 'added',
154
+ correspondingLine: i,
155
+ charChanges: [{ value: afterLines[j + l], type: 'added' }]
156
+ });
157
+ }
158
+ j += k;
159
+ matchFound = true;
160
+ break;
161
+ }
162
+ }
163
 
164
+ if (!matchFound) {
165
+ // No match found - try to find character-level changes
166
+ if (i < beforeLines.length && j < afterLines.length) {
167
+ const beforeLine = beforeLines[i];
168
+ const afterLine = afterLines[j];
169
+
170
+ // Find common prefix and suffix
171
+ let prefixLength = 0;
172
+ while (prefixLength < beforeLine.length &&
173
+ prefixLength < afterLine.length &&
174
+ beforeLine[prefixLength] === afterLine[prefixLength]) {
175
+ prefixLength++;
176
+ }
177
+
178
+ let suffixLength = 0;
179
+ while (suffixLength < beforeLine.length - prefixLength &&
180
+ suffixLength < afterLine.length - prefixLength &&
181
+ beforeLine[beforeLine.length - 1 - suffixLength] ===
182
+ afterLine[afterLine.length - 1 - suffixLength]) {
183
+ suffixLength++;
184
+ }
185
+
186
+ const prefix = beforeLine.slice(0, prefixLength);
187
+ const beforeMiddle = beforeLine.slice(prefixLength, beforeLine.length - suffixLength);
188
+ const afterMiddle = afterLine.slice(prefixLength, afterLine.length - suffixLength);
189
+ const suffix = beforeLine.slice(beforeLine.length - suffixLength);
190
+
191
+ if (beforeMiddle || afterMiddle) {
192
+ // There are character-level changes
193
+ if (beforeMiddle) {
194
+ lineChanges.before.add(i);
195
+ unifiedBlocks.push({
196
+ lineNumber: i,
197
+ content: beforeLine,
198
+ type: 'removed',
199
+ correspondingLine: j,
200
+ charChanges: [
201
+ { value: prefix, type: 'unchanged' },
202
+ { value: beforeMiddle, type: 'removed' },
203
+ { value: suffix, type: 'unchanged' }
204
+ ]
205
+ });
206
+ i++;
207
+ }
208
+ if (afterMiddle) {
209
+ lineChanges.after.add(j);
210
+ unifiedBlocks.push({
211
+ lineNumber: j,
212
+ content: afterLine,
213
+ type: 'added',
214
+ correspondingLine: i - 1,
215
+ charChanges: [
216
+ { value: prefix, type: 'unchanged' },
217
+ { value: afterMiddle, type: 'added' },
218
+ { value: suffix, type: 'unchanged' }
219
+ ]
220
+ });
221
+ j++;
222
+ }
223
+ } else {
224
+ // No character-level changes found, treat as regular line changes
225
+ if (i < beforeLines.length) {
226
+ lineChanges.before.add(i);
227
+ unifiedBlocks.push({
228
+ lineNumber: i,
229
+ content: beforeLines[i],
230
+ type: 'removed',
231
+ correspondingLine: j,
232
+ charChanges: [{ value: beforeLines[i], type: 'removed' }]
233
+ });
234
+ i++;
235
+ }
236
+ if (j < afterLines.length) {
237
+ lineChanges.after.add(j);
238
+ unifiedBlocks.push({
239
+ lineNumber: j,
240
+ content: afterLines[j],
241
+ type: 'added',
242
+ correspondingLine: i - 1,
243
+ charChanges: [{ value: afterLines[j], type: 'added' }]
244
+ });
245
+ j++;
246
+ }
247
+ }
248
+ } else {
249
+ // Handle remaining lines
250
+ if (i < beforeLines.length) {
251
+ lineChanges.before.add(i);
252
+ unifiedBlocks.push({
253
+ lineNumber: i,
254
+ content: beforeLines[i],
255
+ type: 'removed',
256
+ correspondingLine: j,
257
+ charChanges: [{ value: beforeLines[i], type: 'removed' }]
258
+ });
259
+ i++;
260
+ }
261
+ if (j < afterLines.length) {
262
+ lineChanges.after.add(j);
263
+ unifiedBlocks.push({
264
+ lineNumber: j,
265
+ content: afterLines[j],
266
+ type: 'added',
267
+ correspondingLine: i - 1,
268
+ charChanges: [{ value: afterLines[j], type: 'added' }]
269
+ });
270
+ j++;
271
+ }
272
+ }
273
+ }
274
  }
275
+ }
276
 
277
+ // Sort blocks by line number
278
+ const processedBlocks = unifiedBlocks.sort((a, b) => a.lineNumber - b.lineNumber);
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
  return {
281
  beforeLines,
282
  afterLines,
283
  hasChanges: lineChanges.before.size > 0 || lineChanges.after.size > 0,
284
  lineChanges,
285
+ unifiedBlocks: processedBlocks,
286
  isBinary: false
287
  };
288
  } catch (error) {
 
299
  }
300
  };
301
 
302
+ const lineNumberStyles = "w-9 shrink-0 pl-2 py-1 text-left font-mono text-bolt-elements-textTertiary border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1";
303
+ const lineContentStyles = "px-1 py-1 font-mono whitespace-pre flex-1 group-hover:bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary";
304
+ const diffPanelStyles = "h-full overflow-auto diff-panel-content";
305
+ const diffLineStyles = {
306
+ added: 'bg-green-500/20 border-l-4 border-green-500',
307
+ removed: 'bg-red-500/20 border-l-4 border-red-500',
308
+ unchanged: ''
309
+ };
310
 
311
  const renderContentWarning = (type: 'binary' | 'error') => (
312
  <div className="h-full flex items-center justify-center p-4">
 
371
  content,
372
  type,
373
  highlighter,
374
+ language,
375
+ block
376
  }: {
377
  lineNumber: number;
378
  content: string;
379
  type: 'added' | 'removed' | 'unchanged';
380
  highlighter: any;
381
  language: string;
382
+ block: DiffBlock;
383
  }) => {
384
  const bgColor = {
385
  added: 'bg-green-500/20 border-l-4 border-green-500',
 
387
  unchanged: ''
388
  }[type];
389
 
390
+ const renderContent = () => {
391
+ if (type === 'unchanged' || !block.charChanges) {
392
+ const highlightedCode = highlighter ?
393
+ highlighter.codeToHtml(content, { lang: language, theme: 'github-dark' })
394
+ .replace(/<\/?pre[^>]*>/g, '')
395
+ .replace(/<\/?code[^>]*>/g, '')
396
+ : content;
397
+ return <span dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
398
+ }
399
+
400
+ return (
401
+ <>
402
+ {block.charChanges.map((change, index) => {
403
+ const changeClass = {
404
+ added: 'text-green-500 bg-green-500/20',
405
+ removed: 'text-red-500 bg-red-500/20',
406
+ unchanged: ''
407
+ }[change.type];
408
+
409
+ const highlightedCode = highlighter ?
410
+ highlighter.codeToHtml(change.value, { lang: language, theme: 'github-dark' })
411
+ .replace(/<\/?pre[^>]*>/g, '')
412
+ .replace(/<\/?code[^>]*>/g, '')
413
+ : change.value;
414
+
415
+ return (
416
+ <span
417
+ key={index}
418
+ className={changeClass}
419
+ dangerouslySetInnerHTML={{ __html: highlightedCode }}
420
+ />
421
+ );
422
+ })}
423
+ </>
424
+ );
425
+ };
426
 
427
  return (
428
  <div className="flex group min-w-fit">
 
433
  {type === 'removed' && '-'}
434
  {type === 'unchanged' && ' '}
435
  </span>
436
+ {renderContent()}
437
  </div>
438
  </div>
439
  );
 
539
  beforeCode={beforeCode}
540
  afterCode={afterCode}
541
  />
542
+ <div className={diffPanelStyles}>
543
  {hasChanges ? (
544
+ <div className="overflow-x-auto min-w-full">
545
  {unifiedBlocks.map((block, index) => (
546
  <CodeLine
547
  key={`${block.lineNumber}-${index}`}
 
550
  type={block.type}
551
  highlighter={highlighter}
552
  language={language}
553
+ block={block}
554
  />
555
  ))}
556
  </div>
 
567
  );
568
  });
569
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  interface DiffViewProps {
571
  fileHistory: Record<string, FileHistory>;
572
  setFileHistory: React.Dispatch<React.SetStateAction<Record<string, FileHistory>>>;
 
573
  actionRunner: ActionRunner;
574
  }
575
 
576
+ export const DiffView = memo(({ fileHistory, setFileHistory, actionRunner }: DiffViewProps) => {
577
  const files = useStore(workbenchStore.files) as FileMap;
578
  const selectedFile = useStore(workbenchStore.selectedFile);
579
  const currentDocument = useStore(workbenchStore.currentDocument) as EditorDocument;
 
682
  try {
683
  return (
684
  <div className="h-full overflow-hidden">
685
+ <InlineDiffComparison
686
+ beforeCode={effectiveOriginalContent}
687
+ afterCode={currentContent}
688
+ language={language}
689
+ filename={selectedFile}
690
+ lightTheme="github-light"
691
+ darkTheme="github-dark"
692
+ />
 
 
 
 
 
 
 
 
 
 
 
693
  </div>
694
  );
695
  } catch (error) {
app/components/workbench/Workbench.client.tsx CHANGED
@@ -74,13 +74,9 @@ const workbenchVariants = {
74
  const FileModifiedDropdown = memo(({
75
  fileHistory,
76
  onSelectFile,
77
- diffViewMode,
78
- toggleDiffViewMode,
79
  }: {
80
  fileHistory: Record<string, FileHistory>,
81
  onSelectFile: (filePath: string) => void,
82
- diffViewMode: 'inline' | 'side',
83
- toggleDiffViewMode: () => void,
84
  }) => {
85
  const modifiedFiles = Object.entries(fileHistory);
86
  const hasChanges = modifiedFiles.length > 0;
@@ -251,12 +247,6 @@ const FileModifiedDropdown = memo(({
251
  </>
252
  )}
253
  </Popover>
254
- <button
255
- onClick={(e) => { e.stopPropagation(); toggleDiffViewMode(); }}
256
- className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg bg-bolt-elements-background-depth-2 hover:bg-bolt-elements-background-depth-3 transition-colors text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
257
- >
258
- <span className="font-medium">{diffViewMode === 'inline' ? 'Inline' : 'Side by Side'}</span>
259
- </button>
260
  </div>
261
  );
262
  });
@@ -272,7 +262,6 @@ export const Workbench = memo(({
272
 
273
  const [isSyncing, setIsSyncing] = useState(false);
274
  const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
275
- const [diffViewMode, setDiffViewMode] = useState<'inline' | 'side'>('inline');
276
  const [fileHistory, setFileHistory] = useState<Record<string, FileHistory>>({});
277
 
278
  const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
@@ -343,10 +332,6 @@ export const Workbench = memo(({
343
  workbenchStore.currentView.set('diff');
344
  }, []);
345
 
346
- const toggleDiffViewMode = useCallback(() => {
347
- setDiffViewMode(prev => prev === 'inline' ? 'side' : 'inline');
348
- }, []);
349
-
350
  return (
351
  chatStarted && (
352
  <motion.div
@@ -405,8 +390,6 @@ export const Workbench = memo(({
405
  <FileModifiedDropdown
406
  fileHistory={fileHistory}
407
  onSelectFile={handleSelectFile}
408
- diffViewMode={diffViewMode}
409
- toggleDiffViewMode={toggleDiffViewMode}
410
  />
411
  )}
412
  <IconButton
@@ -444,7 +427,6 @@ export const Workbench = memo(({
444
  <DiffView
445
  fileHistory={fileHistory}
446
  setFileHistory={setFileHistory}
447
- diffViewMode={diffViewMode}
448
  actionRunner={actionRunner}
449
  />
450
  </View>
 
74
  const FileModifiedDropdown = memo(({
75
  fileHistory,
76
  onSelectFile,
 
 
77
  }: {
78
  fileHistory: Record<string, FileHistory>,
79
  onSelectFile: (filePath: string) => void,
 
 
80
  }) => {
81
  const modifiedFiles = Object.entries(fileHistory);
82
  const hasChanges = modifiedFiles.length > 0;
 
247
  </>
248
  )}
249
  </Popover>
 
 
 
 
 
 
250
  </div>
251
  );
252
  });
 
262
 
263
  const [isSyncing, setIsSyncing] = useState(false);
264
  const [isPushDialogOpen, setIsPushDialogOpen] = useState(false);
 
265
  const [fileHistory, setFileHistory] = useState<Record<string, FileHistory>>({});
266
 
267
  const modifiedFiles = Array.from(useStore(workbenchStore.unsavedFiles).keys());
 
332
  workbenchStore.currentView.set('diff');
333
  }, []);
334
 
 
 
 
 
335
  return (
336
  chatStarted && (
337
  <motion.div
 
390
  <FileModifiedDropdown
391
  fileHistory={fileHistory}
392
  onSelectFile={handleSelectFile}
 
 
393
  />
394
  )}
395
  <IconButton
 
427
  <DiffView
428
  fileHistory={fileHistory}
429
  setFileHistory={setFileHistory}
 
430
  actionRunner={actionRunner}
431
  />
432
  </View>
package.json CHANGED
@@ -74,12 +74,11 @@
74
  "@radix-ui/react-switch": "^1.1.1",
75
  "@radix-ui/react-tabs": "^1.1.2",
76
  "@radix-ui/react-tooltip": "^1.1.4",
77
- "lucide-react": "^0.474.0",
78
- "next-themes": "^0.4.4",
79
  "@remix-run/cloudflare": "^2.15.2",
80
  "@remix-run/cloudflare-pages": "^2.15.2",
81
  "@remix-run/node": "^2.15.2",
82
  "@remix-run/react": "^2.15.2",
 
83
  "@types/react-beautiful-dnd": "^13.1.8",
84
  "@uiw/codemirror-theme-vscode": "^4.23.6",
85
  "@unocss/reset": "^0.61.9",
@@ -105,7 +104,9 @@
105
  "js-cookie": "^3.0.5",
106
  "jspdf": "^2.5.2",
107
  "jszip": "^3.10.1",
 
108
  "nanostores": "^0.10.3",
 
109
  "ollama-ai-provider": "^0.15.2",
110
  "path-browserify": "^1.0.1",
111
  "react": "^18.3.1",
@@ -135,6 +136,8 @@
135
  "@iconify-json/ph": "^1.2.1",
136
  "@iconify/types": "^2.0.0",
137
  "@remix-run/dev": "^2.15.2",
 
 
138
  "@types/diff": "^5.2.3",
139
  "@types/dom-speech-recognition": "^0.0.4",
140
  "@types/file-saver": "^2.0.7",
@@ -142,9 +145,11 @@
142
  "@types/path-browserify": "^1.0.3",
143
  "@types/react": "^18.3.12",
144
  "@types/react-dom": "^18.3.1",
 
145
  "fast-glob": "^3.3.2",
146
  "husky": "9.1.7",
147
  "is-ci": "^3.0.1",
 
148
  "node-fetch": "^3.3.2",
149
  "pnpm": "^9.14.4",
150
  "prettier": "^3.4.1",
 
74
  "@radix-ui/react-switch": "^1.1.1",
75
  "@radix-ui/react-tabs": "^1.1.2",
76
  "@radix-ui/react-tooltip": "^1.1.4",
 
 
77
  "@remix-run/cloudflare": "^2.15.2",
78
  "@remix-run/cloudflare-pages": "^2.15.2",
79
  "@remix-run/node": "^2.15.2",
80
  "@remix-run/react": "^2.15.2",
81
+ "@tanstack/react-virtual": "^3.13.0",
82
  "@types/react-beautiful-dnd": "^13.1.8",
83
  "@uiw/codemirror-theme-vscode": "^4.23.6",
84
  "@unocss/reset": "^0.61.9",
 
104
  "js-cookie": "^3.0.5",
105
  "jspdf": "^2.5.2",
106
  "jszip": "^3.10.1",
107
+ "lucide-react": "^0.474.0",
108
  "nanostores": "^0.10.3",
109
+ "next-themes": "^0.4.4",
110
  "ollama-ai-provider": "^0.15.2",
111
  "path-browserify": "^1.0.1",
112
  "react": "^18.3.1",
 
136
  "@iconify-json/ph": "^1.2.1",
137
  "@iconify/types": "^2.0.0",
138
  "@remix-run/dev": "^2.15.2",
139
+ "@testing-library/jest-dom": "^6.6.3",
140
+ "@testing-library/react": "^16.2.0",
141
  "@types/diff": "^5.2.3",
142
  "@types/dom-speech-recognition": "^0.0.4",
143
  "@types/file-saver": "^2.0.7",
 
145
  "@types/path-browserify": "^1.0.3",
146
  "@types/react": "^18.3.12",
147
  "@types/react-dom": "^18.3.1",
148
+ "@vitejs/plugin-react": "^4.3.4",
149
  "fast-glob": "^3.3.2",
150
  "husky": "9.1.7",
151
  "is-ci": "^3.0.1",
152
+ "jsdom": "^26.0.0",
153
  "node-fetch": "^3.3.2",
154
  "pnpm": "^9.14.4",
155
  "prettier": "^3.4.1",
pnpm-lock.yaml CHANGED
The diff for this file is too large to render. See raw diff