difflicious 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,1617 @@
1
+ /**
2
+ * Minimal JavaScript for diff interactions
3
+ * Replaces Alpine.js with lightweight vanilla JS
4
+ */
5
+
6
+ // Debug toggle
7
+ const DEBUG = false;
8
+
9
+ // DOM manipulation utilities
10
+ const $ = (selector) => document.querySelector(selector);
11
+ const $$ = (selector) => document.querySelectorAll(selector);
12
+
13
+ // State management
14
+ const DiffState = {
15
+ expandedFiles: new Set(),
16
+ expandedGroups: new Set(['untracked', 'unstaged', 'staged']),
17
+ repositoryName: null,
18
+ storageKey: 'difflicious-state', // fallback key
19
+ theme: 'light', // current theme
20
+
21
+ async init() {
22
+ await this.initializeRepository();
23
+ this.bindEventListeners();
24
+ this.restoreState();
25
+ this.installSearchHotkeys();
26
+ this.installLiveSearchFilter();
27
+ },
28
+
29
+ async initializeRepository() {
30
+ try {
31
+ const response = await fetch('/api/status');
32
+ const data = await response.json();
33
+ if (data.status === 'ok' && data.repository_name) {
34
+ this.repositoryName = data.repository_name;
35
+ this.storageKey = `difflicious-${this.repositoryName}`;
36
+ if (DEBUG) console.log(`Initialized for repository: ${this.repositoryName}, storage key: ${this.storageKey}`);
37
+ } else {
38
+ if (DEBUG) console.warn('Failed to get repository name, using fallback storage key');
39
+ }
40
+ } catch (error) {
41
+ if (DEBUG) console.warn('Error fetching repository info:', error);
42
+ }
43
+ },
44
+
45
+ bindEventListeners() {
46
+ // Global expand/collapse buttons
47
+ const expandAllBtn = $('#expandAll');
48
+ const collapseAllBtn = $('#collapseAll');
49
+
50
+ if (expandAllBtn) expandAllBtn.addEventListener('click', () => expandAllFiles());
51
+ if (collapseAllBtn) collapseAllBtn.addEventListener('click', () => collapseAllFiles());
52
+
53
+ // Form auto-submit on changes
54
+ $$('input[type="checkbox"], select').forEach(input => {
55
+ input.addEventListener('change', () => {
56
+ input.closest('form')?.submit();
57
+ });
58
+ });
59
+ },
60
+
61
+ restoreState() {
62
+ // First, sync with server-rendered state by checking which files are initially visible
63
+ const serverExpandedFiles = new Set();
64
+ $$('[data-file-content]').forEach(contentElement => {
65
+ const filePath = contentElement.dataset.fileContent;
66
+ const isVisible = contentElement.style.display !== 'none';
67
+ if (isVisible) {
68
+ serverExpandedFiles.add(filePath);
69
+ }
70
+ });
71
+
72
+ // Then try to restore from localStorage if available
73
+ const saved = localStorage.getItem(this.storageKey);
74
+ if (saved) {
75
+ try {
76
+ const state = JSON.parse(saved);
77
+
78
+ // Restore file expansion states
79
+ if (state.expandedFiles) {
80
+ // Merge server state with localStorage state
81
+ // Server state (visible files) takes priority and should always be included
82
+ const savedExpandedFiles = new Set(state.expandedFiles);
83
+ this.expandedFiles = new Set([...serverExpandedFiles, ...savedExpandedFiles]);
84
+
85
+ // Apply the merged state to all files
86
+ $$('[data-file-content]').forEach(contentElement => {
87
+ const filePath = contentElement.dataset.fileContent;
88
+ const fileElement = $(`[data-file="${filePath}"]`);
89
+ const toggleIcon = fileElement?.querySelector('.toggle-icon');
90
+
91
+ if (contentElement && fileElement && toggleIcon) {
92
+ // Prioritize server state (if file is visible, it should be expanded)
93
+ // Otherwise use saved state
94
+ const shouldBeExpanded = serverExpandedFiles.has(filePath) || savedExpandedFiles.has(filePath);
95
+
96
+ // Apply visual state based on merged state
97
+ contentElement.style.display = shouldBeExpanded ? 'block' : 'none';
98
+ toggleIcon.textContent = shouldBeExpanded ? '▼' : '▶';
99
+ toggleIcon.dataset.expanded = shouldBeExpanded ? 'true' : 'false';
100
+
101
+ if (DEBUG) console.log(`Restored ${shouldBeExpanded ? 'expanded' : 'collapsed'} state for file: ${filePath}`);
102
+ }
103
+ });
104
+ } else {
105
+ // No saved files, just use server state
106
+ this.expandedFiles = serverExpandedFiles;
107
+ }
108
+
109
+ // Restore group expansion states
110
+ if (state.expandedGroups) {
111
+ this.expandedGroups = new Set(state.expandedGroups);
112
+ } else {
113
+ // Default expanded groups if no saved state
114
+ this.expandedGroups = new Set(['untracked', 'unstaged', 'staged']);
115
+ }
116
+
117
+ // Apply visual state to all groups based on expandedGroups set
118
+ const allPossibleGroups = ['untracked', 'unstaged', 'staged', 'changes'];
119
+ allPossibleGroups.forEach(groupKey => {
120
+ const contentElement = $(`[data-group-content="${groupKey}"]`);
121
+ const groupElement = $(`[data-group="${groupKey}"]`);
122
+ const toggleIcon = groupElement?.querySelector('.toggle-icon');
123
+
124
+ if (contentElement && toggleIcon) {
125
+ const shouldBeExpanded = this.expandedGroups.has(groupKey);
126
+
127
+ contentElement.style.display = shouldBeExpanded ? 'block' : 'none';
128
+ toggleIcon.textContent = shouldBeExpanded ? '▼' : '▶';
129
+ toggleIcon.dataset.expanded = shouldBeExpanded ? 'true' : 'false';
130
+
131
+ if (DEBUG) console.log(`Restored ${shouldBeExpanded ? 'expanded' : 'collapsed'} state for group: ${groupKey}`);
132
+ }
133
+ });
134
+
135
+ if (DEBUG) console.log(`Restored state for ${this.repositoryName}:`, state);
136
+ } catch (e) {
137
+ if (DEBUG) console.warn('Failed to restore state:', e);
138
+ // Use defaults on error, but preserve server state
139
+ this.expandedFiles = serverExpandedFiles;
140
+ this.expandedGroups = new Set(['untracked', 'unstaged', 'staged']);
141
+ }
142
+ } else {
143
+ // No saved state, use server state and defaults
144
+ this.expandedFiles = serverExpandedFiles;
145
+ this.expandedGroups = new Set(['untracked', 'unstaged', 'staged']);
146
+ }
147
+ },
148
+
149
+ saveState() {
150
+ const state = {
151
+ expandedFiles: Array.from(this.expandedFiles),
152
+ expandedGroups: Array.from(this.expandedGroups),
153
+ repositoryName: this.repositoryName,
154
+ lastUpdated: new Date().toISOString()
155
+ };
156
+ localStorage.setItem(this.storageKey, JSON.stringify(state));
157
+ if (DEBUG) console.log(`Saved state for ${this.repositoryName}:`, state);
158
+ },
159
+ installSearchHotkeys() {
160
+ document.addEventListener('keydown', (e) => {
161
+ const active = document.activeElement;
162
+ const isTyping = active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable);
163
+
164
+ if (e.key === '/' && !isTyping) {
165
+ e.preventDefault();
166
+ const searchInput = document.querySelector('input[name="search"]');
167
+ if (searchInput) {
168
+ searchInput.focus();
169
+ searchInput.select();
170
+ }
171
+ }
172
+
173
+ if (e.key === 'Escape' && active && active.id === 'diff-search-input') {
174
+ e.preventDefault();
175
+ active.value = '';
176
+ applyFilenameFilter('');
177
+ const clearBtn = document.querySelector('#diff-search-clear');
178
+ if (clearBtn) clearBtn.classList.add('hidden');
179
+ active.blur();
180
+ }
181
+
182
+ // Enter no longer cycles results; filtering is live on input
183
+ });
184
+ },
185
+
186
+ installLiveSearchFilter() {
187
+ const searchInput = document.querySelector('#diff-search-input');
188
+ const clearBtn = document.querySelector('#diff-search-clear');
189
+ if (!searchInput) return;
190
+
191
+ const applyFilter = () => {
192
+ const query = (searchInput.value || '').trim();
193
+ applyFilenameFilter(query);
194
+ // Toggle clear button visibility
195
+ if (clearBtn) clearBtn.classList.toggle('hidden', query.length === 0);
196
+ };
197
+ searchInput.addEventListener('input', applyFilter);
198
+
199
+ if (clearBtn) {
200
+ clearBtn.addEventListener('click', () => {
201
+ searchInput.value = '';
202
+ applyFilenameFilter('');
203
+ clearBtn.classList.add('hidden');
204
+ searchInput.focus();
205
+ });
206
+ }
207
+
208
+ // Apply initial filter if there is an existing value
209
+ applyFilter();
210
+ }
211
+ };
212
+
213
+ // File operations
214
+ function toggleFile(filePath) {
215
+ const fileElement = $(`[data-file="${filePath}"]`);
216
+ const contentElement = $(`[data-file-content="${filePath}"]`);
217
+ const toggleIcon = fileElement?.querySelector('.toggle-icon');
218
+
219
+ if (!fileElement || !contentElement || !toggleIcon) return;
220
+
221
+ const isExpanded = DiffState.expandedFiles.has(filePath);
222
+
223
+ if (isExpanded) {
224
+ // Collapse
225
+ contentElement.style.display = 'none';
226
+ toggleIcon.textContent = '▶';
227
+ toggleIcon.dataset.expanded = 'false';
228
+ DiffState.expandedFiles.delete(filePath);
229
+ } else {
230
+ // Expand
231
+ contentElement.style.display = 'block';
232
+ toggleIcon.textContent = '▼';
233
+ toggleIcon.dataset.expanded = 'true';
234
+ DiffState.expandedFiles.add(filePath);
235
+ }
236
+
237
+ DiffState.saveState();
238
+ }
239
+
240
+ function toggleGroup(groupKey) {
241
+ const groupElement = $(`[data-group="${groupKey}"]`);
242
+ const contentElement = $(`[data-group-content="${groupKey}"]`);
243
+ const toggleIcon = groupElement?.querySelector('.toggle-icon');
244
+
245
+ if (!groupElement || !contentElement || !toggleIcon) return;
246
+
247
+ const isExpanded = DiffState.expandedGroups.has(groupKey);
248
+
249
+ if (isExpanded) {
250
+ // Collapse
251
+ contentElement.style.display = 'none';
252
+ toggleIcon.textContent = '▶';
253
+ toggleIcon.dataset.expanded = 'false';
254
+ DiffState.expandedGroups.delete(groupKey);
255
+ } else {
256
+ // Expand
257
+ contentElement.style.display = 'block';
258
+ toggleIcon.textContent = '▼';
259
+ toggleIcon.dataset.expanded = 'true';
260
+ DiffState.expandedGroups.add(groupKey);
261
+ }
262
+
263
+ DiffState.saveState();
264
+ }
265
+
266
+ function expandAllFiles() {
267
+ // Batch DOM operations to avoid layout thrashing
268
+ const elementsToUpdate = [];
269
+ const filesToAdd = [];
270
+
271
+ // Collect all elements that need updates first (minimize DOM queries)
272
+ $$('[data-file]').forEach(fileElement => {
273
+ const filePath = fileElement.dataset.file;
274
+ if (filePath) {
275
+ const contentElement = $(`[data-file-content="${filePath}"]`);
276
+ const isVisuallyExpanded = contentElement && contentElement.style.display !== 'none';
277
+
278
+ if (!isVisuallyExpanded && contentElement) {
279
+ const toggleIcon = fileElement.querySelector('.toggle-icon');
280
+ elementsToUpdate.push({
281
+ contentElement,
282
+ toggleIcon,
283
+ filePath
284
+ });
285
+ filesToAdd.push(filePath);
286
+ }
287
+ }
288
+ });
289
+
290
+ // Batch DOM updates to minimize browser reflows
291
+ if (elementsToUpdate.length > 0) {
292
+ // Use requestAnimationFrame for smoother performance
293
+ requestAnimationFrame(() => {
294
+ elementsToUpdate.forEach(({ contentElement, toggleIcon }) => {
295
+ contentElement.style.display = 'block';
296
+ if (toggleIcon) {
297
+ toggleIcon.textContent = '▼';
298
+ toggleIcon.dataset.expanded = 'true';
299
+ }
300
+ });
301
+ });
302
+
303
+ // Update internal state in batch
304
+ filesToAdd.forEach(filePath => DiffState.expandedFiles.add(filePath));
305
+
306
+ // Save state once after all changes
307
+ DiffState.saveState();
308
+ }
309
+ }
310
+
311
+ function collapseAllFiles() {
312
+ // Batch DOM operations to avoid layout thrashing
313
+ const elementsToUpdate = [];
314
+ const filesToRemove = [];
315
+
316
+ // Collect all elements that need updates first (minimize DOM queries)
317
+ $$('[data-file]').forEach(fileElement => {
318
+ const filePath = fileElement.dataset.file;
319
+ if (filePath) {
320
+ const contentElement = $(`[data-file-content="${filePath}"]`);
321
+ const isVisuallyExpanded = contentElement && contentElement.style.display !== 'none';
322
+
323
+ if (isVisuallyExpanded && contentElement) {
324
+ const toggleIcon = fileElement.querySelector('.toggle-icon');
325
+ elementsToUpdate.push({
326
+ contentElement,
327
+ toggleIcon,
328
+ filePath
329
+ });
330
+ filesToRemove.push(filePath);
331
+ }
332
+ }
333
+ });
334
+
335
+ // Batch DOM updates to minimize browser reflows
336
+ if (elementsToUpdate.length > 0) {
337
+ // Use requestAnimationFrame for smoother performance
338
+ requestAnimationFrame(() => {
339
+ elementsToUpdate.forEach(({ contentElement, toggleIcon }) => {
340
+ contentElement.style.display = 'none';
341
+ if (toggleIcon) {
342
+ toggleIcon.textContent = '▶';
343
+ toggleIcon.dataset.expanded = 'false';
344
+ }
345
+ });
346
+ });
347
+
348
+ // Update internal state in batch
349
+ filesToRemove.forEach(filePath => DiffState.expandedFiles.delete(filePath));
350
+
351
+ // Save state once after all changes
352
+ DiffState.saveState();
353
+ }
354
+ }
355
+
356
+ // Navigation
357
+ function navigateToPreviousFile(currentFilePath) {
358
+ const allFiles = Array.from($$('[data-file]'));
359
+ const currentIndex = allFiles.findIndex(el => el.dataset.file === currentFilePath);
360
+
361
+ if (currentIndex > 0) {
362
+ const prevFile = allFiles[currentIndex - 1];
363
+ prevFile.scrollIntoView({ behavior: 'smooth', block: 'start' });
364
+ }
365
+ }
366
+
367
+ function navigateToNextFile(currentFilePath) {
368
+ const allFiles = Array.from($$('[data-file]'));
369
+ const currentIndex = allFiles.findIndex(el => el.dataset.file === currentFilePath);
370
+
371
+ if (currentIndex >= 0 && currentIndex < allFiles.length - 1) {
372
+ const nextFile = allFiles[currentIndex + 1];
373
+ nextFile.scrollIntoView({ behavior: 'smooth', block: 'start' });
374
+ }
375
+ }
376
+
377
+ // Context expansion
378
+ async function expandContext(button, filePath, hunkIndex, direction, contextLines = 10, format = 'pygments') {
379
+ if (DEBUG) console.log(`🔥 expandContext called! File: ${filePath}, Direction: ${direction}, Range: ${button.dataset.targetStart}-${button.dataset.targetEnd}`);
380
+
381
+ const originalText = button.textContent;
382
+ const timestamp = Date.now();
383
+ const expansionId = `expand-${filePath.replace(/[^a-zA-Z0-9]/g, '_')}-${hunkIndex}-${direction}-${timestamp}`;
384
+
385
+ // Get button target range data
386
+ const targetStart = parseInt(button.dataset.targetStart);
387
+ const targetEnd = parseInt(button.dataset.targetEnd);
388
+
389
+ // Show loading state
390
+ button.textContent = '...';
391
+ button.disabled = true;
392
+
393
+ try {
394
+ const params = new URLSearchParams({
395
+ file_path: filePath,
396
+ hunk_index: hunkIndex,
397
+ direction,
398
+ context_lines: contextLines,
399
+ format,
400
+ target_start: targetStart,
401
+ target_end: targetEnd
402
+ });
403
+
404
+ const response = await fetch(`/api/expand-context?${params}`);
405
+ const result = await response.json();
406
+
407
+ if (result.status === 'ok') {
408
+ if (DEBUG) console.log(`Context expansion successful for ${filePath}, format: ${result.format}`);
409
+
410
+ // Handle format-specific processing
411
+ let expandedHtml;
412
+ if (format === 'pygments' && result.format === 'pygments') {
413
+ if (DEBUG) console.log(`Injecting Pygments CSS and creating HTML for ${result.lines.length} lines`);
414
+ injectPygmentsCss(result.css_styles);
415
+ expandedHtml = createExpandedContextHtml(result, expansionId, button, direction);
416
+ } else {
417
+ if (DEBUG) console.log(`Using plain format for ${result.lines.length} lines`);
418
+ expandedHtml = createPlainContextHtml(result, expansionId, button, direction);
419
+ }
420
+
421
+ // Insert the expanded context
422
+ insertExpandedContext(button, filePath, hunkIndex, direction, expandedHtml);
423
+
424
+ // Update data attributes after expansion to maintain consistency for future expansions
425
+ updateHunkLinesDataAttributes(button, direction, result.lines.length);
426
+
427
+ // Common post-processing logic
428
+ handlePostExpansionLogic(button, result, contextLines, targetStart, targetEnd, direction, originalText);
429
+
430
+ if (DEBUG) console.log(`Successfully inserted expanded context with ID: ${expansionId}`);
431
+ } else {
432
+ if (DEBUG) console.error('Context expansion failed:', result.message);
433
+ button.textContent = originalText;
434
+ }
435
+ } catch (error) {
436
+ if (DEBUG) console.error('Context expansion error:', error);
437
+ button.textContent = originalText;
438
+ } finally {
439
+ button.disabled = false;
440
+ }
441
+ }
442
+
443
+ // Helper function to handle all post-expansion logic
444
+ function handlePostExpansionLogic(button, result, contextLines, targetStart, targetEnd, direction, originalText) {
445
+ const linesReceived = result.lines.length;
446
+ let shouldHideButton = linesReceived < contextLines;
447
+
448
+ // Check if expansion would make this hunk adjacent to adjacent hunks
449
+ shouldHideButton = checkHunkAdjacency(button, direction, targetStart, targetEnd) || shouldHideButton;
450
+
451
+ if (shouldHideButton) {
452
+ handleButtonHiding(button, direction, targetStart, targetEnd);
453
+ } else {
454
+ updateButtonForNextExpansion(button, direction, targetStart, targetEnd, contextLines, originalText);
455
+ }
456
+
457
+ // Update hunk range data to include the expanded lines
458
+ updateHunkRangeAfterExpansion(button, targetStart, targetEnd);
459
+
460
+ // Check for potential hunk merging
461
+ checkAndMergeHunks(button);
462
+ }
463
+
464
+ // Helper function to check if expansion makes hunks adjacent
465
+ function checkHunkAdjacency(button, direction, targetStart, targetEnd) {
466
+ if (direction === 'after') {
467
+ const context = hunkContext(button);
468
+ if (!context?.fileElement) return false;
469
+
470
+ const { currentHunk, nextHunk } = context;
471
+ if (nextHunk) {
472
+ const nextHunkStart = parseInt(nextHunk.dataset.lineStart);
473
+ if (targetEnd === nextHunkStart - 1) {
474
+ // Hide both before buttons in the next hunk (left and right sides)
475
+ const nextHunkBeforeBtns = nextHunk.querySelectorAll('.expansion-btn[data-direction="before"]');
476
+ nextHunkBeforeBtns.forEach(btn => {
477
+ btn.style.display = 'none';
478
+ });
479
+ if (nextHunkBeforeBtns.length > 0) {
480
+ hideExpansionBarIfAllButtonsHidden(nextHunkBeforeBtns[0]);
481
+ }
482
+
483
+ // Also hide all remaining after buttons in the current hunk since no more expansion is possible
484
+ const currentHunkAfterBtns = currentHunk.querySelectorAll('.expansion-btn[data-direction="after"]');
485
+ currentHunkAfterBtns.forEach(btn => {
486
+ btn.style.display = 'none';
487
+ });
488
+ if (currentHunkAfterBtns.length > 0) {
489
+ hideExpansionBarIfAllButtonsHidden(currentHunkAfterBtns[0]);
490
+ }
491
+
492
+ return true;
493
+ }
494
+ }
495
+ } else if (direction === 'before') {
496
+ const context = hunkContext(button);
497
+ if (!context?.fileElement) return false;
498
+
499
+ const { currentHunk, prevHunk } = context;
500
+ if (prevHunk) {
501
+ const prevHunkEnd = parseInt(prevHunk.dataset.lineEnd);
502
+ if (targetStart <= prevHunkEnd + 1) {
503
+ // Hide both after buttons in the previous hunk (left and right sides)
504
+ const prevHunkAfterBtns = prevHunk.querySelectorAll('.expansion-btn[data-direction="after"]');
505
+ prevHunkAfterBtns.forEach(btn => {
506
+ btn.style.display = 'none';
507
+ });
508
+ if (prevHunkAfterBtns.length > 0) {
509
+ hideExpansionBarIfAllButtonsHidden(prevHunkAfterBtns[0]);
510
+ }
511
+
512
+ // Also hide all remaining before buttons in the current hunk since no more expansion is possible
513
+ const currentHunkBeforeBtns = currentHunk.querySelectorAll('.expansion-btn[data-direction="before"]');
514
+ currentHunkBeforeBtns.forEach(btn => {
515
+ btn.style.display = 'none';
516
+ });
517
+ if (currentHunkBeforeBtns.length > 0) {
518
+ hideExpansionBarIfAllButtonsHidden(currentHunkBeforeBtns[0]);
519
+ }
520
+
521
+ return true;
522
+ }
523
+ }
524
+ }
525
+ return false;
526
+ }
527
+
528
+ // Helper function to handle button hiding logic
529
+ function handleButtonHiding(button, direction, targetStart, targetEnd) {
530
+ // End of file reached or touching next hunk - hide both buttons (left and right sides)
531
+ const context = hunkContext(button);
532
+ if (!context) return;
533
+
534
+ const { currentHunk } = context;
535
+ const sameSideButtons = currentHunk.querySelectorAll(`.expansion-btn[data-direction="${direction}"][data-target-start="${targetStart}"][data-target-end="${targetEnd}"]`);
536
+ sameSideButtons.forEach(btn => {
537
+ btn.style.display = 'none';
538
+ });
539
+ if (DEBUG) console.log(`Hiding button. Target range: ${targetStart}-${targetEnd}`);
540
+
541
+ // If this was an up button that reached line 1, hide all buttons (file fully expanded)
542
+ if (direction === 'before' && targetStart === 1) {
543
+ if (DEBUG) console.log('Up button reached line 1 - hiding all expansion buttons');
544
+ hideAllExpansionButtonsInHunk(button);
545
+ }
546
+
547
+ // Check if all expansion buttons in this hunk are now hidden
548
+ hideExpansionBarIfAllButtonsHidden(button);
549
+ }
550
+
551
+ // Helper function to update button for next expansion
552
+ function updateButtonForNextExpansion(button, direction, targetStart, targetEnd, contextLines, originalText) {
553
+ // More lines available - update button for next expansion
554
+ button.textContent = originalText;
555
+ button.title = `Expand ${contextLines} more lines ${direction}`;
556
+
557
+ // Update button's target range for next click
558
+ if (direction === 'before') {
559
+ const newTargetEnd = targetStart - 1;
560
+ const newTargetStart = Math.max(1, newTargetEnd - contextLines + 1);
561
+
562
+ // Check if we've reached the beginning of the file
563
+ if (targetStart === 1) {
564
+ // Beginning of file reached - hide both buttons (left and right sides)
565
+ const context = hunkContext(button);
566
+ if (!context) return;
567
+
568
+ const { currentHunk } = context;
569
+ const sameSideButtons = currentHunk.querySelectorAll(`.expansion-btn[data-direction="${direction}"][data-target-start="${targetStart}"][data-target-end="${targetEnd}"]`);
570
+ sameSideButtons.forEach(btn => {
571
+ btn.style.display = 'none';
572
+ });
573
+ if (DEBUG) console.log(`Beginning of file reached. Current targetStart was ${targetStart}. Hiding up buttons.`);
574
+ hideExpansionBarIfAllButtonsHidden(button);
575
+ } else {
576
+ // Check for overlap with previous hunk before setting new range
577
+ const context = hunkContext(button);
578
+ if (!context) return;
579
+
580
+ const { currentHunk, fileElement, prevHunk } = context;
581
+ let adjustedTargetStart = newTargetStart;
582
+
583
+ if (fileElement && prevHunk) {
584
+ const prevHunkEnd = parseInt(prevHunk.dataset.lineEnd);
585
+
586
+ // Ensure we don't expand into the previous hunk's visible range
587
+ adjustedTargetStart = Math.max(adjustedTargetStart, prevHunkEnd + 1);
588
+
589
+ // If adjustment makes the range invalid (start > end), hide both buttons (left and right sides)
590
+ if (adjustedTargetStart > newTargetEnd) {
591
+ const sameSideButtons = currentHunk.querySelectorAll(`.expansion-btn[data-direction="${direction}"][data-target-start="${targetStart}"][data-target-end="${targetEnd}"]`);
592
+ sameSideButtons.forEach(btn => {
593
+ btn.style.display = 'none';
594
+ });
595
+ if (DEBUG) console.log('No room for expansion between hunks. Hiding up buttons.');
596
+ hideExpansionBarIfAllButtonsHidden(button);
597
+ return;
598
+ }
599
+ }
600
+
601
+ button.dataset.targetStart = adjustedTargetStart;
602
+ button.dataset.targetEnd = newTargetEnd;
603
+ }
604
+ } else {
605
+ const newTargetStart = targetEnd + 1;
606
+ const newTargetEnd = newTargetStart + contextLines - 1;
607
+
608
+ // Check for overlap with next hunk before setting new range
609
+ const context = hunkContext(button);
610
+ if (!context) return;
611
+
612
+ const { currentHunk, fileElement, nextHunk } = context;
613
+ let adjustedTargetEnd = newTargetEnd;
614
+
615
+ if (fileElement && nextHunk) {
616
+ const nextHunkStart = parseInt(nextHunk.dataset.lineStart);
617
+
618
+ // Ensure we don't expand into the next hunk's visible range
619
+ adjustedTargetEnd = Math.min(adjustedTargetEnd, nextHunkStart - 1);
620
+
621
+ // If adjustment makes the range invalid (start > end), hide both buttons (left and right sides)
622
+ if (newTargetStart > adjustedTargetEnd) {
623
+ const sameSideButtons = currentHunk.querySelectorAll(`.expansion-btn[data-direction="${direction}"][data-target-start="${targetStart}"][data-target-end="${targetEnd}"]`);
624
+ sameSideButtons.forEach(btn => {
625
+ btn.style.display = 'none';
626
+ });
627
+ if (DEBUG) console.log('No room for expansion between hunks. Hiding down buttons.');
628
+ hideExpansionBarIfAllButtonsHidden(button);
629
+ return;
630
+ }
631
+ }
632
+
633
+ button.dataset.targetStart = newTargetStart;
634
+ button.dataset.targetEnd = adjustedTargetEnd;
635
+ }
636
+ }
637
+
638
+ // Helper function to get hunk context from a button
639
+ function hunkContext(button) {
640
+ const currentHunk = button.closest('.hunk');
641
+ if (!currentHunk) {
642
+ return null;
643
+ }
644
+
645
+ const fileElement = currentHunk.closest('[data-file]');
646
+ if (!fileElement) {
647
+ return { currentHunk, fileElement: null, allHunks: [], currentIndex: -1, prevHunk: null, nextHunk: null };
648
+ }
649
+
650
+ const allHunks = Array.from(fileElement.querySelectorAll('.hunk'));
651
+ const currentIndex = allHunks.indexOf(currentHunk);
652
+
653
+ return {
654
+ currentHunk,
655
+ fileElement,
656
+ allHunks,
657
+ currentIndex,
658
+ prevHunk: currentIndex > 0 ? allHunks[currentIndex - 1] : null,
659
+ nextHunk: currentIndex < allHunks.length - 1 ? allHunks[currentIndex + 1] : null
660
+ };
661
+ }
662
+
663
+ // Helper functions for context expansion
664
+
665
+ function escapeHtml(text) {
666
+ // Basic HTML escaping for security
667
+ const div = document.createElement('div');
668
+ div.textContent = text;
669
+ return div.innerHTML;
670
+ }
671
+
672
+ function injectPygmentsCss(cssStyles) {
673
+ // Inject Pygments CSS styles into the document head if not already present
674
+ if (!cssStyles) return;
675
+
676
+ // Check if Pygments styles are already injected
677
+ if ($('#pygments-styles')) return;
678
+
679
+ const styleElement = document.createElement('style');
680
+ styleElement.id = 'pygments-styles';
681
+ styleElement.textContent = cssStyles;
682
+ document.head.appendChild(styleElement);
683
+ }
684
+
685
+ function createExpandedContextHtml(result, expansionId, triggerButton, direction) {
686
+ // Create HTML for Pygments-formatted expanded context lines
687
+ const lines = result.lines || [];
688
+
689
+ // Use the original working logic from before the broken refactoring
690
+ const context = hunkContext(triggerButton);
691
+ const hunkLinesDiv = context?.currentHunk?.querySelector('.hunk-lines');
692
+
693
+ let startLineNumLeft, startLineNumRight;
694
+
695
+ if (hunkLinesDiv) {
696
+ const curRightStart = parseInt(hunkLinesDiv.dataset.rightStartLine);
697
+ const curRightEnd = parseInt(hunkLinesDiv.dataset.rightEndLine);
698
+ const curLeftStart = parseInt(hunkLinesDiv.dataset.leftStartLine || '0');
699
+ const curLeftEnd = parseInt(hunkLinesDiv.dataset.leftEndLine || '0');
700
+
701
+ if (direction === 'after') {
702
+ // Right side: continue from right end
703
+ startLineNumRight = curRightEnd + 1;
704
+
705
+ // Left side: use the ORIGINAL working logic that was removed
706
+ const leftBase = (curLeftEnd || (curLeftStart + (curRightEnd - curRightStart)));
707
+ startLineNumLeft = (leftBase || 0) + 1;
708
+
709
+ if (DEBUG) console.log(`After expansion (original logic): curLeftStart=${curLeftStart}, curLeftEnd=${curLeftEnd}, curRightStart=${curRightStart}, curRightEnd=${curRightEnd}, leftBase=${leftBase}, calculated: left=${startLineNumLeft}, right=${startLineNumRight}`);
710
+ } else if (direction === 'before') {
711
+ // Right side: expand backwards
712
+ startLineNumRight = curRightStart - lines.length;
713
+
714
+ // Left side: use the ORIGINAL working logic that was removed
715
+ const leftEndBefore = (curLeftStart || 1) - 1;
716
+ startLineNumLeft = Math.max(1, leftEndBefore - (lines.length - 1));
717
+
718
+ if (DEBUG) console.log(`Before expansion (original logic): curLeftStart=${curLeftStart}, leftEndBefore=${leftEndBefore}, lines=${lines.length}, calculated: left=${startLineNumLeft}, right=${startLineNumRight}`);
719
+ }
720
+ } else {
721
+ // Fallback: should not happen with proper data attributes
722
+ if (DEBUG) console.warn('Missing hunk-lines data attributes, using fallback line numbering');
723
+ startLineNumRight = 1;
724
+ startLineNumLeft = 1;
725
+ }
726
+
727
+ let html = `<div id="${expansionId}" class="expanded-context bg-neutral-25">`;
728
+
729
+ lines.forEach((lineData, index) => {
730
+ const lineNumRight = startLineNumRight + index;
731
+ const lineNumLeft = startLineNumLeft + index;
732
+ const content = lineData.highlighted_content || lineData.content || '';
733
+
734
+ html += `
735
+ <div class="diff-line grid grid-cols-2 hover:bg-neutral-25 line-context">
736
+ <!-- Left Side (Before) -->
737
+ <div class="line-left border-r border-neutral-200">
738
+ <div class="flex">
739
+ <div class="line-num w-12 px-2 py-1 text-neutral-400 text-right border-r border-neutral-200 select-none">
740
+ <span>${lineNumLeft}</span>
741
+ </div>
742
+ <div class="line-content flex-1 px-2 py-1 overflow-x-auto min-w-0">
743
+ <span class="text-neutral-400">&nbsp;</span>
744
+ <span class="highlight break-words">${content}</span>
745
+ ${lineData.missing_newline ? '<span class="no-newline-indicator text-danger-text-500">↩</span>' : ''}
746
+ </div>
747
+ </div>
748
+ </div>
749
+ <!-- Right Side (After) -->
750
+ <div class="line-right">
751
+ <div class="flex">
752
+ <div class="line-num w-12 px-2 py-1 text-neutral-400 text-right border-r border-neutral-200 select-none">
753
+ <span>${lineNumRight}</span>
754
+ </div>
755
+ <div class="line-content flex-1 px-2 py-1 overflow-x-auto min-w-0">
756
+ <span class="text-neutral-400">&nbsp;</span>
757
+ <span class="highlight break-words">${content}</span>
758
+ ${lineData.missing_newline ? '<span class="no-newline-indicator text-danger-text-500">↩</span>' : ''}
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </div>`;
763
+ });
764
+
765
+ html += '</div>';
766
+ return html;
767
+ }
768
+
769
+ function createPlainContextHtml(result, expansionId, triggerButton, direction) {
770
+ // Create HTML for plain text expanded context lines
771
+ const lines = result.lines || [];
772
+
773
+ // Get line numbers from the hunk-lines data attributes
774
+ const context = hunkContext(triggerButton);
775
+ const hunkLinesDiv = context?.currentHunk?.querySelector('.hunk-lines');
776
+
777
+ let startLineNumLeft, startLineNumRight;
778
+
779
+ if (hunkLinesDiv && direction === 'before') {
780
+ // For 'before' expansion, calculate backwards from the current hunk start
781
+ const currentLeftStart = parseInt(hunkLinesDiv.dataset.leftStartLine);
782
+ const currentRightStart = parseInt(hunkLinesDiv.dataset.rightStartLine);
783
+
784
+ if (isNaN(currentLeftStart) || isNaN(currentRightStart)) {
785
+ if (DEBUG) console.warn('Invalid hunk data attributes for before expansion, using fallback');
786
+ startLineNumRight = result.right_start_line || result.start_line || 1;
787
+ startLineNumLeft = result.left_start_line || startLineNumRight;
788
+ } else {
789
+ // Both sides expand backwards by the same number of lines
790
+ startLineNumLeft = currentLeftStart - lines.length;
791
+ startLineNumRight = currentRightStart - lines.length;
792
+ if (DEBUG) console.log(`Before expansion: leftStart=${currentLeftStart}, rightStart=${currentRightStart}, expansion lines=${lines.length}, calculated start: left=${startLineNumLeft}, right=${startLineNumRight}`);
793
+ }
794
+ } else if (hunkLinesDiv && direction === 'after') {
795
+ // For 'after' expansion, we need to check if left/right sides have diverged
796
+ const currentLeftEnd = parseInt(hunkLinesDiv.dataset.leftEndLine);
797
+ const currentRightEnd = parseInt(hunkLinesDiv.dataset.rightEndLine);
798
+
799
+ if (isNaN(currentLeftEnd) || isNaN(currentRightEnd)) {
800
+ if (DEBUG) console.warn('Invalid hunk data attributes for after expansion, using fallback');
801
+ startLineNumRight = result.right_start_line || result.start_line || 1;
802
+ startLineNumLeft = result.left_start_line || startLineNumRight;
803
+ } else {
804
+ // Right side always continues from the right end
805
+ startLineNumRight = currentRightEnd + 1;
806
+
807
+ // Left side logic: if left and right are tracking together, continue together
808
+ // Otherwise, left side continues from its own end
809
+ if (currentLeftEnd === 0) {
810
+ // Left side hasn't been set or is at 0, derive from right side
811
+ startLineNumLeft = startLineNumRight;
812
+ } else {
813
+ // Left side has its own tracking, continue from its end
814
+ startLineNumLeft = currentLeftEnd + 1;
815
+ }
816
+
817
+ if (DEBUG) console.log(`After expansion: leftEnd=${currentLeftEnd}, rightEnd=${currentRightEnd}, calculated start: left=${startLineNumLeft}, right=${startLineNumRight}`);
818
+ }
819
+ } else {
820
+ // Fallback: should not happen with proper data attributes
821
+ if (DEBUG) console.warn('Missing hunk-lines div or invalid direction, using fallback line numbering');
822
+ startLineNumRight = result.right_start_line || result.start_line || 1;
823
+ startLineNumLeft = result.left_start_line || startLineNumRight;
824
+ }
825
+
826
+ let html = `<div id="${expansionId}" class="expanded-context bg-neutral-25">`;
827
+
828
+ lines.forEach((line, index) => {
829
+ const lineNumRight = startLineNumRight + index;
830
+ const lineNumLeft = startLineNumLeft + index;
831
+ const content = escapeHtml(line || '');
832
+
833
+ html += `
834
+ <div class="diff-line grid grid-cols-2 hover:bg-neutral-25 line-context">
835
+ <!-- Left Side (Before) -->
836
+ <div class="line-left border-r border-neutral-200">
837
+ <div class="flex">
838
+ <div class="line-num w-12 px-2 py-1 text-neutral-400 text-right border-r border-neutral-200 select-none">
839
+ <span>${lineNumLeft}</span>
840
+ </div>
841
+ <div class="line-content flex-1 px-2 py-1 overflow-x-auto min-w-0">
842
+ <span class="text-neutral-400">&nbsp;</span>
843
+ <span class="break-words">${content}</span>
844
+ ${line.missing_newline ? '<span class="no-newline-indicator text-danger-text-500">↩</span>' : ''}
845
+ </div>
846
+ </div>
847
+ </div>
848
+ <!-- Right Side (After) -->
849
+ <div class="line-right">
850
+ <div class="flex">
851
+ <div class="line-num w-12 px-2 py-1 text-neutral-400 text-right border-r border-neutral-200 select-none">
852
+ <span>${lineNumRight}</span>
853
+ </div>
854
+ <div class="line-content flex-1 px-2 py-1 overflow-x-auto min-w-0">
855
+ <span class="text-neutral-400">&nbsp;</span>
856
+ <span class="break-words">${content}</span>
857
+ ${line.missing_newline ? '<span class="no-newline-indicator text-danger-text-500">↩</span>' : ''}
858
+ </div>
859
+ </div>
860
+ </div>
861
+ </div>`;
862
+ });
863
+
864
+ html += '</div>';
865
+ return html;
866
+ }
867
+
868
+ function insertExpandedContext(button, filePath, hunkIndex, direction, expandedHtml) {
869
+ // Insert expanded context HTML into the appropriate location in the DOM
870
+ if (DEBUG) console.log(`Inserting expanded context for ${filePath}, direction: ${direction}`);
871
+
872
+ // Find the specific hunk using the button's parent elements
873
+ const context = hunkContext(button);
874
+ if (!context) {
875
+ if (DEBUG) console.error('Could not find hunk element for insertion');
876
+ return;
877
+ }
878
+
879
+ // Find the hunk lines container within this specific hunk
880
+ const { currentHunk } = context;
881
+ const hunkLinesElement = currentHunk.querySelector('.hunk-lines');
882
+ if (!hunkLinesElement) {
883
+ if (DEBUG) console.error('Could not find hunk-lines element for insertion');
884
+ return;
885
+ }
886
+
887
+ if (DEBUG) console.log('Found hunk-lines element, creating expanded content...');
888
+
889
+ // Create a temporary container to parse the HTML
890
+ const tempDiv = document.createElement('div');
891
+ tempDiv.innerHTML = expandedHtml;
892
+ const expandedElement = tempDiv.firstElementChild;
893
+
894
+ if (!expandedElement) {
895
+ if (DEBUG) console.error('Failed to create expanded element from HTML');
896
+ return;
897
+ }
898
+
899
+ if (direction === 'before') {
900
+ // Insert at the beginning of hunk-lines
901
+ if (DEBUG) console.log('Inserting expanded content before existing lines');
902
+ hunkLinesElement.insertBefore(expandedElement, hunkLinesElement.firstChild);
903
+ } else {
904
+ // Insert at the end of hunk-lines
905
+ if (DEBUG) console.log('Inserting expanded content after existing lines');
906
+ hunkLinesElement.appendChild(expandedElement);
907
+ }
908
+
909
+ if (DEBUG) console.log('Successfully inserted expanded content into DOM');
910
+ }
911
+
912
+ // Range conflict detection and state management functions
913
+
914
+ function updateHunkLinesDataAttributes(button, direction, linesAdded) {
915
+ // Find the hunk-lines div and update its data attributes
916
+ const context = hunkContext(button);
917
+ const hunkLinesDiv = context?.currentHunk?.querySelector('.hunk-lines');
918
+
919
+ if (!hunkLinesDiv) return;
920
+
921
+ const currentLeftStart = parseInt(hunkLinesDiv.dataset.leftStartLine);
922
+ const currentLeftEnd = parseInt(hunkLinesDiv.dataset.leftEndLine);
923
+ const currentRightStart = parseInt(hunkLinesDiv.dataset.rightStartLine);
924
+ const currentRightEnd = parseInt(hunkLinesDiv.dataset.rightEndLine);
925
+
926
+ if (DEBUG) console.log(`Updating hunk data attributes: direction=${direction}, linesAdded=${linesAdded}, current: leftStart=${currentLeftStart}, leftEnd=${currentLeftEnd}, rightStart=${currentRightStart}, rightEnd=${currentRightEnd}`);
927
+
928
+ if (direction === 'before') {
929
+ // Update start lines by moving them backwards
930
+ hunkLinesDiv.dataset.leftStartLine = (currentLeftStart - linesAdded).toString();
931
+ hunkLinesDiv.dataset.rightStartLine = (currentRightStart - linesAdded).toString();
932
+ } else if (direction === 'after') {
933
+ // Update end lines by moving them forwards
934
+ hunkLinesDiv.dataset.leftEndLine = (currentLeftEnd + linesAdded).toString();
935
+ hunkLinesDiv.dataset.rightEndLine = (currentRightEnd + linesAdded).toString();
936
+ }
937
+
938
+ if (DEBUG) console.log(`Updated hunk data attributes: leftStart=${hunkLinesDiv.dataset.leftStartLine}, leftEnd=${hunkLinesDiv.dataset.leftEndLine}, rightStart=${hunkLinesDiv.dataset.rightStartLine}, rightEnd=${hunkLinesDiv.dataset.rightEndLine}`);
939
+ }
940
+
941
+ function updateHunkRangeAfterExpansion(button, targetStart, targetEnd) {
942
+ // Get the hunk element containing this button
943
+ const context = hunkContext(button);
944
+ if (!context) return;
945
+
946
+ // Get current range
947
+ const { currentHunk } = context;
948
+ const currentStart = parseInt(currentHunk.dataset.lineStart);
949
+ const currentEnd = parseInt(currentHunk.dataset.lineEnd);
950
+ const currentLeftStart = parseInt(currentHunk.dataset.leftLineStart || '0');
951
+ const currentLeftEnd = parseInt(currentHunk.dataset.leftLineEnd || '0');
952
+
953
+ // Calculate new expanded right-side range
954
+ const newStart = Math.min(currentStart, targetStart);
955
+ const newEnd = Math.max(currentEnd, targetEnd);
956
+ const insertedLength = Math.max(0, targetEnd - targetStart + 1);
957
+
958
+ // Update hunk data attributes
959
+ // Update right side range
960
+ currentHunk.dataset.lineStart = newStart;
961
+ currentHunk.dataset.lineEnd = newEnd;
962
+
963
+ // Update left side range using only the newly inserted span
964
+ const dir = directionFromButton(button);
965
+ if (dir === 'after') {
966
+ const prevLeftEnd = isNaN(currentLeftEnd) || currentLeftEnd === 0
967
+ ? (isNaN(currentLeftStart) || currentLeftStart === 0 ? 0 : (currentLeftStart + (currentEnd - currentStart)))
968
+ : currentLeftEnd;
969
+ const leftStart = prevLeftEnd + 1;
970
+ const leftEnd = leftStart + Math.max(0, insertedLength - 1);
971
+ currentHunk.dataset.leftLineStart = leftStart.toString();
972
+ currentHunk.dataset.leftLineEnd = leftEnd.toString();
973
+ } else if (dir === 'before') {
974
+ const prevLeftStart = isNaN(currentLeftStart) || currentLeftStart === 0 ? 1 : currentLeftStart;
975
+ const leftEnd = prevLeftStart - 1;
976
+ const leftStart = Math.max(1, leftEnd - Math.max(0, insertedLength - 1));
977
+ currentHunk.dataset.leftLineStart = leftStart.toString();
978
+ currentHunk.dataset.leftLineEnd = leftEnd.toString();
979
+ }
980
+
981
+ if (DEBUG) console.log(`Updated hunk range from ${currentStart}-${currentEnd} to ${newStart}-${newEnd}`);
982
+ }
983
+
984
+ // Determine expansion direction from button dataset
985
+ function directionFromButton(button) {
986
+ const d = (button?.dataset?.direction || '').toLowerCase();
987
+ return d === 'after' ? 'after' : 'before';
988
+ }
989
+
990
+ function hideAllExpansionButtonsInHunk(triggerButton) {
991
+ // Get the hunk element containing the trigger button
992
+ const hunkElement = triggerButton.closest('.hunk');
993
+ if (!hunkElement) return;
994
+
995
+ // Find the expansion bar within this hunk
996
+ const expansionBar = hunkElement.querySelector('.hunk-expansion');
997
+ if (!expansionBar) return;
998
+
999
+ // Hide all expansion buttons in this hunk
1000
+ const expansionButtons = expansionBar.querySelectorAll('.expansion-btn');
1001
+ expansionButtons.forEach(btn => {
1002
+ btn.style.display = 'none';
1003
+ if (DEBUG) console.log(`Hiding ${btn.dataset.direction} button`);
1004
+ });
1005
+ }
1006
+
1007
+ function hideExpansionBarIfAllButtonsHidden(triggerButton) {
1008
+ // Find the specific expansion bar that contains the trigger button
1009
+ const expansionBar = triggerButton.closest('.hunk-expansion');
1010
+ if (!expansionBar) return;
1011
+
1012
+ // Check if all expansion buttons in this specific expansion bar are hidden
1013
+ const expansionButtons = expansionBar.querySelectorAll('.expansion-btn');
1014
+ const buttonStates = Array.from(expansionButtons).map(btn => ({
1015
+ direction: btn.dataset.direction,
1016
+ display: btn.style.display,
1017
+ hidden: btn.style.display === 'none'
1018
+ }));
1019
+
1020
+ if (DEBUG) console.log('Button states in expansion bar:', buttonStates);
1021
+
1022
+ const allHidden = buttonStates.every(state => state.hidden);
1023
+
1024
+ if (allHidden) {
1025
+ expansionBar.style.display = 'none';
1026
+ if (DEBUG) console.log('All expansion buttons hidden - hiding expansion bar');
1027
+ } else {
1028
+ if (DEBUG) console.log('Not all buttons hidden - keeping expansion bar visible');
1029
+ }
1030
+ }
1031
+
1032
+ function checkAndMergeHunks(triggerButton) {
1033
+ // Get the current hunk element
1034
+ const context = hunkContext(triggerButton);
1035
+ if (!context?.fileElement) return;
1036
+
1037
+ const { currentHunk, prevHunk, nextHunk } = context;
1038
+ const currentStart = parseInt(currentHunk.dataset.lineStart);
1039
+ const currentEnd = parseInt(currentHunk.dataset.lineEnd);
1040
+
1041
+ // Check previous hunk for overlap
1042
+ if (prevHunk) {
1043
+ const prevEnd = parseInt(prevHunk.dataset.lineEnd);
1044
+
1045
+ // If current hunk now overlaps or touches previous hunk
1046
+ if (currentStart <= prevEnd + 1) {
1047
+ if (DEBUG) console.log(`Hunk merge detected: previous hunk ends at ${prevEnd}, current starts at ${currentStart}`);
1048
+ mergeHunks(prevHunk, currentHunk);
1049
+ return; // Exit after merge to avoid further processing
1050
+ }
1051
+ }
1052
+
1053
+ // Check next hunk for overlap
1054
+ if (nextHunk) {
1055
+ const nextStart = parseInt(nextHunk.dataset.lineStart);
1056
+
1057
+ // If current hunk now overlaps or touches next hunk
1058
+ if (currentEnd >= nextStart - 1) {
1059
+ if (DEBUG) console.log(`Hunk merge detected: current hunk ends at ${currentEnd}, next starts at ${nextStart}`);
1060
+ mergeHunks(currentHunk, nextHunk);
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ function mergeHunks(firstHunk, secondHunk) {
1066
+ // Simple merge: hide the expansion bar of the second hunk
1067
+ // In a full implementation, we'd merge the actual content
1068
+ const secondExpansionBar = secondHunk.querySelector('.hunk-expansion');
1069
+ if (secondExpansionBar) {
1070
+ secondExpansionBar.style.display = 'none';
1071
+ if (DEBUG) console.log('Merged hunks - hiding second expansion bar');
1072
+ }
1073
+
1074
+ // Update the line range of the first hunk to encompass both
1075
+ const firstStart = parseInt(firstHunk.dataset.lineStart);
1076
+ const firstEnd = parseInt(firstHunk.dataset.lineEnd);
1077
+ const secondStart = parseInt(secondHunk.dataset.lineStart);
1078
+ const secondEnd = parseInt(secondHunk.dataset.lineEnd);
1079
+
1080
+ const mergedStart = Math.min(firstStart, secondStart);
1081
+ const mergedEnd = Math.max(firstEnd, secondEnd);
1082
+
1083
+ firstHunk.dataset.lineStart = mergedStart;
1084
+ firstHunk.dataset.lineEnd = mergedEnd;
1085
+
1086
+ if (DEBUG) console.log(`Merged hunk range: ${mergedStart}-${mergedEnd}`);
1087
+ }
1088
+
1089
+ // Initialize when DOM is ready
1090
+ document.addEventListener('DOMContentLoaded', async() => {
1091
+ initializeTheme();
1092
+ await DiffState.init();
1093
+
1094
+ // Apply initial state - state has already been restored in restoreState()
1095
+ setTimeout(() => {
1096
+ // Just ensure files not in expanded state are properly collapsed
1097
+ $$('[data-file]').forEach((fileElement) => {
1098
+ const filePath = fileElement.dataset.file;
1099
+ const contentElement = $(`[data-file-content="${filePath}"]`);
1100
+ const toggleIcon = fileElement.querySelector('.toggle-icon');
1101
+
1102
+ if (contentElement && toggleIcon && !DiffState.expandedFiles.has(filePath)) {
1103
+ // Ensure non-expanded files are properly collapsed
1104
+ contentElement.style.display = 'none';
1105
+ toggleIcon.textContent = '▶';
1106
+ toggleIcon.dataset.expanded = 'false';
1107
+ }
1108
+ });
1109
+
1110
+ // Ensure non-expanded groups are properly collapsed
1111
+ const allPossibleGroups = ['untracked', 'unstaged', 'staged', 'changes'];
1112
+ allPossibleGroups.forEach(groupKey => {
1113
+ if (!DiffState.expandedGroups.has(groupKey)) {
1114
+ const contentElement = $(`[data-group-content="${groupKey}"]`);
1115
+ const toggleIcon = $(`[data-group="${groupKey}"] .toggle-icon`);
1116
+ if (contentElement && toggleIcon) {
1117
+ contentElement.style.display = 'none';
1118
+ toggleIcon.textContent = '▶';
1119
+ toggleIcon.dataset.expanded = 'false';
1120
+ }
1121
+ }
1122
+ });
1123
+
1124
+ // Ensure all expansion buttons are enabled and functional
1125
+ if (DEBUG) console.log('Initializing expansion buttons...');
1126
+ $$('.expansion-btn').forEach((button, index) => {
1127
+ const targetStart = parseInt(button.dataset.targetStart);
1128
+ const targetEnd = parseInt(button.dataset.targetEnd);
1129
+
1130
+ if (DEBUG) console.log(`Button ${index}: direction=${button.dataset.direction}, targetStart=${targetStart}, targetEnd=${targetEnd}`);
1131
+
1132
+ // Ensure button is properly enabled
1133
+ button.disabled = false;
1134
+ button.style.opacity = '1';
1135
+ button.style.cursor = 'pointer';
1136
+ button.style.pointerEvents = 'auto';
1137
+ button.title = `Expand 10 lines ${button.dataset.direction} (${targetStart}-${targetEnd})`;
1138
+
1139
+ if (DEBUG) console.log(`Button ${index} enabled and ready`);
1140
+ });
1141
+ }, 100);
1142
+ });
1143
+
1144
+ // Global functions for HTML onclick handlers
1145
+ window.toggleFile = toggleFile;
1146
+ window.toggleGroup = toggleGroup;
1147
+ window.expandAllFiles = expandAllFiles;
1148
+ window.collapseAllFiles = collapseAllFiles;
1149
+ window.navigateToPreviousFile = navigateToPreviousFile;
1150
+ window.navigateToNextFile = navigateToNextFile;
1151
+ window.expandContext = expandContext;
1152
+ window.toggleTheme = toggleTheme;
1153
+ // This is just to shut up eslint. It triggers the no-unused-vars
1154
+ // because it can't detect the usage because it's in the HTML in the
1155
+ // onclick handler.
1156
+ window.__loadFullDiff = loadFullDiff;
1157
+
1158
+ // Filename search helpers
1159
+ // Helper kept minimal; currently not highlighting individual headers in filter mode
1160
+
1161
+ // Note: focusNextFilenameMatch removed in favor of live filtering
1162
+
1163
+ function escapeRegExp(text) {
1164
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1165
+ }
1166
+
1167
+ function buildSearchRegex(query) {
1168
+ const raw = (query || '').trim();
1169
+ if (!raw) return null;
1170
+ const tokens = raw.split(/\s+/).filter(Boolean).map(escapeRegExp);
1171
+ if (tokens.length === 0) return null;
1172
+ const pattern = tokens.join('.*');
1173
+ const hasUpper = /[A-Z]/.test(raw);
1174
+ const flags = hasUpper ? '' : 'i';
1175
+ try {
1176
+ return new RegExp(pattern, flags);
1177
+ } catch (_e) {
1178
+ // Fallback to literal, case-insensitive contains
1179
+ return null;
1180
+ }
1181
+ }
1182
+
1183
+ function applyFilenameFilter(query) {
1184
+ const regex = buildSearchRegex(query);
1185
+ const lower = (query || '').toLowerCase();
1186
+ // Show/hide files
1187
+ let hiddenCount = 0;
1188
+ document.querySelectorAll('[data-file]').forEach(fileEl => {
1189
+ const headerNameEl = fileEl.querySelector('.file-header .font-mono');
1190
+ const name = headerNameEl ? (headerNameEl.textContent || '') : '';
1191
+ let matches;
1192
+ if (!query || query.length === 0) {
1193
+ matches = true;
1194
+ } else if (regex) {
1195
+ matches = regex.test(name);
1196
+ } else {
1197
+ // Fallback contains, case-insensitive
1198
+ matches = name.toLowerCase().includes(lower);
1199
+ }
1200
+ if (!matches) hiddenCount += 1;
1201
+ fileEl.style.display = matches ? '' : 'none';
1202
+ // Also hide associated content block to avoid large gaps
1203
+ const fileId = fileEl.getAttribute('data-file');
1204
+ const contentEl = document.querySelector(`[data-file-content="${CSS.escape(fileId)}"]`);
1205
+ if (contentEl) contentEl.style.display = matches ? contentEl.style.display : 'none';
1206
+ });
1207
+
1208
+ // Hide groups with no visible files
1209
+ document.querySelectorAll('.diff-group').forEach(groupEl => {
1210
+ const anyVisible = groupEl.querySelector('[data-file]:not([style*="display: none"])');
1211
+ groupEl.style.display = anyVisible ? '' : 'none';
1212
+ });
1213
+
1214
+ // Show hidden-count banner
1215
+ upsertHiddenBanner(hiddenCount);
1216
+ }
1217
+
1218
+ function upsertHiddenBanner(hiddenCount) {
1219
+ // Prefer the banner spot in global controls
1220
+ const banner = document.getElementById('hidden-files-banner');
1221
+ if (!banner) return;
1222
+ if (hiddenCount > 0) {
1223
+ banner.textContent = `${hiddenCount} file${hiddenCount === 1 ? '' : 's'} hidden by search`;
1224
+ banner.style.display = '';
1225
+ } else {
1226
+ banner.style.display = 'none';
1227
+ }
1228
+ }
1229
+
1230
+ /**
1231
+ * Load and display the complete diff for a file with unlimited context
1232
+ * @param {string} filePath - Path to the file
1233
+ * @param {string} fileId - Unique identifier for the file element
1234
+ */
1235
+ async function loadFullDiff(filePath, fileId) {
1236
+ const expandIcon = document.getElementById(`expand-icon-${fileId}`);
1237
+ const fileContentElement = document.querySelector(`[data-file-content="${fileId}"]`);
1238
+
1239
+ if (!expandIcon || !fileContentElement) {
1240
+ console.error('Full diff: Required elements not found');
1241
+ return;
1242
+ }
1243
+
1244
+ // Get current diff parameters from URL or app state
1245
+ const urlParams = new URLSearchParams(window.location.search);
1246
+ const baseRef = urlParams.get('base_ref');
1247
+ const useHead = urlParams.get('use_head') === 'true';
1248
+ const useCached = urlParams.get('use_cached') === 'true';
1249
+
1250
+ // Show loading state
1251
+ const originalIconContent = expandIcon.textContent;
1252
+ expandIcon.textContent = '⏳';
1253
+ expandIcon.style.pointerEvents = 'none';
1254
+
1255
+ // Show loading indicator in content area
1256
+ fileContentElement.innerHTML = `
1257
+ <div class="p-8 text-center text-neutral-500">
1258
+ <div class="text-4xl mb-2">⏳</div>
1259
+ <p>Loading full diff...</p>
1260
+ <p class="text-sm">Fetching complete file comparison with unlimited context</p>
1261
+ </div>
1262
+ `;
1263
+
1264
+ try {
1265
+ // Build API URL with parameters
1266
+ const apiUrl = new URL('/api/diff/full', window.location.origin);
1267
+ apiUrl.searchParams.set('file_path', filePath);
1268
+ if (baseRef) apiUrl.searchParams.set('base_ref', baseRef);
1269
+ if (useHead) apiUrl.searchParams.set('use_head', 'true');
1270
+ if (useCached) apiUrl.searchParams.set('use_cached', 'true');
1271
+
1272
+ const response = await fetch(apiUrl.toString());
1273
+ const result = await response.json();
1274
+
1275
+ if (result.status === 'ok') {
1276
+ if (result.has_changes) {
1277
+ await renderFullDiff(fileContentElement, result, fileId);
1278
+
1279
+ // Hide the expand icon permanently
1280
+ expandIcon.style.display = 'none';
1281
+ } else {
1282
+ // No changes to show
1283
+ fileContentElement.innerHTML = `
1284
+ <div class="p-8 text-center text-neutral-500">
1285
+ <div class="text-4xl mb-2">✓</div>
1286
+ <p>No changes in this file</p>
1287
+ <p class="text-sm">${result.comparison_mode}</p>
1288
+ </div>
1289
+ `;
1290
+ }
1291
+ } else {
1292
+ throw new Error(result.message || 'Failed to load full diff');
1293
+ }
1294
+ } catch (error) {
1295
+ console.error('Full diff error:', error);
1296
+ fileContentElement.innerHTML = `
1297
+ <div class="p-8 text-center text-danger-text-500">
1298
+ <div class="text-4xl mb-2">⚠️</div>
1299
+ <p class="font-medium">Failed to load full diff</p>
1300
+ <p class="text-sm mt-2">${escapeHtml(error.message)}</p>
1301
+ <p class="text-xs mt-4 text-neutral-400">Check the browser console for more details</p>
1302
+ <button onclick="loadFullDiff('${filePath}', '${fileId}')"
1303
+ class="mt-4 px-3 py-1 text-sm bg-danger-bg-100 text-danger-text-700 rounded hover:bg-danger-bg-200 transition-colors">
1304
+ Retry
1305
+ </button>
1306
+ </div>
1307
+ `;
1308
+ // Reset icon state on error
1309
+ expandIcon.textContent = originalIconContent;
1310
+ expandIcon.style.pointerEvents = '';
1311
+ } finally {
1312
+ // Restore icon state if still in loading mode
1313
+ if (expandIcon.textContent === '⏳') {
1314
+ expandIcon.textContent = originalIconContent;
1315
+ expandIcon.style.pointerEvents = '';
1316
+ }
1317
+ }
1318
+ }
1319
+
1320
+ /**
1321
+ * Render the full diff data into the file content element
1322
+ * @param {HTMLElement} contentElement - Container to render diff into
1323
+ * @param {Object} diffData - Full diff data from API
1324
+ * @param {string} fileId - File identifier
1325
+ */
1326
+ async function renderFullDiff(contentElement, diffData, fileId) {
1327
+ // Add debug logging to understand what we receive
1328
+ console.log('Rendering full diff for', diffData.file_path);
1329
+ console.log('Diff data structure:', diffData);
1330
+
1331
+ if (diffData.diff_data && diffData.diff_data.hunks && diffData.diff_data.hunks.length > 0) {
1332
+ console.log('Using parsed diff data with', diffData.diff_data.hunks.length, 'hunks');
1333
+ console.log('First hunk sample:', diffData.diff_data.hunks[0]);
1334
+
1335
+ // Use parsed diff data to render side-by-side hunks
1336
+ let htmlContent = '';
1337
+
1338
+ for (let i = 0; i < diffData.diff_data.hunks.length; i++) {
1339
+ const hunk = diffData.diff_data.hunks[i];
1340
+ console.log(`Hunk ${i} has ${hunk.lines?.length || 0} lines`);
1341
+ if (hunk.lines && hunk.lines.length > 0) {
1342
+ console.log(`First few lines of hunk ${i}:`, hunk.lines.slice(0, 3));
1343
+ }
1344
+ htmlContent += renderSideBySideHunk(hunk, diffData.file_path, i);
1345
+ }
1346
+
1347
+ contentElement.innerHTML = htmlContent;
1348
+ } else if (diffData.diff_content && diffData.diff_content.trim()) {
1349
+ console.log('Using raw diff content fallback');
1350
+ // Render raw diff content with better formatting
1351
+ const lines = diffData.diff_content.split('\n');
1352
+ let htmlContent = `
1353
+ <div class="full-diff-container">
1354
+ <div class="full-diff-header bg-neutral-100 px-4 py-2 text-sm text-neutral-700 border-b">
1355
+ <span class="font-medium">Full diff:</span> ${escapeHtml(diffData.comparison_mode)} (unlimited context)
1356
+ </div>
1357
+ <div class="full-diff-content">
1358
+ `;
1359
+
1360
+ for (const line of lines) {
1361
+ if (line.startsWith('@@')) {
1362
+ // Hunk header
1363
+ htmlContent += `<div class="hunk-header bg-neutral-50 px-4 py-2 text-sm font-mono text-neutral-600 border-b border-neutral-200">${escapeHtml(line)}</div>`;
1364
+ } else if (line.startsWith('+')) {
1365
+ // Addition
1366
+ htmlContent += `<div class="diff-line addition bg-success-bg-50 border-l-4 border-success-bg-300 px-4 py-1 text-sm font-mono"><span class="text-success-text-600">+</span>${escapeHtml(line.substring(1))}</div>`;
1367
+ } else if (line.startsWith('-')) {
1368
+ // Deletion
1369
+ htmlContent += `<div class="diff-line deletion bg-danger-bg-50 border-l-4 border-danger-bg-300 px-4 py-1 text-sm font-mono"><span class="text-danger-text-600">-</span>${escapeHtml(line.substring(1))}</div>`;
1370
+ } else if (line.startsWith(' ') || line === '') {
1371
+ // Context line
1372
+ htmlContent += `<div class="diff-line context bg-white px-4 py-1 text-sm font-mono"><span class="text-neutral-400"> </span>${escapeHtml(line.substring(1) || '')}</div>`;
1373
+ } else if (line.startsWith('diff --git') || line.startsWith('index ') || line.startsWith('+++') || line.startsWith('---')) {
1374
+ // File header lines - skip or style differently
1375
+ htmlContent += `<div class="file-header-line text-xs text-neutral-500 px-4 py-1 font-mono bg-neutral-25">${escapeHtml(line)}</div>`;
1376
+ } else {
1377
+ // Other lines
1378
+ htmlContent += `<div class="diff-line other px-4 py-1 text-sm font-mono text-neutral-600">${escapeHtml(line)}</div>`;
1379
+ }
1380
+ }
1381
+
1382
+ htmlContent += `
1383
+ </div>
1384
+ </div>
1385
+ `;
1386
+
1387
+ contentElement.innerHTML = htmlContent;
1388
+ } else {
1389
+ console.log('No diff content available');
1390
+ // No diff content available
1391
+ contentElement.innerHTML = `
1392
+ <div class="p-8 text-center text-neutral-500">
1393
+ <div class="text-4xl mb-2">📄</div>
1394
+ <p>No diff content available</p>
1395
+ <p class="text-sm">The full diff is empty or could not be processed.</p>
1396
+ <p class="text-xs mt-2 text-neutral-400">Comparison: ${escapeHtml(diffData.comparison_mode || 'unknown')}</p>
1397
+ </div>
1398
+ `;
1399
+ }
1400
+ }
1401
+
1402
+ /**
1403
+ * Check if content appears to be syntax-highlighted HTML
1404
+ * @param {string} content - Content to check
1405
+ * @returns {boolean} True if content appears to be HTML
1406
+ */
1407
+ function isHighlightedContent(content) {
1408
+ // Check for HTML tags or entities that indicate syntax highlighting
1409
+ return content && (
1410
+ content.includes('<span') ||
1411
+ content.includes('</span>') ||
1412
+ content.includes('&nbsp;') ||
1413
+ content.includes('&lt;') ||
1414
+ content.includes('&gt;') ||
1415
+ content.includes('&amp;') ||
1416
+ content.includes('<code>') ||
1417
+ content.includes('<em>') ||
1418
+ content.includes('<strong>')
1419
+ );
1420
+ }
1421
+
1422
+ /**
1423
+ * Create HTML for a single diff hunk in side-by-side format
1424
+ * @param {Object} hunk - Hunk data from the diff parser
1425
+ * @param {string} filePath - File path
1426
+ * @param {number} hunkIndex - Hunk index
1427
+ * @returns {string} HTML string
1428
+ */
1429
+ function renderSideBySideHunk(hunk, filePath, hunkIndex) {
1430
+ console.log('Rendering hunk', hunkIndex, 'with', hunk.lines?.length || 0, 'lines');
1431
+
1432
+ // Debug: show line types for first few lines
1433
+ if (hunk.lines && hunk.lines.length > 0) {
1434
+ const sampleLines = hunk.lines.slice(0, 5);
1435
+ console.log('Sample line types:', sampleLines.map(l => ({ type: l.type, hasLeft: !!l.left, hasRight: !!l.right, leftContent: l.left?.content?.substring(0, 20), rightContent: l.right?.content?.substring(0, 20) })));
1436
+ }
1437
+
1438
+ let html = `
1439
+ <div class="hunk border-b border-neutral-100 last:border-b-0">
1440
+ <!-- Hunk Lines -->
1441
+ <div class="hunk-lines font-mono text-xs">
1442
+ `;
1443
+
1444
+ if (hunk.lines && Array.isArray(hunk.lines)) {
1445
+ for (const line of hunk.lines) {
1446
+ html += renderSideBySideLine(line);
1447
+ }
1448
+ } else {
1449
+ console.warn('Hunk has no lines array:', hunk);
1450
+ html += '<div class="p-4 text-center text-neutral-500">No line data available for this hunk</div>';
1451
+ }
1452
+
1453
+ html += `
1454
+ </div>
1455
+ </div>
1456
+ `;
1457
+
1458
+ return html;
1459
+ }
1460
+
1461
+ /**
1462
+ * Render a single line in side-by-side format
1463
+ * @param {Object} line - Line data from diff parser
1464
+ * @returns {string} HTML string
1465
+ */
1466
+ function renderSideBySideLine(line) {
1467
+ // Handle the complex line structure that the diff parser creates
1468
+ // The line might have 'left' and 'right' properties for side-by-side view
1469
+
1470
+ let leftContent = '';
1471
+ let rightContent = '';
1472
+ let leftLineNum = '';
1473
+ let rightLineNum = '';
1474
+ let leftBg = 'bg-white';
1475
+ let rightBg = 'bg-white';
1476
+
1477
+ if (line.type === 'context') {
1478
+ // Context line appears on both sides
1479
+ leftContent = line.left?.content || '';
1480
+ rightContent = line.right?.content || '';
1481
+ leftLineNum = line.left?.line_num || '';
1482
+ rightLineNum = line.right?.line_num || '';
1483
+ leftBg = 'bg-neutral-25';
1484
+ rightBg = 'bg-neutral-25';
1485
+ } else if (line.left && line.right) {
1486
+ // Changed line - deletion on left, addition on right
1487
+ leftContent = line.left.content || '';
1488
+ rightContent = line.right.content || '';
1489
+ leftLineNum = line.left.line_num || '';
1490
+ rightLineNum = line.right.line_num || '';
1491
+ leftBg = 'bg-danger-bg-50';
1492
+ rightBg = 'bg-success-bg-50';
1493
+ } else if (line.left) {
1494
+ // Deletion only
1495
+ leftContent = line.left.content || '';
1496
+ leftLineNum = line.left.line_num || '';
1497
+ leftBg = 'bg-red-50';
1498
+ } else if (line.right) {
1499
+ // Addition only
1500
+ rightContent = line.right.content || '';
1501
+ rightLineNum = line.right.line_num || '';
1502
+ rightBg = 'bg-success-bg-50';
1503
+ } else {
1504
+ // Simple line structure
1505
+ if (line.type === 'addition') {
1506
+ rightContent = line.content || '';
1507
+ rightLineNum = line.new_line_number || '';
1508
+ rightBg = 'bg-success-bg-50';
1509
+ } else if (line.type === 'deletion') {
1510
+ leftContent = line.content || '';
1511
+ leftLineNum = line.old_line_number || '';
1512
+ leftBg = 'bg-danger-bg-50';
1513
+ } else {
1514
+ leftContent = rightContent = line.content || '';
1515
+ leftLineNum = line.old_line_number || '';
1516
+ rightLineNum = line.new_line_number || '';
1517
+ }
1518
+ }
1519
+
1520
+ return `
1521
+ <div class="diff-line grid grid-cols-2 border-b border-gray-50 hover:bg-neutral-25 line-${line.type || 'context'}">
1522
+ <!-- Left Side (Before) -->
1523
+ <div class="line-left border-r border-neutral-200 ${leftBg}">
1524
+ <div class="flex">
1525
+ <div class="line-num w-12 px-2 py-1 text-neutral-400 text-right bg-neutral-50 border-r border-neutral-200 select-none">
1526
+ ${leftLineNum ? `<span>${leftLineNum}</span>` : ''}
1527
+ </div>
1528
+ <div class="line-content flex-1 px-2 py-1 overflow-x-auto">
1529
+ ${leftContent
1530
+ ? (leftBg.includes('danger') ? `<span class="text-danger-text-600">-</span><span>${isHighlightedContent(leftContent) ? leftContent : escapeHtml(leftContent)}</span>` : `<span class="text-neutral-400">&nbsp;</span><span>${isHighlightedContent(leftContent) ? leftContent : escapeHtml(leftContent)}</span>`)
1531
+ : ''}
1532
+ </div>
1533
+ </div>
1534
+ </div>
1535
+
1536
+ <!-- Right Side (After) -->
1537
+ <div class="line-right ${rightBg}">
1538
+ <div class="flex">
1539
+ <div class="line-num w-12 px-2 py-1 text-neutral-400 text-right bg-neutral-50 border-r border-neutral-200 select-none">
1540
+ ${rightLineNum ? `<span>${rightLineNum}</span>` : ''}
1541
+ </div>
1542
+ <div class="line-content flex-1 px-2 py-1 overflow-x-auto">
1543
+ ${rightContent
1544
+ ? (rightBg.includes('success') ? `<span class="text-success-text-600">+</span><span>${isHighlightedContent(rightContent) ? rightContent : escapeHtml(rightContent)}</span>` : `<span class="text-neutral-400">&nbsp;</span><span>${isHighlightedContent(rightContent) ? rightContent : escapeHtml(rightContent)}</span>`)
1545
+ : ''}
1546
+ </div>
1547
+ </div>
1548
+ </div>
1549
+ </div>
1550
+ `;
1551
+ }
1552
+
1553
+ // Theme switching functionality
1554
+ function toggleTheme() {
1555
+ const htmlElement = document.documentElement;
1556
+ const isDark = htmlElement.getAttribute('data-theme') === 'dark';
1557
+ const newTheme = isDark ? 'light' : 'dark';
1558
+
1559
+ if (isDark) {
1560
+ htmlElement.removeAttribute('data-theme');
1561
+ } else {
1562
+ htmlElement.setAttribute('data-theme', 'dark');
1563
+ }
1564
+
1565
+ DiffState.theme = newTheme;
1566
+
1567
+ // Update theme icon
1568
+ const themeIcon = document.getElementById('theme-icon');
1569
+ if (themeIcon) {
1570
+ themeIcon.textContent = newTheme === 'dark' ? '☀️' : '🌙';
1571
+ }
1572
+
1573
+ // Save theme preference
1574
+ localStorage.setItem('difflicious-theme', newTheme);
1575
+
1576
+ if (DEBUG) console.log(`Theme switched to ${newTheme}`);
1577
+
1578
+ // Prevent any form submission or navigation
1579
+ return false;
1580
+ }
1581
+
1582
+ // Initialize theme on load
1583
+ function initializeTheme() {
1584
+ const savedTheme = localStorage.getItem('difflicious-theme');
1585
+ const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1586
+ const defaultTheme = savedTheme || (systemPrefersDark ? 'dark' : 'light');
1587
+
1588
+ if (defaultTheme === 'dark') {
1589
+ document.documentElement.setAttribute('data-theme', 'dark');
1590
+ } else {
1591
+ document.documentElement.removeAttribute('data-theme');
1592
+ }
1593
+
1594
+ DiffState.theme = defaultTheme;
1595
+
1596
+ // Update theme icon
1597
+ const themeIcon = document.getElementById('theme-icon');
1598
+ if (themeIcon) {
1599
+ themeIcon.textContent = defaultTheme === 'dark' ? '☀️' : '🌙';
1600
+ }
1601
+
1602
+ // Listen for system theme changes
1603
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
1604
+ if (!localStorage.getItem('difflicious-theme')) {
1605
+ const newTheme = e.matches ? 'dark' : 'light';
1606
+ if (newTheme === 'dark') {
1607
+ document.documentElement.setAttribute('data-theme', 'dark');
1608
+ } else {
1609
+ document.documentElement.removeAttribute('data-theme');
1610
+ }
1611
+ DiffState.theme = newTheme;
1612
+ if (themeIcon) {
1613
+ themeIcon.textContent = newTheme === 'dark' ? '☀️' : '🌙';
1614
+ }
1615
+ }
1616
+ });
1617
+ }