gh-here 3.0.3 → 3.1.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/.playwright-mcp/fixed-alignment.png +0 -0
- package/.playwright-mcp/fixed-layout.png +0 -0
- package/.playwright-mcp/gh-here-home-header-table.png +0 -0
- package/.playwright-mcp/gh-here-home.png +0 -0
- package/.playwright-mcp/line-selection-multiline.png +0 -0
- package/.playwright-mcp/line-selection-test-after.png +0 -0
- package/.playwright-mcp/line-selection-test-before.png +0 -0
- package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
- package/lib/constants.js +25 -15
- 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 +10 -54
- package/lib/gitignore.js +70 -41
- package/lib/renderers.js +15 -19
- package/lib/server.js +70 -193
- package/lib/symbol-parser.js +600 -0
- package/package.json +1 -1
- package/public/app.js +134 -68
- 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/public/styles.css +2049 -296
- 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/js/draft-manager.js +0 -36
- package/public/js/editor-manager.js +0 -159
- 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,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-text content search handler
|
|
3
|
+
* Search across entire codebase
|
|
4
|
+
*
|
|
5
|
+
* @class ContentSearchHandler
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { escapeHtml } from './utils.js';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Constants (alpha-sorted)
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
const API_ENDPOINT = '/api/search-content';
|
|
15
|
+
const DEBOUNCE_DELAY = 300;
|
|
16
|
+
const MAX_RESULTS = 100;
|
|
17
|
+
const SEARCH_OVERLAY_CLASS = 'content-search-overlay';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// ContentSearchHandler Class
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export class ContentSearchHandler {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.isActive = false;
|
|
26
|
+
this.searchOverlay = null;
|
|
27
|
+
this.searchInput = null;
|
|
28
|
+
this.results = [];
|
|
29
|
+
this.currentQuery = '';
|
|
30
|
+
this.regexMode = false;
|
|
31
|
+
this.caseSensitive = false;
|
|
32
|
+
this.debounceTimer = null;
|
|
33
|
+
this.abortController = null;
|
|
34
|
+
this.eventHandlers = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ========================================================================
|
|
38
|
+
// Public API
|
|
39
|
+
// ========================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize content search
|
|
43
|
+
*/
|
|
44
|
+
init() {
|
|
45
|
+
this.setupKeyboardShortcuts();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Show search overlay
|
|
50
|
+
*/
|
|
51
|
+
show() {
|
|
52
|
+
if (this.isActive) {
|
|
53
|
+
this.focusSearchInput();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.isActive = true;
|
|
58
|
+
this.createSearchOverlay();
|
|
59
|
+
document.body.appendChild(this.searchOverlay);
|
|
60
|
+
this.focusSearchInput();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Hide search overlay
|
|
65
|
+
*/
|
|
66
|
+
hide() {
|
|
67
|
+
if (!this.isActive) return;
|
|
68
|
+
|
|
69
|
+
this.cancelSearch();
|
|
70
|
+
this.cleanupDebounce();
|
|
71
|
+
this.removeSearchOverlay();
|
|
72
|
+
this.resetState();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Cleanup: Remove event listeners and cancel requests
|
|
77
|
+
*/
|
|
78
|
+
destroy() {
|
|
79
|
+
this.hide();
|
|
80
|
+
this.removeEventListeners();
|
|
81
|
+
this.results = [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ========================================================================
|
|
85
|
+
// Search Operations
|
|
86
|
+
// ========================================================================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Perform search with debouncing and cancellation
|
|
90
|
+
*/
|
|
91
|
+
async performSearch() {
|
|
92
|
+
if (!this.searchInput) return;
|
|
93
|
+
|
|
94
|
+
const query = this.searchInput.value.trim();
|
|
95
|
+
this.currentQuery = query;
|
|
96
|
+
|
|
97
|
+
const statusEl = this.getStatusElement();
|
|
98
|
+
const resultsContainer = this.getResultsContainer();
|
|
99
|
+
|
|
100
|
+
if (!query) {
|
|
101
|
+
this.clearResults(statusEl, resultsContainer);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.cancelSearch();
|
|
106
|
+
this.abortController = new AbortController();
|
|
107
|
+
this.updateStatus(statusEl, 'Searching...');
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const data = await this.fetchSearchResults(query);
|
|
111
|
+
|
|
112
|
+
// Check if query changed during async operation
|
|
113
|
+
if (this.currentQuery !== query) return;
|
|
114
|
+
|
|
115
|
+
this.results = data.results || [];
|
|
116
|
+
this.displayResults(data);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.name === 'AbortError') return;
|
|
119
|
+
this.handleSearchError(error, statusEl, resultsContainer);
|
|
120
|
+
} finally {
|
|
121
|
+
this.abortController = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fetch search results from API
|
|
127
|
+
*/
|
|
128
|
+
async fetchSearchResults(query) {
|
|
129
|
+
const params = new URLSearchParams({
|
|
130
|
+
q: query,
|
|
131
|
+
regex: String(this.regexMode),
|
|
132
|
+
caseSensitive: String(this.caseSensitive),
|
|
133
|
+
maxResults: String(MAX_RESULTS)
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const response = await fetch(`${API_ENDPOINT}?${params.toString()}`, {
|
|
137
|
+
signal: this.abortController.signal
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
|
|
146
|
+
if (!data.success) {
|
|
147
|
+
throw new Error(data.error || 'Search failed');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return data;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Display search results
|
|
155
|
+
*/
|
|
156
|
+
displayResults(data) {
|
|
157
|
+
const statusEl = this.getStatusElement();
|
|
158
|
+
const resultsContainer = this.getResultsContainer();
|
|
159
|
+
|
|
160
|
+
if (!statusEl || !resultsContainer) return;
|
|
161
|
+
|
|
162
|
+
statusEl.classList.remove('error');
|
|
163
|
+
|
|
164
|
+
if (data.total === 0) {
|
|
165
|
+
statusEl.textContent = 'No results found';
|
|
166
|
+
resultsContainer.innerHTML = '';
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.updateStatusWithCounts(statusEl, data);
|
|
171
|
+
this.renderResults(resultsContainer, data.results);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Render search results HTML
|
|
176
|
+
*/
|
|
177
|
+
renderResults(container, results) {
|
|
178
|
+
const resultsHtml = results.map(result => this.renderResultItem(result)).join('');
|
|
179
|
+
container.innerHTML = resultsHtml;
|
|
180
|
+
this.attachResultClickHandlers(container);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Render a single result item
|
|
185
|
+
*/
|
|
186
|
+
renderResultItem(result) {
|
|
187
|
+
const matchesHtml = result.matches.slice(0, 5).map(match =>
|
|
188
|
+
this.renderMatchLine(match)
|
|
189
|
+
).join('');
|
|
190
|
+
|
|
191
|
+
const moreMatches = result.matchCount > 5
|
|
192
|
+
? `<div class="content-search-more">+${result.matchCount - 5} more matches</div>`
|
|
193
|
+
: '';
|
|
194
|
+
|
|
195
|
+
const escapedPath = escapeHtml(result.path);
|
|
196
|
+
|
|
197
|
+
return `
|
|
198
|
+
<div class="content-search-result" data-path="${escapedPath}">
|
|
199
|
+
<div class="content-search-result-header">
|
|
200
|
+
<span class="result-path">${escapedPath}</span>
|
|
201
|
+
<span class="result-count">${result.matchCount} match${result.matchCount !== 1 ? 'es' : ''}</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="content-search-matches">
|
|
204
|
+
${matchesHtml}
|
|
205
|
+
${moreMatches}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Render a single match line
|
|
213
|
+
*/
|
|
214
|
+
renderMatchLine(match) {
|
|
215
|
+
const highlighted = this.highlightMatch(match.text, match.match);
|
|
216
|
+
return `
|
|
217
|
+
<div class="content-search-match">
|
|
218
|
+
<span class="match-line-number">${match.line}</span>
|
|
219
|
+
<span class="match-content">${highlighted}</span>
|
|
220
|
+
</div>
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Attach click handlers to result items
|
|
226
|
+
*/
|
|
227
|
+
attachResultClickHandlers(container) {
|
|
228
|
+
container.querySelectorAll('.content-search-result').forEach(resultEl => {
|
|
229
|
+
const clickHandler = () => {
|
|
230
|
+
const path = resultEl.getAttribute('data-path');
|
|
231
|
+
if (path) {
|
|
232
|
+
window.location.href = `/?path=${encodeURIComponent(path)}`;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
resultEl.addEventListener('click', clickHandler);
|
|
236
|
+
resultEl._clickHandler = clickHandler;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Highlight match in text
|
|
242
|
+
*/
|
|
243
|
+
highlightMatch(text, match) {
|
|
244
|
+
if (!match || !text) return escapeHtml(text);
|
|
245
|
+
|
|
246
|
+
const escapedText = escapeHtml(text);
|
|
247
|
+
const escapedMatch = escapeHtml(match);
|
|
248
|
+
const escapedPattern = escapedMatch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
249
|
+
const regex = new RegExp(escapedPattern, 'gi');
|
|
250
|
+
|
|
251
|
+
return escapedText.replace(regex, `<mark class="content-search-highlight">$&</mark>`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ========================================================================
|
|
255
|
+
// UI Management
|
|
256
|
+
// ========================================================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Create search overlay UI
|
|
260
|
+
*/
|
|
261
|
+
createSearchOverlay() {
|
|
262
|
+
this.searchOverlay = document.createElement('div');
|
|
263
|
+
this.searchOverlay.className = SEARCH_OVERLAY_CLASS;
|
|
264
|
+
this.searchOverlay.setAttribute('role', 'dialog');
|
|
265
|
+
this.searchOverlay.setAttribute('aria-label', 'Search in files');
|
|
266
|
+
this.searchOverlay.innerHTML = this.getSearchOverlayHTML();
|
|
267
|
+
|
|
268
|
+
this.attachSearchOverlayHandlers();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get search overlay HTML template
|
|
273
|
+
*/
|
|
274
|
+
getSearchOverlayHTML() {
|
|
275
|
+
return `
|
|
276
|
+
<div class="content-search-container">
|
|
277
|
+
<div class="content-search-header">
|
|
278
|
+
<h3>Search in files</h3>
|
|
279
|
+
<button class="content-search-close" aria-label="Close search">×</button>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="content-search-input-wrapper">
|
|
282
|
+
<input
|
|
283
|
+
type="text"
|
|
284
|
+
class="content-search-input"
|
|
285
|
+
placeholder="Search for text in files..."
|
|
286
|
+
autocomplete="off"
|
|
287
|
+
aria-label="Search query"
|
|
288
|
+
/>
|
|
289
|
+
<div class="content-search-options">
|
|
290
|
+
<button class="content-search-option" data-option="case" title="Case sensitive" aria-pressed="false">
|
|
291
|
+
<span class="option-label">Aa</span>
|
|
292
|
+
</button>
|
|
293
|
+
<button class="content-search-option" data-option="regex" title="Use regular expression" aria-pressed="false">
|
|
294
|
+
<span class="option-label">.*</span>
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="content-search-info">
|
|
299
|
+
<span class="content-search-status">Ready to search</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="content-search-results"></div>
|
|
302
|
+
</div>
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Attach event handlers to search overlay elements
|
|
308
|
+
*/
|
|
309
|
+
attachSearchOverlayHandlers() {
|
|
310
|
+
this.searchInput = this.searchOverlay.querySelector('.content-search-input');
|
|
311
|
+
const closeBtn = this.searchOverlay.querySelector('.content-search-close');
|
|
312
|
+
const caseBtn = this.searchOverlay.querySelector('[data-option="case"]');
|
|
313
|
+
const regexBtn = this.searchOverlay.querySelector('[data-option="regex"]');
|
|
314
|
+
|
|
315
|
+
this.attachCloseHandler(closeBtn);
|
|
316
|
+
this.attachCaseHandler(caseBtn);
|
|
317
|
+
this.attachRegexHandler(regexBtn);
|
|
318
|
+
this.attachInputHandler(this.searchInput);
|
|
319
|
+
this.updateButtonStates(caseBtn, regexBtn);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Attach close button handler
|
|
324
|
+
*/
|
|
325
|
+
attachCloseHandler(closeBtn) {
|
|
326
|
+
const handler = () => this.hide();
|
|
327
|
+
closeBtn.addEventListener('click', handler);
|
|
328
|
+
this.eventHandlers.set('close', handler);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Attach case sensitive toggle handler
|
|
333
|
+
*/
|
|
334
|
+
attachCaseHandler(caseBtn) {
|
|
335
|
+
const handler = () => {
|
|
336
|
+
this.caseSensitive = !this.caseSensitive;
|
|
337
|
+
caseBtn.classList.toggle('active', this.caseSensitive);
|
|
338
|
+
caseBtn.setAttribute('aria-pressed', String(this.caseSensitive));
|
|
339
|
+
this.performSearch();
|
|
340
|
+
};
|
|
341
|
+
caseBtn.addEventListener('click', handler);
|
|
342
|
+
this.eventHandlers.set('case', handler);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Attach regex toggle handler
|
|
347
|
+
*/
|
|
348
|
+
attachRegexHandler(regexBtn) {
|
|
349
|
+
const handler = () => {
|
|
350
|
+
this.regexMode = !this.regexMode;
|
|
351
|
+
regexBtn.classList.toggle('active', this.regexMode);
|
|
352
|
+
regexBtn.setAttribute('aria-pressed', String(this.regexMode));
|
|
353
|
+
this.performSearch();
|
|
354
|
+
};
|
|
355
|
+
regexBtn.addEventListener('click', handler);
|
|
356
|
+
this.eventHandlers.set('regex', handler);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Attach input handler with debounce
|
|
361
|
+
*/
|
|
362
|
+
attachInputHandler(input) {
|
|
363
|
+
const handler = () => {
|
|
364
|
+
this.cleanupDebounce();
|
|
365
|
+
this.debounceTimer = setTimeout(() => {
|
|
366
|
+
this.performSearch();
|
|
367
|
+
}, DEBOUNCE_DELAY);
|
|
368
|
+
};
|
|
369
|
+
input.addEventListener('input', handler);
|
|
370
|
+
this.eventHandlers.set('input', handler);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Update button states to reflect current settings
|
|
375
|
+
*/
|
|
376
|
+
updateButtonStates(caseBtn, regexBtn) {
|
|
377
|
+
if (this.caseSensitive) {
|
|
378
|
+
caseBtn.classList.add('active');
|
|
379
|
+
caseBtn.setAttribute('aria-pressed', 'true');
|
|
380
|
+
}
|
|
381
|
+
if (this.regexMode) {
|
|
382
|
+
regexBtn.classList.add('active');
|
|
383
|
+
regexBtn.setAttribute('aria-pressed', 'true');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ========================================================================
|
|
388
|
+
// Event Handling
|
|
389
|
+
// ========================================================================
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Setup keyboard shortcuts
|
|
393
|
+
*/
|
|
394
|
+
setupKeyboardShortcuts() {
|
|
395
|
+
const keyHandler = (e) => this.handleKeydown(e);
|
|
396
|
+
document.addEventListener('keydown', keyHandler);
|
|
397
|
+
this.eventHandlers.set('keydown', keyHandler);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Handle keyboard events
|
|
402
|
+
*/
|
|
403
|
+
handleKeydown(e) {
|
|
404
|
+
// Don't interfere if user is typing in an input
|
|
405
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Cmd/Ctrl+Shift+F: Show content search
|
|
410
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'F') {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
this.show();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Only handle other shortcuts when search is active
|
|
417
|
+
if (!this.isActive) return;
|
|
418
|
+
|
|
419
|
+
// Escape: Close search
|
|
420
|
+
if (e.key === 'Escape') {
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
this.hide();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Remove all event listeners
|
|
429
|
+
*/
|
|
430
|
+
removeEventListeners() {
|
|
431
|
+
this.eventHandlers.forEach((handler, event) => {
|
|
432
|
+
if (event === 'keydown') {
|
|
433
|
+
document.removeEventListener(event, handler);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
this.eventHandlers.clear();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ========================================================================
|
|
440
|
+
// State Management
|
|
441
|
+
// ========================================================================
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Reset search state
|
|
445
|
+
*/
|
|
446
|
+
resetState() {
|
|
447
|
+
this.isActive = false;
|
|
448
|
+
this.results = [];
|
|
449
|
+
this.currentQuery = '';
|
|
450
|
+
this.searchInput = null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Clear search results
|
|
455
|
+
*/
|
|
456
|
+
clearResults(statusEl, resultsContainer) {
|
|
457
|
+
if (statusEl) statusEl.textContent = 'Ready to search';
|
|
458
|
+
if (resultsContainer) resultsContainer.innerHTML = '';
|
|
459
|
+
this.results = [];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Handle search errors
|
|
464
|
+
*/
|
|
465
|
+
handleSearchError(error, statusEl, resultsContainer) {
|
|
466
|
+
if (statusEl) {
|
|
467
|
+
statusEl.textContent = `Error: ${error.message}`;
|
|
468
|
+
statusEl.classList.add('error');
|
|
469
|
+
}
|
|
470
|
+
if (resultsContainer) resultsContainer.innerHTML = '';
|
|
471
|
+
this.results = [];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ========================================================================
|
|
475
|
+
// Utilities
|
|
476
|
+
// ========================================================================
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get status element from overlay
|
|
480
|
+
*/
|
|
481
|
+
getStatusElement() {
|
|
482
|
+
return this.searchOverlay?.querySelector('.content-search-status');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get results container from overlay
|
|
487
|
+
*/
|
|
488
|
+
getResultsContainer() {
|
|
489
|
+
return this.searchOverlay?.querySelector('.content-search-results');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Update status text
|
|
494
|
+
*/
|
|
495
|
+
updateStatus(statusEl, text) {
|
|
496
|
+
if (!statusEl) return;
|
|
497
|
+
statusEl.textContent = text;
|
|
498
|
+
statusEl.classList.remove('error');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Update status with result counts
|
|
503
|
+
*/
|
|
504
|
+
updateStatusWithCounts(statusEl, data) {
|
|
505
|
+
const fileCount = data.results.length;
|
|
506
|
+
const resultCount = data.total;
|
|
507
|
+
statusEl.textContent = `Found ${resultCount} result${resultCount !== 1 ? 's' : ''} in ${fileCount} file${fileCount !== 1 ? 's' : ''}`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Focus search input
|
|
512
|
+
*/
|
|
513
|
+
focusSearchInput() {
|
|
514
|
+
if (!this.searchInput) return;
|
|
515
|
+
|
|
516
|
+
requestAnimationFrame(() => {
|
|
517
|
+
this.searchInput.focus();
|
|
518
|
+
this.searchInput.select();
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Remove search overlay from DOM
|
|
524
|
+
*/
|
|
525
|
+
removeSearchOverlay() {
|
|
526
|
+
if (this.searchOverlay) {
|
|
527
|
+
this.searchOverlay.remove();
|
|
528
|
+
this.searchOverlay = null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Cancel in-flight search request
|
|
534
|
+
*/
|
|
535
|
+
cancelSearch() {
|
|
536
|
+
if (this.abortController) {
|
|
537
|
+
this.abortController.abort();
|
|
538
|
+
this.abortController = null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Cleanup debounce timer
|
|
544
|
+
*/
|
|
545
|
+
cleanupDebounce() {
|
|
546
|
+
if (this.debounceTimer) {
|
|
547
|
+
clearTimeout(this.debounceTimer);
|
|
548
|
+
this.debounceTimer = null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|