coderaph 0.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/README.md +126 -0
- package/dist/cli/analyzer.d.ts +13 -0
- package/dist/cli/analyzer.js +68 -0
- package/dist/cli/file-graph.d.ts +6 -0
- package/dist/cli/file-graph.js +32 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +69 -0
- package/dist/cli/server.d.ts +1 -0
- package/dist/cli/server.js +68 -0
- package/dist/cli/symbol-graph.d.ts +6 -0
- package/dist/cli/symbol-graph.js +112 -0
- package/dist/types/graph.d.ts +22 -0
- package/dist/types/graph.js +1 -0
- package/package.json +25 -0
- package/src/web/app.js +790 -0
- package/src/web/controls.js +732 -0
- package/src/web/graph.js +245 -0
- package/src/web/index.html +93 -0
- package/src/web/style.css +102 -0
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import {
|
|
2
|
+
switchMode, getGraphData, getCurrentEdges, getNodeMeshes,
|
|
3
|
+
focusNode, highlightNodes, clearHighlights, setFilteredView, clearFilter,
|
|
4
|
+
setTheme, setDimension, setGroups, setShowArrows, setNodeSize, setEdgeThickness, setNodeDistance, setLabelsVisible, setLabelFontSize,
|
|
5
|
+
setNodeVisibility, clearConnectedHighlight
|
|
6
|
+
} from './app.js';
|
|
7
|
+
|
|
8
|
+
// ── DOM References ──────────────────────────────────────────────────
|
|
9
|
+
const viewToggle = document.getElementById('view-toggle');
|
|
10
|
+
const infoPanel = document.getElementById('info-panel');
|
|
11
|
+
const infoName = document.getElementById('info-name');
|
|
12
|
+
const infoKind = document.getElementById('info-kind');
|
|
13
|
+
const infoFile = document.getElementById('info-file');
|
|
14
|
+
const infoClose = document.getElementById('info-close');
|
|
15
|
+
const canvas = document.getElementById('canvas');
|
|
16
|
+
const searchInput = document.getElementById('search-input');
|
|
17
|
+
const legend = document.getElementById('legend');
|
|
18
|
+
|
|
19
|
+
// Settings
|
|
20
|
+
const settingsBtn = document.getElementById('settings-btn');
|
|
21
|
+
const settingsPanel = document.getElementById('settings-panel');
|
|
22
|
+
const settingsClose = document.getElementById('settings-close');
|
|
23
|
+
const settingTheme = document.getElementById('setting-theme');
|
|
24
|
+
const settingNodeSize = document.getElementById('setting-node-size');
|
|
25
|
+
const settingEdgeThickness = document.getElementById('setting-edge-thickness');
|
|
26
|
+
const settingNodeDistance = document.getElementById('setting-node-distance');
|
|
27
|
+
const settingDimension = document.getElementById('setting-dimension');
|
|
28
|
+
const settingLabels = document.getElementById('setting-labels');
|
|
29
|
+
const settingArrows = document.getElementById('setting-arrows');
|
|
30
|
+
const valNodeSize = document.getElementById('val-node-size');
|
|
31
|
+
const valEdgeThickness = document.getElementById('val-edge-thickness');
|
|
32
|
+
const valNodeDistance = document.getElementById('val-node-distance');
|
|
33
|
+
const settingFontSize = document.getElementById('setting-font-size');
|
|
34
|
+
const valFontSize = document.getElementById('val-font-size');
|
|
35
|
+
|
|
36
|
+
// Filter
|
|
37
|
+
const filterKindCheckboxes = document.querySelectorAll('#filter-kinds input[type="checkbox"]');
|
|
38
|
+
const filterPath = document.getElementById('filter-path');
|
|
39
|
+
|
|
40
|
+
// ── Settings Persistence (localStorage) ─────────────────────────────
|
|
41
|
+
const STORAGE_KEY = 'coderaph-settings';
|
|
42
|
+
|
|
43
|
+
const defaults = {
|
|
44
|
+
theme: 'dark',
|
|
45
|
+
dimension: '3d',
|
|
46
|
+
groups: [],
|
|
47
|
+
nodeSize: 1.0,
|
|
48
|
+
edgeThickness: 1.0,
|
|
49
|
+
nodeDistance: 30,
|
|
50
|
+
fontSize: 10,
|
|
51
|
+
showLabels: true,
|
|
52
|
+
showArrows: false,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function loadSettings() {
|
|
56
|
+
try {
|
|
57
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
58
|
+
if (raw) return { ...defaults, ...JSON.parse(raw) };
|
|
59
|
+
} catch { /* ignore */ }
|
|
60
|
+
return { ...defaults };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function saveSettings() {
|
|
64
|
+
const settings = {
|
|
65
|
+
theme: settingTheme.value,
|
|
66
|
+
dimension: settingDimension.value,
|
|
67
|
+
groups: getGroupDefs(),
|
|
68
|
+
nodeSize: parseFloat(settingNodeSize.value),
|
|
69
|
+
edgeThickness: parseFloat(settingEdgeThickness.value),
|
|
70
|
+
nodeDistance: parseInt(settingNodeDistance.value, 10),
|
|
71
|
+
fontSize: parseInt(settingFontSize.value, 10),
|
|
72
|
+
showLabels: settingLabels.checked,
|
|
73
|
+
showArrows: settingArrows.checked,
|
|
74
|
+
};
|
|
75
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function applySettings(s) {
|
|
79
|
+
settingTheme.value = s.theme;
|
|
80
|
+
document.documentElement.dataset.theme = s.theme === 'light' ? 'light' : '';
|
|
81
|
+
setTheme(s.theme);
|
|
82
|
+
|
|
83
|
+
settingDimension.value = s.dimension;
|
|
84
|
+
setDimension(s.dimension);
|
|
85
|
+
|
|
86
|
+
restoreGroups(s.groups || []);
|
|
87
|
+
|
|
88
|
+
settingNodeSize.value = s.nodeSize;
|
|
89
|
+
valNodeSize.textContent = s.nodeSize.toFixed(1);
|
|
90
|
+
setNodeSize(s.nodeSize);
|
|
91
|
+
|
|
92
|
+
settingEdgeThickness.value = s.edgeThickness;
|
|
93
|
+
valEdgeThickness.textContent = s.edgeThickness.toFixed(1);
|
|
94
|
+
setEdgeThickness(s.edgeThickness);
|
|
95
|
+
|
|
96
|
+
settingNodeDistance.value = s.nodeDistance;
|
|
97
|
+
valNodeDistance.textContent = s.nodeDistance;
|
|
98
|
+
setNodeDistance(s.nodeDistance);
|
|
99
|
+
|
|
100
|
+
settingFontSize.value = s.fontSize;
|
|
101
|
+
valFontSize.textContent = s.fontSize;
|
|
102
|
+
setLabelFontSize(s.fontSize);
|
|
103
|
+
|
|
104
|
+
settingLabels.checked = s.showLabels;
|
|
105
|
+
setLabelsVisible(s.showLabels);
|
|
106
|
+
|
|
107
|
+
settingArrows.checked = s.showArrows;
|
|
108
|
+
setShowArrows(s.showArrows);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Apply saved settings after data loads
|
|
112
|
+
canvas.addEventListener('graph-ready', () => {
|
|
113
|
+
const s = loadSettings();
|
|
114
|
+
applySettings(s);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── 1. View Mode Toggle ────────────────────────────────────────────
|
|
118
|
+
viewToggle.addEventListener('click', () => {
|
|
119
|
+
const current = viewToggle.dataset.mode;
|
|
120
|
+
const next = current === 'file' ? 'symbol' : 'file';
|
|
121
|
+
viewToggle.dataset.mode = next;
|
|
122
|
+
viewToggle.textContent = next === 'file' ? 'File View' : 'Symbol View';
|
|
123
|
+
switchMode(next);
|
|
124
|
+
clearInfoPanel();
|
|
125
|
+
buildLegend(next);
|
|
126
|
+
applyFilter();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── 2. Settings Panel ──────────────────────────────────────────────
|
|
130
|
+
settingsBtn.addEventListener('click', () => settingsPanel.classList.toggle('hidden'));
|
|
131
|
+
settingsClose.addEventListener('click', () => settingsPanel.classList.add('hidden'));
|
|
132
|
+
|
|
133
|
+
settingTheme.addEventListener('change', () => {
|
|
134
|
+
document.documentElement.dataset.theme = settingTheme.value === 'light' ? 'light' : '';
|
|
135
|
+
setTheme(settingTheme.value);
|
|
136
|
+
saveSettings();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
settingDimension.addEventListener('change', () => {
|
|
140
|
+
setDimension(settingDimension.value);
|
|
141
|
+
saveSettings();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
settingNodeSize.addEventListener('input', () => {
|
|
146
|
+
const val = parseFloat(settingNodeSize.value);
|
|
147
|
+
valNodeSize.textContent = val.toFixed(1);
|
|
148
|
+
setNodeSize(val);
|
|
149
|
+
saveSettings();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
settingEdgeThickness.addEventListener('input', () => {
|
|
153
|
+
const val = parseFloat(settingEdgeThickness.value);
|
|
154
|
+
valEdgeThickness.textContent = val.toFixed(1);
|
|
155
|
+
setEdgeThickness(val);
|
|
156
|
+
saveSettings();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
settingNodeDistance.addEventListener('input', () => {
|
|
160
|
+
const val = parseInt(settingNodeDistance.value, 10);
|
|
161
|
+
valNodeDistance.textContent = val;
|
|
162
|
+
setNodeDistance(val);
|
|
163
|
+
saveSettings();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
settingFontSize.addEventListener('input', () => {
|
|
167
|
+
const val = parseInt(settingFontSize.value, 10);
|
|
168
|
+
valFontSize.textContent = val;
|
|
169
|
+
setLabelFontSize(val);
|
|
170
|
+
saveSettings();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
settingLabels.addEventListener('change', () => {
|
|
174
|
+
setLabelsVisible(settingLabels.checked);
|
|
175
|
+
saveSettings();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
settingArrows.addEventListener('change', () => {
|
|
179
|
+
setShowArrows(settingArrows.checked);
|
|
180
|
+
saveSettings();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── 3. Info Panel ──────────────────────────────────────────────────
|
|
184
|
+
function showInfoPanel(node) {
|
|
185
|
+
infoName.textContent = node.name || node.id;
|
|
186
|
+
infoKind.textContent = node.kind ? 'Kind: ' + node.kind : 'File';
|
|
187
|
+
infoFile.textContent = node.fileId ? 'File: ' + node.fileId : node.id;
|
|
188
|
+
infoPanel.classList.remove('hidden');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function clearInfoPanel() {
|
|
192
|
+
infoPanel.classList.add('hidden');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
infoClose.addEventListener('click', clearInfoPanel);
|
|
196
|
+
|
|
197
|
+
canvas.addEventListener('node-click', (e) => {
|
|
198
|
+
const { nodeId } = e.detail;
|
|
199
|
+
const data = getGraphData();
|
|
200
|
+
const mode = viewToggle.dataset.mode;
|
|
201
|
+
let node;
|
|
202
|
+
if (mode === 'file') node = data.files.find(f => f.id === nodeId);
|
|
203
|
+
else node = data.symbols.find(s => s.id === nodeId);
|
|
204
|
+
if (node) showInfoPanel(node);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
canvas.addEventListener('node-deselect', () => clearInfoPanel());
|
|
208
|
+
|
|
209
|
+
// ── 4. Dependency Chain Filtering (dblclick) ────────────────────────
|
|
210
|
+
canvas.addEventListener('node-dblclick', (e) => {
|
|
211
|
+
const { nodeId } = e.detail;
|
|
212
|
+
const edges = getCurrentEdges();
|
|
213
|
+
const chain = computeDependencyChain(nodeId, edges);
|
|
214
|
+
const visible = new Set([nodeId, ...chain.upstream, ...chain.downstream]);
|
|
215
|
+
setFilteredView(visible);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
document.addEventListener('keydown', (e) => {
|
|
219
|
+
if (e.key === 'Escape') {
|
|
220
|
+
clearFilter();
|
|
221
|
+
clearHighlights();
|
|
222
|
+
clearConnectedHighlight();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
export function computeDependencyChain(nodeId, edges) {
|
|
227
|
+
const forward = new Map();
|
|
228
|
+
const reverse = new Map();
|
|
229
|
+
for (const edge of edges) {
|
|
230
|
+
if (!forward.has(edge.source)) forward.set(edge.source, []);
|
|
231
|
+
forward.get(edge.source).push(edge.target);
|
|
232
|
+
if (!reverse.has(edge.target)) reverse.set(edge.target, []);
|
|
233
|
+
reverse.get(edge.target).push(edge.source);
|
|
234
|
+
}
|
|
235
|
+
function bfs(startId, adjacencyMap) {
|
|
236
|
+
const visited = new Set();
|
|
237
|
+
const queue = [startId];
|
|
238
|
+
while (queue.length > 0) {
|
|
239
|
+
const current = queue.shift();
|
|
240
|
+
if (visited.has(current)) continue;
|
|
241
|
+
visited.add(current);
|
|
242
|
+
const neighbors = adjacencyMap.get(current) || [];
|
|
243
|
+
for (const n of neighbors) { if (!visited.has(n)) queue.push(n); }
|
|
244
|
+
}
|
|
245
|
+
visited.delete(startId);
|
|
246
|
+
return visited;
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
downstream: bfs(nodeId, forward),
|
|
250
|
+
upstream: bfs(nodeId, reverse),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Autocomplete Helper ─────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
function getAutocompleteCandidates() {
|
|
257
|
+
const data = getGraphData();
|
|
258
|
+
if (!data) return [];
|
|
259
|
+
const mode = viewToggle.dataset.mode;
|
|
260
|
+
const items = mode === 'file' ? data.files : data.symbols;
|
|
261
|
+
return items;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createAutocomplete(inputEl, getCandidatesFn, onSelect) {
|
|
265
|
+
const list = document.createElement('div');
|
|
266
|
+
list.className = 'autocomplete-list hidden';
|
|
267
|
+
document.body.appendChild(list);
|
|
268
|
+
let activeIdx = -1;
|
|
269
|
+
let items = [];
|
|
270
|
+
|
|
271
|
+
function show(query) {
|
|
272
|
+
const q = query.trim().toLowerCase();
|
|
273
|
+
list.replaceChildren();
|
|
274
|
+
activeIdx = -1;
|
|
275
|
+
if (!q || q.length < 1) { list.classList.add('hidden'); return; }
|
|
276
|
+
|
|
277
|
+
const candidates = getCandidatesFn();
|
|
278
|
+
items = [];
|
|
279
|
+
for (const c of candidates) {
|
|
280
|
+
const name = (c.name || c.id.split('/').pop()).toLowerCase();
|
|
281
|
+
const id = c.id.toLowerCase();
|
|
282
|
+
const file = (c.fileId || '').toLowerCase();
|
|
283
|
+
const kind = (c.kind || '').toLowerCase();
|
|
284
|
+
if (name.includes(q) || id.includes(q) || file.includes(q) || kind.includes(q)) {
|
|
285
|
+
items.push(c);
|
|
286
|
+
if (items.length >= 8) break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (items.length === 0) { list.classList.add('hidden'); return; }
|
|
291
|
+
|
|
292
|
+
for (let i = 0; i < items.length; i++) {
|
|
293
|
+
const c = items[i];
|
|
294
|
+
const el = document.createElement('div');
|
|
295
|
+
el.className = 'autocomplete-item';
|
|
296
|
+
const displayName = c.name || c.id.split('/').pop();
|
|
297
|
+
const sub = c.fileId ? ' \u2014 ' + c.fileId : (c.kind ? ' \u2014 ' + c.kind : '');
|
|
298
|
+
|
|
299
|
+
// Highlight match
|
|
300
|
+
const lowerName = displayName.toLowerCase();
|
|
301
|
+
const matchStart = lowerName.indexOf(q);
|
|
302
|
+
if (matchStart >= 0) {
|
|
303
|
+
el.appendChild(document.createTextNode(displayName.slice(0, matchStart)));
|
|
304
|
+
const mark = document.createElement('span');
|
|
305
|
+
mark.className = 'ac-match';
|
|
306
|
+
mark.textContent = displayName.slice(matchStart, matchStart + q.length);
|
|
307
|
+
el.appendChild(mark);
|
|
308
|
+
el.appendChild(document.createTextNode(displayName.slice(matchStart + q.length)));
|
|
309
|
+
} else {
|
|
310
|
+
el.appendChild(document.createTextNode(displayName));
|
|
311
|
+
}
|
|
312
|
+
if (sub) {
|
|
313
|
+
const small = document.createTextNode(sub);
|
|
314
|
+
el.appendChild(small);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
el.addEventListener('mousedown', (e) => {
|
|
318
|
+
e.preventDefault();
|
|
319
|
+
onSelect(c, inputEl);
|
|
320
|
+
hide();
|
|
321
|
+
});
|
|
322
|
+
list.appendChild(el);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const rect = inputEl.getBoundingClientRect();
|
|
326
|
+
list.style.left = rect.left + 'px';
|
|
327
|
+
list.style.top = (rect.bottom + 2) + 'px';
|
|
328
|
+
list.style.minWidth = rect.width + 'px';
|
|
329
|
+
list.classList.remove('hidden');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hide() {
|
|
333
|
+
list.classList.add('hidden');
|
|
334
|
+
activeIdx = -1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function navigate(delta) {
|
|
338
|
+
const children = list.querySelectorAll('.autocomplete-item');
|
|
339
|
+
if (children.length === 0) return false;
|
|
340
|
+
if (activeIdx >= 0 && children[activeIdx]) children[activeIdx].classList.remove('active');
|
|
341
|
+
activeIdx = (activeIdx + delta + children.length) % children.length;
|
|
342
|
+
children[activeIdx].classList.add('active');
|
|
343
|
+
children[activeIdx].scrollIntoView({ block: 'nearest' });
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function selectActive() {
|
|
348
|
+
if (activeIdx >= 0 && items[activeIdx]) {
|
|
349
|
+
onSelect(items[activeIdx], inputEl);
|
|
350
|
+
hide();
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
inputEl.addEventListener('input', () => show(inputEl.value));
|
|
357
|
+
inputEl.addEventListener('blur', () => setTimeout(hide, 150));
|
|
358
|
+
inputEl.addEventListener('focus', () => { if (inputEl.value.trim()) show(inputEl.value); });
|
|
359
|
+
|
|
360
|
+
return { show, hide, navigate, selectActive };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── 5. Search ──────────────────────────────────────────────────────
|
|
364
|
+
const searchNav = document.getElementById('search-nav');
|
|
365
|
+
const searchCount = document.getElementById('search-count');
|
|
366
|
+
const searchPrev = document.getElementById('search-prev');
|
|
367
|
+
const searchNext = document.getElementById('search-next');
|
|
368
|
+
|
|
369
|
+
let searchTimeout = null;
|
|
370
|
+
let searchResults = [];
|
|
371
|
+
let searchIndex = 0;
|
|
372
|
+
|
|
373
|
+
function runSearch() {
|
|
374
|
+
const query = searchInput.value.trim().toLowerCase();
|
|
375
|
+
clearHighlights();
|
|
376
|
+
searchResults = [];
|
|
377
|
+
searchIndex = 0;
|
|
378
|
+
|
|
379
|
+
if (!query) {
|
|
380
|
+
searchNav.classList.add('hidden');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const data = getGraphData();
|
|
385
|
+
const mode = viewToggle.dataset.mode;
|
|
386
|
+
const items = mode === 'file' ? data.files : data.symbols;
|
|
387
|
+
for (const item of items) {
|
|
388
|
+
const name = (item.name || '').toLowerCase();
|
|
389
|
+
const id = item.id.toLowerCase();
|
|
390
|
+
const file = (item.fileId || '').toLowerCase();
|
|
391
|
+
if (name.includes(query) || id.includes(query) || file.includes(query)) searchResults.push(item.id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (searchResults.length > 0) {
|
|
395
|
+
updateSearchNav();
|
|
396
|
+
searchNav.classList.remove('hidden');
|
|
397
|
+
} else {
|
|
398
|
+
searchNav.classList.add('hidden');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function updateSearchNav() {
|
|
403
|
+
searchCount.textContent = (searchIndex + 1) + ' / ' + searchResults.length;
|
|
404
|
+
clearHighlights();
|
|
405
|
+
highlightNodes(searchResults, searchResults[searchIndex]);
|
|
406
|
+
focusNode(searchResults[searchIndex]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function searchGo(delta) {
|
|
410
|
+
if (searchResults.length === 0) return;
|
|
411
|
+
searchIndex = (searchIndex + delta + searchResults.length) % searchResults.length;
|
|
412
|
+
updateSearchNav();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
searchInput.addEventListener('input', () => {
|
|
416
|
+
clearTimeout(searchTimeout);
|
|
417
|
+
searchTimeout = setTimeout(runSearch, 300);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
searchInput.addEventListener('keydown', (e) => {
|
|
421
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); searchAc.navigate(1); return; }
|
|
422
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); searchAc.navigate(-1); return; }
|
|
423
|
+
if (e.key === 'Enter') {
|
|
424
|
+
e.preventDefault();
|
|
425
|
+
if (!searchAc.selectActive() && searchResults.length > 0) {
|
|
426
|
+
searchGo(e.shiftKey ? -1 : 1);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
searchPrev.addEventListener('click', () => searchGo(-1));
|
|
432
|
+
searchNext.addEventListener('click', () => searchGo(1));
|
|
433
|
+
|
|
434
|
+
const searchAc = createAutocomplete(
|
|
435
|
+
searchInput,
|
|
436
|
+
getAutocompleteCandidates,
|
|
437
|
+
(item) => {
|
|
438
|
+
searchInput.value = item.name || item.id.split('/').pop();
|
|
439
|
+
runSearch();
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// ── 6. Filter ──────────────────────────────────────────────────────
|
|
444
|
+
|
|
445
|
+
function applyFilter() {
|
|
446
|
+
const data = getGraphData();
|
|
447
|
+
if (!data) return;
|
|
448
|
+
|
|
449
|
+
const mode = viewToggle.dataset.mode;
|
|
450
|
+
const pathQuery = filterPath.value.trim().toLowerCase();
|
|
451
|
+
|
|
452
|
+
if (mode === 'file') {
|
|
453
|
+
// File mode: only path filter applies
|
|
454
|
+
if (!pathQuery) {
|
|
455
|
+
setNodeVisibility(null);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const visible = new Set();
|
|
459
|
+
for (const f of data.files) {
|
|
460
|
+
if (f.id.toLowerCase().includes(pathQuery)) visible.add(f.id);
|
|
461
|
+
}
|
|
462
|
+
setNodeVisibility(visible);
|
|
463
|
+
} else {
|
|
464
|
+
// Symbol mode: kind checkboxes + path filter
|
|
465
|
+
const enabledKinds = new Set();
|
|
466
|
+
for (const cb of filterKindCheckboxes) {
|
|
467
|
+
if (cb.checked) enabledKinds.add(cb.dataset.kind);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const allKindsEnabled = enabledKinds.size === filterKindCheckboxes.length;
|
|
471
|
+
const noPathFilter = !pathQuery;
|
|
472
|
+
|
|
473
|
+
if (allKindsEnabled && noPathFilter) {
|
|
474
|
+
setNodeVisibility(null);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const visible = new Set();
|
|
479
|
+
for (const s of data.symbols) {
|
|
480
|
+
if (!enabledKinds.has(s.kind)) continue;
|
|
481
|
+
if (pathQuery) {
|
|
482
|
+
const filePath = (s.fileId || '').toLowerCase();
|
|
483
|
+
const name = (s.name || '').toLowerCase();
|
|
484
|
+
if (!filePath.includes(pathQuery) && !name.includes(pathQuery)) continue;
|
|
485
|
+
}
|
|
486
|
+
visible.add(s.id);
|
|
487
|
+
}
|
|
488
|
+
setNodeVisibility(visible);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (const cb of filterKindCheckboxes) {
|
|
493
|
+
cb.addEventListener('change', applyFilter);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let filterPathTimeout = null;
|
|
497
|
+
filterPath.addEventListener('input', () => {
|
|
498
|
+
clearTimeout(filterPathTimeout);
|
|
499
|
+
filterPathTimeout = setTimeout(applyFilter, 300);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const filterAc = createAutocomplete(
|
|
503
|
+
filterPath,
|
|
504
|
+
getAutocompleteCandidates,
|
|
505
|
+
(item) => {
|
|
506
|
+
// For filter, use the path/fileId
|
|
507
|
+
filterPath.value = item.fileId || item.id;
|
|
508
|
+
applyFilter();
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
filterPath.addEventListener('keydown', (e) => {
|
|
513
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); filterAc.navigate(1); return; }
|
|
514
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); filterAc.navigate(-1); return; }
|
|
515
|
+
if (e.key === 'Enter') { e.preventDefault(); filterAc.selectActive(); }
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ── 7. Legend ───────────────────────────────────────────────────────
|
|
519
|
+
function createLegendItem(color, label) {
|
|
520
|
+
const item = document.createElement('div');
|
|
521
|
+
const dot = document.createElement('span');
|
|
522
|
+
dot.style.cssText = 'display:inline-block;width:10px;height:10px;border-radius:50%;background:' + color + ';margin-right:6px;vertical-align:middle;';
|
|
523
|
+
item.appendChild(dot);
|
|
524
|
+
item.appendChild(document.createTextNode(label));
|
|
525
|
+
return item;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function buildLegend(mode) {
|
|
529
|
+
legend.replaceChildren();
|
|
530
|
+
if (mode === 'symbol') {
|
|
531
|
+
const kinds = [
|
|
532
|
+
{ color: '#4488ff', label: 'Class' },
|
|
533
|
+
{ color: '#44cc66', label: 'Function' },
|
|
534
|
+
{ color: '#aa66cc', label: 'Interface' },
|
|
535
|
+
{ color: '#ff8844', label: 'Type' },
|
|
536
|
+
{ color: '#ee4444', label: 'Enum' },
|
|
537
|
+
{ color: '#888888', label: 'Variable' },
|
|
538
|
+
];
|
|
539
|
+
for (const { color, label } of kinds) legend.appendChild(createLegendItem(color, label));
|
|
540
|
+
} else {
|
|
541
|
+
legend.appendChild(createLegendItem('#4488ff', 'File'));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
buildLegend('file');
|
|
546
|
+
|
|
547
|
+
// ── 8. Multi-Group Panel ──────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
const groupList = document.getElementById('group-list');
|
|
550
|
+
const groupAdd = document.getElementById('group-add');
|
|
551
|
+
|
|
552
|
+
const GROUP_COLORS = [
|
|
553
|
+
0xee4444, 0xccaa33, 0x44cc66, 0x4488ff, 0xaa66cc,
|
|
554
|
+
0x44bbbb, 0xff8844, 0xff66aa, 0x66aaff, 0x88cc88,
|
|
555
|
+
];
|
|
556
|
+
let groupIdCounter = 0;
|
|
557
|
+
|
|
558
|
+
function colorToHex(c) {
|
|
559
|
+
return '#' + c.toString(16).padStart(6, '0');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function createGroupRow(query, colorNum) {
|
|
563
|
+
const id = ++groupIdCounter;
|
|
564
|
+
const row = document.createElement('div');
|
|
565
|
+
row.className = 'group-row';
|
|
566
|
+
row.dataset.groupId = id;
|
|
567
|
+
|
|
568
|
+
const input = document.createElement('input');
|
|
569
|
+
input.type = 'text';
|
|
570
|
+
input.className = 'group-query';
|
|
571
|
+
input.placeholder = '검색어 입력... (path: file: kind:)';
|
|
572
|
+
input.value = query || '';
|
|
573
|
+
|
|
574
|
+
const colorInput = document.createElement('input');
|
|
575
|
+
colorInput.type = 'color';
|
|
576
|
+
colorInput.className = 'group-color-input';
|
|
577
|
+
colorInput.value = colorToHex(colorNum);
|
|
578
|
+
|
|
579
|
+
const colorDot = document.createElement('span');
|
|
580
|
+
colorDot.className = 'group-color';
|
|
581
|
+
colorDot.style.background = colorInput.value;
|
|
582
|
+
|
|
583
|
+
colorDot.addEventListener('click', () => colorInput.click());
|
|
584
|
+
colorInput.addEventListener('input', () => {
|
|
585
|
+
colorDot.style.background = colorInput.value;
|
|
586
|
+
syncGroups();
|
|
587
|
+
saveSettings();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const removeBtn = document.createElement('button');
|
|
591
|
+
removeBtn.className = 'group-remove';
|
|
592
|
+
removeBtn.textContent = '\u00d7';
|
|
593
|
+
|
|
594
|
+
row.appendChild(input);
|
|
595
|
+
row.appendChild(colorInput);
|
|
596
|
+
row.appendChild(colorDot);
|
|
597
|
+
row.appendChild(removeBtn);
|
|
598
|
+
groupList.appendChild(row);
|
|
599
|
+
|
|
600
|
+
let debounce = null;
|
|
601
|
+
input.addEventListener('input', () => {
|
|
602
|
+
clearTimeout(debounce);
|
|
603
|
+
debounce = setTimeout(() => { syncGroups(); saveSettings(); }, 400);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Autocomplete for group query (after prefix)
|
|
607
|
+
const groupAc = createAutocomplete(
|
|
608
|
+
input,
|
|
609
|
+
() => getGroupQueryCandidates(input.value),
|
|
610
|
+
(item, el) => {
|
|
611
|
+
const val = el.value.trim().toLowerCase();
|
|
612
|
+
if (val.startsWith('path:')) {
|
|
613
|
+
el.value = 'path:' + (item.fileId || item.id).split('/').slice(0, -1).join('/');
|
|
614
|
+
} else if (val.startsWith('file:')) {
|
|
615
|
+
el.value = 'file:' + (item.fileId || item.id).split('/').pop();
|
|
616
|
+
} else if (val.startsWith('kind:')) {
|
|
617
|
+
el.value = 'kind:' + (item.kind || 'file');
|
|
618
|
+
} else {
|
|
619
|
+
el.value = item.name || item.id.split('/').pop();
|
|
620
|
+
}
|
|
621
|
+
syncGroups();
|
|
622
|
+
saveSettings();
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
input.addEventListener('keydown', (e) => {
|
|
627
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); groupAc.navigate(1); return; }
|
|
628
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); groupAc.navigate(-1); return; }
|
|
629
|
+
if (e.key === 'Enter') { e.preventDefault(); groupAc.selectActive(); }
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Show hint dropdown on focus (only when empty)
|
|
633
|
+
input.addEventListener('focus', () => showGroupHint(input));
|
|
634
|
+
input.addEventListener('blur', () => setTimeout(hideGroupHint, 150));
|
|
635
|
+
|
|
636
|
+
removeBtn.addEventListener('click', () => {
|
|
637
|
+
row.remove();
|
|
638
|
+
syncGroups();
|
|
639
|
+
saveSettings();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return row;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function getGroupQueryCandidates(rawQuery) {
|
|
646
|
+
const data = getGraphData();
|
|
647
|
+
if (!data) return [];
|
|
648
|
+
const mode = viewToggle.dataset.mode;
|
|
649
|
+
const items = mode === 'file' ? data.files : data.symbols;
|
|
650
|
+
|
|
651
|
+
const q = rawQuery.trim().toLowerCase();
|
|
652
|
+
// For kind: prefix, return unique kinds as pseudo-items
|
|
653
|
+
if (q.startsWith('kind:')) {
|
|
654
|
+
const val = q.slice(5);
|
|
655
|
+
const kinds = new Set();
|
|
656
|
+
for (const item of items) kinds.add(item.kind || 'file');
|
|
657
|
+
return [...kinds].filter(k => k.includes(val)).map(k => ({ id: k, name: k, kind: k }));
|
|
658
|
+
}
|
|
659
|
+
return items;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let hintEl = null;
|
|
663
|
+
function showGroupHint(inputEl) {
|
|
664
|
+
hideGroupHint();
|
|
665
|
+
if (inputEl.value.length > 0) return; // only show when empty
|
|
666
|
+
hintEl = document.createElement('div');
|
|
667
|
+
hintEl.className = 'group-hint';
|
|
668
|
+
const options = [
|
|
669
|
+
{ prefix: 'path:', desc: '일치하는 파일 경로' },
|
|
670
|
+
{ prefix: 'file:', desc: '일치하는 파일 이름' },
|
|
671
|
+
{ prefix: 'kind:', desc: 'class, function, interface 등' },
|
|
672
|
+
];
|
|
673
|
+
for (const opt of options) {
|
|
674
|
+
const item = document.createElement('div');
|
|
675
|
+
const bold = document.createElement('b');
|
|
676
|
+
bold.textContent = opt.prefix;
|
|
677
|
+
item.appendChild(bold);
|
|
678
|
+
item.appendChild(document.createTextNode(' ' + opt.desc));
|
|
679
|
+
item.addEventListener('mousedown', (e) => {
|
|
680
|
+
e.preventDefault();
|
|
681
|
+
inputEl.value = opt.prefix;
|
|
682
|
+
inputEl.focus();
|
|
683
|
+
hideGroupHint();
|
|
684
|
+
});
|
|
685
|
+
hintEl.appendChild(item);
|
|
686
|
+
}
|
|
687
|
+
const rect = inputEl.getBoundingClientRect();
|
|
688
|
+
hintEl.style.left = rect.left + 'px';
|
|
689
|
+
hintEl.style.top = (rect.bottom + 4) + 'px';
|
|
690
|
+
document.body.appendChild(hintEl);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function hideGroupHint() {
|
|
694
|
+
if (hintEl) { hintEl.remove(); hintEl = null; }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function syncGroups() {
|
|
698
|
+
const rows = groupList.querySelectorAll('.group-row');
|
|
699
|
+
const groups = [];
|
|
700
|
+
for (const row of rows) {
|
|
701
|
+
const query = row.querySelector('.group-query').value.trim();
|
|
702
|
+
const color = parseInt(row.querySelector('.group-color-input').value.slice(1), 16);
|
|
703
|
+
if (query) groups.push({ query, color });
|
|
704
|
+
}
|
|
705
|
+
setGroups(groups);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function getGroupDefs() {
|
|
709
|
+
const rows = groupList.querySelectorAll('.group-row');
|
|
710
|
+
const defs = [];
|
|
711
|
+
for (const row of rows) {
|
|
712
|
+
const query = row.querySelector('.group-query').value;
|
|
713
|
+
const color = parseInt(row.querySelector('.group-color-input').value.slice(1), 16);
|
|
714
|
+
defs.push({ query, color });
|
|
715
|
+
}
|
|
716
|
+
return defs;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function restoreGroups(defs) {
|
|
720
|
+
groupList.replaceChildren();
|
|
721
|
+
for (let i = 0; i < defs.length; i++) {
|
|
722
|
+
const color = defs[i].color || GROUP_COLORS[i % GROUP_COLORS.length];
|
|
723
|
+
createGroupRow(defs[i].query || '', color);
|
|
724
|
+
}
|
|
725
|
+
syncGroups();
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
groupAdd.addEventListener('click', () => {
|
|
729
|
+
const idx = groupList.querySelectorAll('.group-row').length;
|
|
730
|
+
const color = GROUP_COLORS[idx % GROUP_COLORS.length];
|
|
731
|
+
createGroupRow('', color);
|
|
732
|
+
});
|