gh-here 3.0.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +0 -0
- package/lib/constants.js +21 -16
- package/lib/content-search.js +212 -0
- package/lib/error-handler.js +39 -28
- package/lib/file-utils.js +438 -287
- package/lib/git.js +11 -55
- package/lib/gitignore.js +70 -41
- package/lib/renderers.js +17 -33
- package/lib/server.js +73 -196
- package/lib/symbol-parser.js +600 -0
- package/package.json +1 -1
- package/public/app.js +135 -68
- package/public/css/components/buttons.css +423 -0
- package/public/css/components/forms.css +171 -0
- package/public/css/components/modals.css +286 -0
- package/public/css/components/notifications.css +36 -0
- package/public/css/file-table.css +318 -0
- package/public/css/file-tree.css +269 -0
- package/public/css/file-viewer.css +1259 -0
- package/public/css/layout.css +372 -0
- package/public/css/main.css +35 -0
- package/public/css/reset.css +64 -0
- package/public/css/search.css +694 -0
- package/public/css/symbol-outline.css +279 -0
- package/public/css/variables.css +135 -0
- package/public/js/constants.js +50 -34
- package/public/js/content-search-handler.js +551 -0
- package/public/js/file-viewer.js +437 -0
- package/public/js/focus-mode.js +280 -0
- package/public/js/inline-search.js +659 -0
- package/public/js/modal-manager.js +14 -28
- package/public/js/symbol-outline.js +454 -0
- package/public/js/utils.js +152 -94
- package/.claude/settings.local.json +0 -30
- package/SAMPLE.md +0 -287
- package/lib/validation.js +0 -77
- package/public/app.js.backup +0 -1902
- package/public/highlight.css +0 -121
- package/public/js/draft-manager.js +0 -36
- package/public/js/editor-manager.js +0 -159
- package/public/styles.css +0 -2727
- package/test.js +0 -138
- package/tests/draftManager.test.js +0 -241
- package/tests/fileTypeDetection.test.js +0 -111
- package/tests/httpService.test.js +0 -268
- package/tests/languageDetection.test.js +0 -145
- package/tests/pathUtils.test.js +0 -136
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-file search functionality
|
|
3
|
+
* Search within the current file with match highlighting
|
|
4
|
+
*
|
|
5
|
+
* @class InlineSearch
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { escapeHtml } from './utils.js';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Constants (alpha-sorted)
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
const CODE_BLOCK_SELECTOR = '.file-content pre code.hljs.with-line-numbers';
|
|
15
|
+
const DEBOUNCE_DELAY = 200;
|
|
16
|
+
const SEARCH_OVERLAY_CLASS = 'inline-search-overlay';
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// InlineSearch Class
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
export class InlineSearch {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.isActive = false;
|
|
25
|
+
this.searchOverlay = null;
|
|
26
|
+
this.searchInput = null;
|
|
27
|
+
this.currentMatchIndex = -1;
|
|
28
|
+
this.matches = [];
|
|
29
|
+
this.caseSensitive = false;
|
|
30
|
+
this.regexMode = false;
|
|
31
|
+
this.originalContent = new Map();
|
|
32
|
+
this.eventHandlers = new Map();
|
|
33
|
+
this.debounceTimer = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ========================================================================
|
|
37
|
+
// Public API
|
|
38
|
+
// ========================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize inline search
|
|
42
|
+
*/
|
|
43
|
+
init() {
|
|
44
|
+
if (!this.isFileViewPage()) return;
|
|
45
|
+
|
|
46
|
+
// Skip if Monaco viewer is active (Monaco has built-in search)
|
|
47
|
+
if (document.querySelector('.monaco-file-viewer')) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.storeOriginalContent();
|
|
52
|
+
this.setupKeyboardShortcuts();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Show search overlay
|
|
57
|
+
*/
|
|
58
|
+
show() {
|
|
59
|
+
if (this.isActive) {
|
|
60
|
+
this.focusSearchInput();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.isActive = true;
|
|
65
|
+
this.createSearchOverlay();
|
|
66
|
+
document.body.appendChild(this.searchOverlay);
|
|
67
|
+
this.focusSearchInput();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Hide search overlay and restore original content
|
|
72
|
+
*/
|
|
73
|
+
hide() {
|
|
74
|
+
if (!this.isActive) return;
|
|
75
|
+
|
|
76
|
+
this.clearHighlights();
|
|
77
|
+
this.cleanupDebounce();
|
|
78
|
+
this.removeSearchOverlay();
|
|
79
|
+
this.resetState();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Cleanup: Remove event listeners and restore content
|
|
84
|
+
*/
|
|
85
|
+
destroy() {
|
|
86
|
+
this.hide();
|
|
87
|
+
this.removeEventListeners();
|
|
88
|
+
this.originalContent.clear();
|
|
89
|
+
this.matches = [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ========================================================================
|
|
93
|
+
// Search Operations
|
|
94
|
+
// ========================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Perform search and highlight matches
|
|
98
|
+
*/
|
|
99
|
+
performSearch() {
|
|
100
|
+
if (!this.searchInput) return;
|
|
101
|
+
|
|
102
|
+
const query = this.searchInput.value.trim();
|
|
103
|
+
|
|
104
|
+
if (!query) {
|
|
105
|
+
this.clearHighlights();
|
|
106
|
+
this.updateMatchCount(0);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.clearHighlights();
|
|
111
|
+
this.resetMatches();
|
|
112
|
+
|
|
113
|
+
const codeBlock = this.findCodeBlock();
|
|
114
|
+
if (!codeBlock) return;
|
|
115
|
+
|
|
116
|
+
const pattern = this.createSearchPattern(query);
|
|
117
|
+
if (!pattern) return;
|
|
118
|
+
|
|
119
|
+
this.findMatches(codeBlock, pattern);
|
|
120
|
+
this.displayMatches();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create search pattern from query
|
|
125
|
+
*/
|
|
126
|
+
createSearchPattern(query) {
|
|
127
|
+
try {
|
|
128
|
+
if (this.regexMode) {
|
|
129
|
+
return new RegExp(query, this.caseSensitive ? 'g' : 'gi');
|
|
130
|
+
} else {
|
|
131
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
132
|
+
return new RegExp(escaped, this.caseSensitive ? 'g' : 'gi');
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.updateMatchCount(0, 'Invalid regular expression');
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Find all matches in code block
|
|
142
|
+
*/
|
|
143
|
+
findMatches(codeBlock, pattern) {
|
|
144
|
+
const lines = codeBlock.querySelectorAll('.line-container');
|
|
145
|
+
|
|
146
|
+
lines.forEach((line, lineIndex) => {
|
|
147
|
+
const lineContent = line.querySelector('.line-content');
|
|
148
|
+
if (!lineContent) return;
|
|
149
|
+
|
|
150
|
+
const text = lineContent.textContent;
|
|
151
|
+
const lineMatches = this.findLineMatches(text, pattern, lineIndex + 1, line, lineContent);
|
|
152
|
+
this.matches.push(...lineMatches);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find matches in a single line
|
|
158
|
+
*/
|
|
159
|
+
findLineMatches(text, pattern, lineNum, lineElement, contentElement) {
|
|
160
|
+
const matches = [];
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const patternMatches = [...text.matchAll(pattern)];
|
|
164
|
+
|
|
165
|
+
patternMatches.forEach(match => {
|
|
166
|
+
if (match.index !== undefined && match[0]) {
|
|
167
|
+
matches.push({
|
|
168
|
+
line: lineNum,
|
|
169
|
+
lineElement,
|
|
170
|
+
contentElement,
|
|
171
|
+
index: match.index,
|
|
172
|
+
length: match[0].length
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// Handle regex errors gracefully
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return matches;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Display matches with highlighting
|
|
185
|
+
*/
|
|
186
|
+
displayMatches() {
|
|
187
|
+
if (this.matches.length > 0) {
|
|
188
|
+
this.highlightMatches();
|
|
189
|
+
this.currentMatchIndex = 0;
|
|
190
|
+
this.scrollToMatch(0);
|
|
191
|
+
this.updateMatchCount(this.matches.length, null, 1);
|
|
192
|
+
} else {
|
|
193
|
+
this.updateMatchCount(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Highlight all matches in the code
|
|
199
|
+
*/
|
|
200
|
+
highlightMatches() {
|
|
201
|
+
const matchesByLine = this.groupMatchesByLine();
|
|
202
|
+
|
|
203
|
+
matchesByLine.forEach((lineMatches, lineNum) => {
|
|
204
|
+
const match = this.matches.find(m => m.line === lineNum);
|
|
205
|
+
if (!match?.contentElement) return;
|
|
206
|
+
|
|
207
|
+
const highlighted = this.buildHighlightedHTML(match.contentElement.textContent, lineMatches);
|
|
208
|
+
match.contentElement.innerHTML = highlighted;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Group matches by line number
|
|
214
|
+
*/
|
|
215
|
+
groupMatchesByLine() {
|
|
216
|
+
const matchesByLine = new Map();
|
|
217
|
+
|
|
218
|
+
this.matches.forEach((match, index) => {
|
|
219
|
+
if (!matchesByLine.has(match.line)) {
|
|
220
|
+
matchesByLine.set(match.line, []);
|
|
221
|
+
}
|
|
222
|
+
matchesByLine.get(match.line).push({ ...match, globalIndex: index });
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return matchesByLine;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build highlighted HTML for a line
|
|
230
|
+
*/
|
|
231
|
+
buildHighlightedHTML(text, lineMatches) {
|
|
232
|
+
let highlighted = '';
|
|
233
|
+
let lastIndex = 0;
|
|
234
|
+
|
|
235
|
+
const sortedMatches = lineMatches.sort((a, b) => a.index - b.index);
|
|
236
|
+
|
|
237
|
+
sortedMatches.forEach((m) => {
|
|
238
|
+
highlighted += escapeHtml(text.substring(lastIndex, m.index));
|
|
239
|
+
|
|
240
|
+
const isActive = m.globalIndex === this.currentMatchIndex;
|
|
241
|
+
const matchText = text.substring(m.index, m.index + m.length);
|
|
242
|
+
highlighted += `<mark class="inline-search-match ${isActive ? 'active' : ''}" data-match-index="${m.globalIndex}">${escapeHtml(matchText)}</mark>`;
|
|
243
|
+
|
|
244
|
+
lastIndex = m.index + m.length;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
highlighted += escapeHtml(text.substring(lastIndex));
|
|
248
|
+
return highlighted;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clear highlights and restore original content
|
|
253
|
+
*/
|
|
254
|
+
clearHighlights() {
|
|
255
|
+
const codeBlock = this.findCodeBlock();
|
|
256
|
+
if (!codeBlock) return;
|
|
257
|
+
|
|
258
|
+
const lines = codeBlock.querySelectorAll('.line-container');
|
|
259
|
+
lines.forEach((line, index) => {
|
|
260
|
+
const lineContent = line.querySelector('.line-content');
|
|
261
|
+
if (!lineContent) return;
|
|
262
|
+
|
|
263
|
+
const original = this.originalContent.get(index + 1);
|
|
264
|
+
if (original !== undefined) {
|
|
265
|
+
lineContent.innerHTML = original;
|
|
266
|
+
} else {
|
|
267
|
+
lineContent.textContent = lineContent.textContent;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ========================================================================
|
|
273
|
+
// Navigation
|
|
274
|
+
// ========================================================================
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Navigate to next match
|
|
278
|
+
*/
|
|
279
|
+
nextMatch() {
|
|
280
|
+
if (this.matches.length === 0) return;
|
|
281
|
+
|
|
282
|
+
this.currentMatchIndex = (this.currentMatchIndex + 1) % this.matches.length;
|
|
283
|
+
this.navigateToMatch(this.currentMatchIndex);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Navigate to previous match
|
|
288
|
+
*/
|
|
289
|
+
previousMatch() {
|
|
290
|
+
if (this.matches.length === 0) return;
|
|
291
|
+
|
|
292
|
+
this.currentMatchIndex = this.currentMatchIndex <= 0
|
|
293
|
+
? this.matches.length - 1
|
|
294
|
+
: this.currentMatchIndex - 1;
|
|
295
|
+
this.navigateToMatch(this.currentMatchIndex);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Navigate to a specific match
|
|
300
|
+
*/
|
|
301
|
+
navigateToMatch(index) {
|
|
302
|
+
this.scrollToMatch(index);
|
|
303
|
+
this.updateActiveMatch();
|
|
304
|
+
this.updateMatchCount(this.matches.length, null, index + 1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Scroll to a specific match
|
|
309
|
+
*/
|
|
310
|
+
scrollToMatch(index) {
|
|
311
|
+
if (index < 0 || index >= this.matches.length) return;
|
|
312
|
+
|
|
313
|
+
const match = this.matches[index];
|
|
314
|
+
if (match?.lineElement) {
|
|
315
|
+
match.lineElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Update active match highlighting
|
|
321
|
+
*/
|
|
322
|
+
updateActiveMatch() {
|
|
323
|
+
document.querySelectorAll('.inline-search-match').forEach(m => {
|
|
324
|
+
m.classList.remove('active');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const activeMatch = document.querySelector(
|
|
328
|
+
`.inline-search-match[data-match-index="${this.currentMatchIndex}"]`
|
|
329
|
+
);
|
|
330
|
+
if (activeMatch) {
|
|
331
|
+
activeMatch.classList.add('active');
|
|
332
|
+
this.highlightMatches(); // Re-highlight to update active state
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ========================================================================
|
|
337
|
+
// UI Management
|
|
338
|
+
// ========================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Create search overlay UI
|
|
342
|
+
*/
|
|
343
|
+
createSearchOverlay() {
|
|
344
|
+
this.searchOverlay = document.createElement('div');
|
|
345
|
+
this.searchOverlay.className = SEARCH_OVERLAY_CLASS;
|
|
346
|
+
this.searchOverlay.setAttribute('role', 'dialog');
|
|
347
|
+
this.searchOverlay.setAttribute('aria-label', 'Search in file');
|
|
348
|
+
this.searchOverlay.innerHTML = this.getSearchOverlayHTML();
|
|
349
|
+
|
|
350
|
+
this.attachSearchOverlayHandlers();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get search overlay HTML template
|
|
355
|
+
*/
|
|
356
|
+
getSearchOverlayHTML() {
|
|
357
|
+
return `
|
|
358
|
+
<div class="inline-search-container">
|
|
359
|
+
<div class="inline-search-input-wrapper">
|
|
360
|
+
<input
|
|
361
|
+
type="text"
|
|
362
|
+
class="inline-search-input"
|
|
363
|
+
placeholder="Search in file..."
|
|
364
|
+
autocomplete="off"
|
|
365
|
+
aria-label="Search query"
|
|
366
|
+
/>
|
|
367
|
+
<div class="inline-search-options">
|
|
368
|
+
<button class="inline-search-option" data-option="case" title="Case sensitive" aria-pressed="false">
|
|
369
|
+
<span class="option-label">Aa</span>
|
|
370
|
+
</button>
|
|
371
|
+
<button class="inline-search-option" data-option="regex" title="Use regular expression" aria-pressed="false">
|
|
372
|
+
<span class="option-label">.*</span>
|
|
373
|
+
</button>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="inline-search-info">
|
|
377
|
+
<span class="inline-search-count">No matches</span>
|
|
378
|
+
<button class="inline-search-close" aria-label="Close search">×</button>
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Attach event handlers to search overlay elements
|
|
386
|
+
*/
|
|
387
|
+
attachSearchOverlayHandlers() {
|
|
388
|
+
this.searchInput = this.searchOverlay.querySelector('.inline-search-input');
|
|
389
|
+
const closeBtn = this.searchOverlay.querySelector('.inline-search-close');
|
|
390
|
+
const caseBtn = this.searchOverlay.querySelector('[data-option="case"]');
|
|
391
|
+
const regexBtn = this.searchOverlay.querySelector('[data-option="regex"]');
|
|
392
|
+
|
|
393
|
+
this.attachCloseHandler(closeBtn);
|
|
394
|
+
this.attachCaseHandler(caseBtn);
|
|
395
|
+
this.attachRegexHandler(regexBtn);
|
|
396
|
+
this.attachInputHandler(this.searchInput);
|
|
397
|
+
this.updateButtonStates(caseBtn, regexBtn);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Attach close button handler
|
|
402
|
+
*/
|
|
403
|
+
attachCloseHandler(closeBtn) {
|
|
404
|
+
const handler = () => this.hide();
|
|
405
|
+
closeBtn.addEventListener('click', handler);
|
|
406
|
+
this.eventHandlers.set('close', handler);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Attach case sensitive toggle handler
|
|
411
|
+
*/
|
|
412
|
+
attachCaseHandler(caseBtn) {
|
|
413
|
+
const handler = () => {
|
|
414
|
+
this.caseSensitive = !this.caseSensitive;
|
|
415
|
+
caseBtn.classList.toggle('active', this.caseSensitive);
|
|
416
|
+
caseBtn.setAttribute('aria-pressed', String(this.caseSensitive));
|
|
417
|
+
this.performSearch();
|
|
418
|
+
};
|
|
419
|
+
caseBtn.addEventListener('click', handler);
|
|
420
|
+
this.eventHandlers.set('case', handler);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Attach regex toggle handler
|
|
425
|
+
*/
|
|
426
|
+
attachRegexHandler(regexBtn) {
|
|
427
|
+
const handler = () => {
|
|
428
|
+
this.regexMode = !this.regexMode;
|
|
429
|
+
regexBtn.classList.toggle('active', this.regexMode);
|
|
430
|
+
regexBtn.setAttribute('aria-pressed', String(this.regexMode));
|
|
431
|
+
this.performSearch();
|
|
432
|
+
};
|
|
433
|
+
regexBtn.addEventListener('click', handler);
|
|
434
|
+
this.eventHandlers.set('regex', handler);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Attach input handler with debounce
|
|
439
|
+
*/
|
|
440
|
+
attachInputHandler(input) {
|
|
441
|
+
const handler = () => {
|
|
442
|
+
this.cleanupDebounce();
|
|
443
|
+
this.debounceTimer = setTimeout(() => {
|
|
444
|
+
this.performSearch();
|
|
445
|
+
}, DEBOUNCE_DELAY);
|
|
446
|
+
};
|
|
447
|
+
input.addEventListener('input', handler);
|
|
448
|
+
this.eventHandlers.set('input', handler);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Update button states to reflect current settings
|
|
453
|
+
*/
|
|
454
|
+
updateButtonStates(caseBtn, regexBtn) {
|
|
455
|
+
if (this.caseSensitive) {
|
|
456
|
+
caseBtn.classList.add('active');
|
|
457
|
+
caseBtn.setAttribute('aria-pressed', 'true');
|
|
458
|
+
}
|
|
459
|
+
if (this.regexMode) {
|
|
460
|
+
regexBtn.classList.add('active');
|
|
461
|
+
regexBtn.setAttribute('aria-pressed', 'true');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Update match count display
|
|
467
|
+
*/
|
|
468
|
+
updateMatchCount(total, error = null, current = null) {
|
|
469
|
+
const countEl = this.searchOverlay?.querySelector('.inline-search-count');
|
|
470
|
+
if (!countEl) return;
|
|
471
|
+
|
|
472
|
+
if (error) {
|
|
473
|
+
countEl.textContent = error;
|
|
474
|
+
countEl.classList.add('error');
|
|
475
|
+
} else if (total === 0) {
|
|
476
|
+
countEl.textContent = 'No matches';
|
|
477
|
+
countEl.classList.remove('error');
|
|
478
|
+
} else if (current !== null) {
|
|
479
|
+
countEl.textContent = `${current} of ${total}`;
|
|
480
|
+
countEl.classList.remove('error');
|
|
481
|
+
} else {
|
|
482
|
+
countEl.textContent = `${total} match${total !== 1 ? 'es' : ''}`;
|
|
483
|
+
countEl.classList.remove('error');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ========================================================================
|
|
488
|
+
// Event Handling
|
|
489
|
+
// ========================================================================
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Setup keyboard shortcuts
|
|
493
|
+
*/
|
|
494
|
+
setupKeyboardShortcuts() {
|
|
495
|
+
const keyHandler = (e) => this.handleKeydown(e);
|
|
496
|
+
document.addEventListener('keydown', keyHandler);
|
|
497
|
+
this.eventHandlers.set('keydown', keyHandler);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Handle keyboard events
|
|
502
|
+
*/
|
|
503
|
+
handleKeydown(e) {
|
|
504
|
+
// Allow Cmd/Ctrl+F in inputs
|
|
505
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
506
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
this.show();
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Cmd/Ctrl+F: Show search
|
|
514
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
|
515
|
+
e.preventDefault();
|
|
516
|
+
this.show();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Only handle other shortcuts when search is active
|
|
521
|
+
if (!this.isActive) return;
|
|
522
|
+
|
|
523
|
+
// Escape: Close search
|
|
524
|
+
if (e.key === 'Escape') {
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
this.hide();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Cmd/Ctrl+G: Next match
|
|
531
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'g' && !e.shiftKey) {
|
|
532
|
+
e.preventDefault();
|
|
533
|
+
this.nextMatch();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Cmd/Ctrl+Shift+G: Previous match
|
|
538
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'G') {
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
this.previousMatch();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Enter: Next match
|
|
545
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
546
|
+
e.preventDefault();
|
|
547
|
+
this.nextMatch();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Shift+Enter: Previous match
|
|
552
|
+
if (e.key === 'Enter' && e.shiftKey) {
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
this.previousMatch();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Remove all event listeners
|
|
561
|
+
*/
|
|
562
|
+
removeEventListeners() {
|
|
563
|
+
this.eventHandlers.forEach((handler, event) => {
|
|
564
|
+
if (event === 'keydown') {
|
|
565
|
+
document.removeEventListener(event, handler);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
this.eventHandlers.clear();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ========================================================================
|
|
572
|
+
// State Management
|
|
573
|
+
// ========================================================================
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Store original line content for restoration
|
|
577
|
+
*/
|
|
578
|
+
storeOriginalContent() {
|
|
579
|
+
const codeBlock = this.findCodeBlock();
|
|
580
|
+
if (!codeBlock) return;
|
|
581
|
+
|
|
582
|
+
const lines = codeBlock.querySelectorAll('.line-container');
|
|
583
|
+
lines.forEach((line, index) => {
|
|
584
|
+
const lineContent = line.querySelector('.line-content');
|
|
585
|
+
if (lineContent) {
|
|
586
|
+
this.originalContent.set(index + 1, lineContent.innerHTML);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Reset search state
|
|
593
|
+
*/
|
|
594
|
+
resetState() {
|
|
595
|
+
this.isActive = false;
|
|
596
|
+
this.matches = [];
|
|
597
|
+
this.currentMatchIndex = -1;
|
|
598
|
+
this.searchInput = null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Reset matches array
|
|
603
|
+
*/
|
|
604
|
+
resetMatches() {
|
|
605
|
+
this.matches = [];
|
|
606
|
+
this.currentMatchIndex = -1;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ========================================================================
|
|
610
|
+
// Utilities
|
|
611
|
+
// ========================================================================
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Check if current page is a file view page
|
|
615
|
+
*/
|
|
616
|
+
isFileViewPage() {
|
|
617
|
+
const fileContent = document.querySelector('.file-content');
|
|
618
|
+
return fileContent?.querySelector(CODE_BLOCK_SELECTOR) !== null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Find the code block element
|
|
623
|
+
*/
|
|
624
|
+
findCodeBlock() {
|
|
625
|
+
return document.querySelector(CODE_BLOCK_SELECTOR);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Focus search input
|
|
630
|
+
*/
|
|
631
|
+
focusSearchInput() {
|
|
632
|
+
if (!this.searchInput) return;
|
|
633
|
+
|
|
634
|
+
requestAnimationFrame(() => {
|
|
635
|
+
this.searchInput.focus();
|
|
636
|
+
this.searchInput.select();
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Remove search overlay from DOM
|
|
642
|
+
*/
|
|
643
|
+
removeSearchOverlay() {
|
|
644
|
+
if (this.searchOverlay) {
|
|
645
|
+
this.searchOverlay.remove();
|
|
646
|
+
this.searchOverlay = null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Cleanup debounce timer
|
|
652
|
+
*/
|
|
653
|
+
cleanupDebounce() {
|
|
654
|
+
if (this.debounceTimer) {
|
|
655
|
+
clearTimeout(this.debounceTimer);
|
|
656
|
+
this.debounceTimer = null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Modal management utilities
|
|
3
|
+
* @module modal-manager
|
|
3
4
|
*/
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Creates a modal overlay with content
|
|
8
|
+
* @param {string} content - HTML content for the modal
|
|
9
|
+
* @param {string} [className=''] - Additional CSS classes
|
|
10
|
+
* @returns {{element: HTMLElement, close: Function}} Modal control object
|
|
11
|
+
*/
|
|
5
12
|
export function createModal(content, className = '') {
|
|
6
13
|
const modal = document.createElement('div');
|
|
7
14
|
modal.className = `modal-overlay ${className}`;
|
|
@@ -15,34 +22,13 @@ export function createModal(content, className = '') {
|
|
|
15
22
|
};
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<p>You have unsaved changes for this file. What would you like to do?</p>
|
|
26
|
-
<div class="draft-actions">
|
|
27
|
-
<button class="btn btn-primary" data-action="load">Load Draft</button>
|
|
28
|
-
<button class="btn btn-secondary" data-action="discard">Discard Draft</button>
|
|
29
|
-
<button class="btn btn-secondary" data-action="cancel">Cancel</button>
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
32
|
-
`;
|
|
33
|
-
|
|
34
|
-
modal.addEventListener('click', e => {
|
|
35
|
-
if (e.target.matches('[data-action]') || e.target === modal) {
|
|
36
|
-
const action = e.target.dataset?.action || 'cancel';
|
|
37
|
-
modal.remove();
|
|
38
|
-
resolve(action);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
document.body.appendChild(modal);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Shows a confirmation dialog
|
|
27
|
+
* @param {string} message - Message to display
|
|
28
|
+
* @param {string} [confirmText='Confirm'] - Confirm button text
|
|
29
|
+
* @param {string} [cancelText='Cancel'] - Cancel button text
|
|
30
|
+
* @returns {Promise<boolean>} Resolves to true if confirmed
|
|
31
|
+
*/
|
|
46
32
|
export function showConfirmDialog(message, confirmText = 'Confirm', cancelText = 'Cancel') {
|
|
47
33
|
return new Promise(resolve => {
|
|
48
34
|
const modal = document.createElement('div');
|