gh-here 3.0.2 → 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 +207 -73
- 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/navigation.js +5 -0
- 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
package/public/js/navigation.js
CHANGED
|
@@ -35,6 +35,11 @@ export class NavigationHandler {
|
|
|
35
35
|
const link = e.target.closest('a');
|
|
36
36
|
if (!link) return;
|
|
37
37
|
|
|
38
|
+
// Skip line number links (hash-only navigation like #L10)
|
|
39
|
+
if (link.classList.contains('line-number')) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
// Skip if clicking inside quick actions or other interactive elements
|
|
39
44
|
if (e.target.closest('.quick-actions, button, .file-action-btn')) {
|
|
40
45
|
return;
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol Outline - Code structure navigation panel
|
|
3
|
+
* Shows functions, classes, and other symbols in the current file
|
|
4
|
+
* @module symbol-outline
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { escapeHtml, PathUtils } from './utils.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Constants (alpha-sorted by key)
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
const SYMBOL_ICONS = {
|
|
14
|
+
class: 'C',
|
|
15
|
+
constant: 'K',
|
|
16
|
+
export: 'E',
|
|
17
|
+
function: 'f',
|
|
18
|
+
import: 'i',
|
|
19
|
+
interface: 'I',
|
|
20
|
+
keyframes: '@',
|
|
21
|
+
media: '@',
|
|
22
|
+
method: 'm',
|
|
23
|
+
mixin: '@',
|
|
24
|
+
selector: '#',
|
|
25
|
+
type: 'T',
|
|
26
|
+
variable: 'v'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const SYMBOL_LABELS = {
|
|
30
|
+
class: 'Classes',
|
|
31
|
+
constant: 'Constants',
|
|
32
|
+
function: 'Functions',
|
|
33
|
+
interface: 'Interfaces',
|
|
34
|
+
keyframes: 'Keyframes',
|
|
35
|
+
media: 'Media Queries',
|
|
36
|
+
method: 'Methods',
|
|
37
|
+
mixin: 'Mixins',
|
|
38
|
+
selector: 'Selectors',
|
|
39
|
+
type: 'Types',
|
|
40
|
+
variable: 'Variables'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export class SymbolOutline {
|
|
44
|
+
constructor() {
|
|
45
|
+
this.panel = null;
|
|
46
|
+
this.button = null;
|
|
47
|
+
this.isOpen = false;
|
|
48
|
+
this.symbols = [];
|
|
49
|
+
this.grouped = {};
|
|
50
|
+
this.currentPath = null;
|
|
51
|
+
this.selectedIndex = -1;
|
|
52
|
+
this.abortController = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialize the symbol outline
|
|
57
|
+
*/
|
|
58
|
+
init() {
|
|
59
|
+
this.currentPath = PathUtils.getCurrentPath();
|
|
60
|
+
|
|
61
|
+
// Only initialize on file view pages (not directories)
|
|
62
|
+
if (!this.isFileViewPage()) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.createButton();
|
|
67
|
+
this.createPanel();
|
|
68
|
+
this.setupKeyboardShortcuts();
|
|
69
|
+
this.loadSymbols();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if we're on a file view page
|
|
74
|
+
*/
|
|
75
|
+
isFileViewPage() {
|
|
76
|
+
const fileContent = document.querySelector('.file-content');
|
|
77
|
+
const codeBlock = document.querySelector('.file-content pre code');
|
|
78
|
+
return fileContent && codeBlock;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create the toggle button in the file header
|
|
83
|
+
*/
|
|
84
|
+
createButton() {
|
|
85
|
+
const fileHeaderActions = document.querySelector('.file-header-actions');
|
|
86
|
+
if (!fileHeaderActions) return;
|
|
87
|
+
|
|
88
|
+
// Check if button already exists
|
|
89
|
+
if (document.querySelector('.symbol-outline-btn')) return;
|
|
90
|
+
|
|
91
|
+
this.button = document.createElement('button');
|
|
92
|
+
this.button.className = 'symbol-outline-btn file-action-btn';
|
|
93
|
+
this.button.title = 'Symbol outline (Cmd+Shift+O)';
|
|
94
|
+
this.button.innerHTML = `
|
|
95
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
96
|
+
<path d="M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 7.75zm0 5a.75.75 0 0 1 .75-.75h12.5a.75.75 0 0 1 0 1.5H1.75a.75.75 0 0 1-.75-.75z"/>
|
|
97
|
+
</svg>
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
this.button.addEventListener('click', () => this.toggle());
|
|
101
|
+
|
|
102
|
+
// Insert at the beginning of actions
|
|
103
|
+
fileHeaderActions.insertBefore(this.button, fileHeaderActions.firstChild);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create the dropdown panel
|
|
108
|
+
*/
|
|
109
|
+
createPanel() {
|
|
110
|
+
// Remove existing panel if any
|
|
111
|
+
const existing = document.querySelector('.symbol-outline-panel');
|
|
112
|
+
if (existing) existing.remove();
|
|
113
|
+
|
|
114
|
+
this.panel = document.createElement('div');
|
|
115
|
+
this.panel.className = 'symbol-outline-panel';
|
|
116
|
+
this.panel.innerHTML = `
|
|
117
|
+
<div class="symbol-outline-header">
|
|
118
|
+
<span class="symbol-outline-title">Outline</span>
|
|
119
|
+
<span class="symbol-outline-count"></span>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="symbol-outline-search">
|
|
122
|
+
<input type="text" placeholder="Filter symbols..." class="symbol-search-input">
|
|
123
|
+
</div>
|
|
124
|
+
<div class="symbol-outline-content">
|
|
125
|
+
<div class="symbol-outline-loading">Loading symbols...</div>
|
|
126
|
+
</div>
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
// Append to body to avoid z-index issues with Monaco editor
|
|
130
|
+
document.body.appendChild(this.panel);
|
|
131
|
+
|
|
132
|
+
// Setup search filtering
|
|
133
|
+
const searchInput = this.panel.querySelector('.symbol-search-input');
|
|
134
|
+
searchInput.addEventListener('input', (e) => this.filterSymbols(e.target.value));
|
|
135
|
+
searchInput.addEventListener('keydown', (e) => this.handleSearchKeydown(e));
|
|
136
|
+
|
|
137
|
+
// Close when clicking outside
|
|
138
|
+
document.addEventListener('click', (e) => {
|
|
139
|
+
if (this.isOpen && !this.panel.contains(e.target) && !this.button.contains(e.target)) {
|
|
140
|
+
this.close();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Setup keyboard shortcuts
|
|
147
|
+
*/
|
|
148
|
+
setupKeyboardShortcuts() {
|
|
149
|
+
document.addEventListener('keydown', (e) => {
|
|
150
|
+
// Cmd+Shift+O to toggle outline
|
|
151
|
+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'o') {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
this.toggle();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Escape to close
|
|
157
|
+
if (e.key === 'Escape' && this.isOpen) {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
this.close();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Toggle panel open/closed
|
|
166
|
+
*/
|
|
167
|
+
toggle() {
|
|
168
|
+
if (this.isOpen) {
|
|
169
|
+
this.close();
|
|
170
|
+
} else {
|
|
171
|
+
this.open();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Open the panel
|
|
177
|
+
*/
|
|
178
|
+
open() {
|
|
179
|
+
if (!this.panel) return;
|
|
180
|
+
|
|
181
|
+
this.isOpen = true;
|
|
182
|
+
this.panel.classList.add('open');
|
|
183
|
+
this.button?.classList.add('active');
|
|
184
|
+
|
|
185
|
+
// Position panel relative to button
|
|
186
|
+
this.positionPanel();
|
|
187
|
+
|
|
188
|
+
// Focus search input
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
const searchInput = this.panel.querySelector('.symbol-search-input');
|
|
191
|
+
searchInput?.focus();
|
|
192
|
+
}, 50);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Position the panel relative to the button
|
|
197
|
+
*/
|
|
198
|
+
positionPanel() {
|
|
199
|
+
if (!this.button || !this.panel) return;
|
|
200
|
+
|
|
201
|
+
const buttonRect = this.button.getBoundingClientRect();
|
|
202
|
+
const panelWidth = 300;
|
|
203
|
+
|
|
204
|
+
// Position below the button, aligned to the right
|
|
205
|
+
let left = buttonRect.right - panelWidth;
|
|
206
|
+
let top = buttonRect.bottom + 8;
|
|
207
|
+
|
|
208
|
+
// Make sure it doesn't go off the left edge
|
|
209
|
+
if (left < 8) left = 8;
|
|
210
|
+
|
|
211
|
+
// Make sure it doesn't go off the right edge
|
|
212
|
+
if (left + panelWidth > window.innerWidth - 8) {
|
|
213
|
+
left = window.innerWidth - panelWidth - 8;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.panel.style.left = `${left}px`;
|
|
217
|
+
this.panel.style.top = `${top}px`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Close the panel
|
|
222
|
+
*/
|
|
223
|
+
close() {
|
|
224
|
+
if (!this.panel) return;
|
|
225
|
+
|
|
226
|
+
this.isOpen = false;
|
|
227
|
+
this.panel.classList.remove('open');
|
|
228
|
+
this.button?.classList.remove('active');
|
|
229
|
+
this.selectedIndex = -1;
|
|
230
|
+
|
|
231
|
+
// Clear search
|
|
232
|
+
const searchInput = this.panel.querySelector('.symbol-search-input');
|
|
233
|
+
if (searchInput) searchInput.value = '';
|
|
234
|
+
this.filterSymbols('');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Load symbols from API
|
|
239
|
+
*/
|
|
240
|
+
async loadSymbols() {
|
|
241
|
+
if (!this.currentPath) return;
|
|
242
|
+
|
|
243
|
+
// Cancel previous request
|
|
244
|
+
if (this.abortController) {
|
|
245
|
+
this.abortController.abort();
|
|
246
|
+
}
|
|
247
|
+
this.abortController = new AbortController();
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await fetch(`/api/symbols?path=${encodeURIComponent(this.currentPath)}`, {
|
|
251
|
+
signal: this.abortController.signal
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const data = await response.json();
|
|
255
|
+
|
|
256
|
+
if (data.success) {
|
|
257
|
+
this.symbols = data.symbols || [];
|
|
258
|
+
this.grouped = data.grouped || {};
|
|
259
|
+
this.renderSymbols();
|
|
260
|
+
this.updateCount();
|
|
261
|
+
} else {
|
|
262
|
+
this.showError('Could not parse symbols');
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error.name !== 'AbortError') {
|
|
266
|
+
this.showError('Failed to load symbols');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Render symbols in the panel
|
|
273
|
+
*/
|
|
274
|
+
renderSymbols() {
|
|
275
|
+
const content = this.panel.querySelector('.symbol-outline-content');
|
|
276
|
+
|
|
277
|
+
if (this.symbols.length === 0) {
|
|
278
|
+
content.innerHTML = '<div class="symbol-outline-empty">No symbols found</div>';
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let html = '';
|
|
283
|
+
|
|
284
|
+
// Render grouped symbols
|
|
285
|
+
const kindOrder = ['class', 'interface', 'type', 'function', 'method', 'constant', 'variable', 'selector', 'keyframes', 'media', 'mixin'];
|
|
286
|
+
|
|
287
|
+
for (const kind of kindOrder) {
|
|
288
|
+
const symbols = this.grouped[kind];
|
|
289
|
+
if (!symbols || symbols.length === 0) continue;
|
|
290
|
+
|
|
291
|
+
html += `
|
|
292
|
+
<div class="symbol-group" data-kind="${kind}">
|
|
293
|
+
<div class="symbol-group-header">
|
|
294
|
+
<span class="symbol-group-label">${SYMBOL_LABELS[kind] || kind}</span>
|
|
295
|
+
<span class="symbol-group-count">${symbols.length}</span>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="symbol-group-items">
|
|
298
|
+
${symbols.map((s, i) => this.renderSymbolItem(s, i)).join('')}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
content.innerHTML = html;
|
|
305
|
+
|
|
306
|
+
// Add click handlers
|
|
307
|
+
content.querySelectorAll('.symbol-item').forEach(item => {
|
|
308
|
+
item.addEventListener('click', () => {
|
|
309
|
+
const line = parseInt(item.dataset.line, 10);
|
|
310
|
+
this.navigateToLine(line);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Render a single symbol item
|
|
317
|
+
*/
|
|
318
|
+
renderSymbolItem(symbol, index) {
|
|
319
|
+
const icon = SYMBOL_ICONS[symbol.kind] || '?';
|
|
320
|
+
return `
|
|
321
|
+
<div class="symbol-item" data-line="${symbol.line}" data-index="${index}" data-name="${symbol.name.toLowerCase()}">
|
|
322
|
+
<span class="symbol-icon symbol-icon-${symbol.kind}">${icon}</span>
|
|
323
|
+
<span class="symbol-name">${escapeHtml(symbol.name)}</span>
|
|
324
|
+
<span class="symbol-line">:${symbol.line}</span>
|
|
325
|
+
</div>
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Update symbol count in header
|
|
331
|
+
*/
|
|
332
|
+
updateCount() {
|
|
333
|
+
const countEl = this.panel.querySelector('.symbol-outline-count');
|
|
334
|
+
if (countEl) {
|
|
335
|
+
countEl.textContent = this.symbols.length > 0 ? `(${this.symbols.length})` : '';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Update button to show count badge if symbols exist
|
|
339
|
+
if (this.button && this.symbols.length > 0) {
|
|
340
|
+
this.button.title = `Symbol outline (${this.symbols.length} symbols) - Cmd+Shift+O`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Filter symbols by search query
|
|
346
|
+
*/
|
|
347
|
+
filterSymbols(query) {
|
|
348
|
+
const items = this.panel.querySelectorAll('.symbol-item');
|
|
349
|
+
const groups = this.panel.querySelectorAll('.symbol-group');
|
|
350
|
+
const lowerQuery = query.toLowerCase();
|
|
351
|
+
|
|
352
|
+
items.forEach(item => {
|
|
353
|
+
const name = item.dataset.name || '';
|
|
354
|
+
const matches = !query || name.includes(lowerQuery);
|
|
355
|
+
item.style.display = matches ? '' : 'none';
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Hide empty groups
|
|
359
|
+
groups.forEach(group => {
|
|
360
|
+
const visibleItems = group.querySelectorAll('.symbol-item[style=""], .symbol-item:not([style])');
|
|
361
|
+
const hasVisible = Array.from(group.querySelectorAll('.symbol-item')).some(item => item.style.display !== 'none');
|
|
362
|
+
group.style.display = hasVisible ? '' : 'none';
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
this.selectedIndex = -1;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Handle keyboard navigation in search
|
|
370
|
+
*/
|
|
371
|
+
handleSearchKeydown(e) {
|
|
372
|
+
const visibleItems = Array.from(this.panel.querySelectorAll('.symbol-item'))
|
|
373
|
+
.filter(item => item.style.display !== 'none');
|
|
374
|
+
|
|
375
|
+
if (e.key === 'ArrowDown') {
|
|
376
|
+
e.preventDefault();
|
|
377
|
+
this.selectedIndex = Math.min(this.selectedIndex + 1, visibleItems.length - 1);
|
|
378
|
+
this.highlightSelected(visibleItems);
|
|
379
|
+
} else if (e.key === 'ArrowUp') {
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
382
|
+
this.highlightSelected(visibleItems);
|
|
383
|
+
} else if (e.key === 'Enter') {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
if (this.selectedIndex >= 0 && visibleItems[this.selectedIndex]) {
|
|
386
|
+
const line = parseInt(visibleItems[this.selectedIndex].dataset.line, 10);
|
|
387
|
+
this.navigateToLine(line);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Highlight selected item
|
|
394
|
+
*/
|
|
395
|
+
highlightSelected(items) {
|
|
396
|
+
items.forEach((item, i) => {
|
|
397
|
+
item.classList.toggle('selected', i === this.selectedIndex);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Scroll selected into view
|
|
401
|
+
if (this.selectedIndex >= 0 && items[this.selectedIndex]) {
|
|
402
|
+
items[this.selectedIndex].scrollIntoView({ block: 'nearest' });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Navigate to a specific line
|
|
408
|
+
*/
|
|
409
|
+
navigateToLine(line) {
|
|
410
|
+
// Close panel
|
|
411
|
+
this.close();
|
|
412
|
+
|
|
413
|
+
// Find the line element
|
|
414
|
+
const lineElement = document.querySelector(`#L${line}`) ||
|
|
415
|
+
document.querySelector(`[data-line="${line}"]`) ||
|
|
416
|
+
document.querySelector(`.line-container[data-line="${line}"]`);
|
|
417
|
+
|
|
418
|
+
if (lineElement) {
|
|
419
|
+
// Scroll to line
|
|
420
|
+
lineElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
421
|
+
|
|
422
|
+
// Highlight the line briefly
|
|
423
|
+
const container = lineElement.closest('.line-container') || lineElement.parentElement;
|
|
424
|
+
if (container) {
|
|
425
|
+
container.classList.add('line-highlight-flash');
|
|
426
|
+
setTimeout(() => {
|
|
427
|
+
container.classList.remove('line-highlight-flash');
|
|
428
|
+
}, 2000);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Update URL hash
|
|
432
|
+
window.history.replaceState(null, '', `#L${line}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Show error message
|
|
438
|
+
*/
|
|
439
|
+
showError(message) {
|
|
440
|
+
const content = this.panel.querySelector('.symbol-outline-content');
|
|
441
|
+
content.innerHTML = `<div class="symbol-outline-error">${escapeHtml(message)}</div>`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Cleanup
|
|
446
|
+
*/
|
|
447
|
+
destroy() {
|
|
448
|
+
if (this.abortController) {
|
|
449
|
+
this.abortController.abort();
|
|
450
|
+
}
|
|
451
|
+
this.panel?.remove();
|
|
452
|
+
this.button?.remove();
|
|
453
|
+
}
|
|
454
|
+
}
|