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.
- difflicious/__init__.py +6 -0
- difflicious/app.py +505 -0
- difflicious/cli.py +77 -0
- difflicious/diff_parser.py +525 -0
- difflicious/dummy_data.json +44 -0
- difflicious/git_operations.py +1005 -0
- difflicious/services/__init__.py +1 -0
- difflicious/services/base_service.py +32 -0
- difflicious/services/diff_service.py +403 -0
- difflicious/services/exceptions.py +19 -0
- difflicious/services/git_service.py +135 -0
- difflicious/services/syntax_service.py +162 -0
- difflicious/services/template_service.py +382 -0
- difflicious/static/css/styles.css +885 -0
- difflicious/static/css/tailwind.css +1 -0
- difflicious/static/css/tailwind.input.css +5 -0
- difflicious/static/js/app.js +1002 -0
- difflicious/static/js/diff-interactions.js +1617 -0
- difflicious/templates/base.html +54 -0
- difflicious/templates/diff_file.html +90 -0
- difflicious/templates/diff_groups.html +29 -0
- difflicious/templates/diff_hunk.html +170 -0
- difflicious/templates/index.html +54 -0
- difflicious/templates/partials/empty_state.html +29 -0
- difflicious/templates/partials/global_controls.html +23 -0
- difflicious/templates/partials/loading_state.html +7 -0
- difflicious/templates/partials/toolbar.html +165 -0
- difflicious-0.1.0.dist-info/METADATA +190 -0
- difflicious-0.1.0.dist-info/RECORD +32 -0
- difflicious-0.1.0.dist-info/WHEEL +4 -0
- difflicious-0.1.0.dist-info/entry_points.txt +2 -0
- difflicious-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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"> </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"> </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"> </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"> </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(' ') ||
|
|
1413
|
+
content.includes('<') ||
|
|
1414
|
+
content.includes('>') ||
|
|
1415
|
+
content.includes('&') ||
|
|
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"> </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"> </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
|
+
}
|