@mishasinitcyn/betterrank 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mishasinitcyn/betterrank",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Structural code index with PageRank-ranked repo maps, symbol search, call-graph queries, and dependency analysis. Built on tree-sitter and graphology.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -35,6 +35,12 @@
35
35
  "tree-sitter-javascript": "0.23.1",
36
36
  "tree-sitter-typescript": "0.23.2",
37
37
  "tree-sitter-python": "0.23.6",
38
+ "graphology": "^0.25.4",
39
+ "graphology-metrics": "^2.4.0",
40
+ "graphology-types": "^0.24.7",
41
+ "glob": "^11.0.1"
42
+ },
43
+ "optionalDependencies": {
38
44
  "tree-sitter-rust": "0.23.2",
39
45
  "tree-sitter-go": "0.23.4",
40
46
  "tree-sitter-ruby": "0.23.1",
@@ -42,11 +48,7 @@
42
48
  "tree-sitter-c": "0.23.4",
43
49
  "tree-sitter-cpp": "0.23.4",
44
50
  "tree-sitter-c-sharp": "0.23.1",
45
- "tree-sitter-php": "0.23.11",
46
- "graphology": "^0.25.4",
47
- "graphology-metrics": "^2.4.0",
48
- "graphology-types": "^0.24.7",
49
- "glob": "^11.0.1"
51
+ "tree-sitter-php": "0.23.11"
50
52
  },
51
53
  "overrides": {
52
54
  "tree-sitter": "0.22.4"
package/src/cache.js CHANGED
@@ -24,38 +24,90 @@ function getPlatformCacheDir() {
24
24
  const CACHE_DIR = getPlatformCacheDir();
25
25
 
26
26
  const IGNORE_PATTERNS = [
27
- // Dependencies & generated
27
+ // ── JS / Node ──────────────────────────────────────────
28
28
  '**/node_modules/**',
29
- '**/dist/**',
30
- '**/build/**',
31
- '**/coverage/**',
32
- '**/vendor/**',
29
+ '**/.npm/**',
30
+ '**/.yarn/**',
31
+ '**/.pnp.*',
32
+ '**/bower_components/**',
33
33
  '**/*.min.js',
34
34
  '**/*.bundle.js',
35
35
  '**/*.map',
36
36
 
37
- // VCS & tool caches
38
- '**/.git/**',
39
- '**/.code-index/**',
40
- '**/.claude/**',
41
- '**/.cursor/**',
42
-
43
- // Language-specific caches
37
+ // ── Python ─────────────────────────────────────────────
44
38
  '**/__pycache__/**',
45
39
  '**/.venv/**',
40
+ '**/venv/**',
41
+ '**/env/**',
42
+ '**/.env/**',
43
+ '**/.virtualenvs/**',
44
+ '**/site-packages/**',
45
+ '**/*.egg-info/**',
46
+ '**/.eggs/**',
47
+ '**/.tox/**',
48
+ '**/.mypy_cache/**',
49
+ '**/.pytest_cache/**',
50
+ '**/.ruff_cache/**',
51
+
52
+ // ── Rust ───────────────────────────────────────────────
53
+ '**/target/debug/**',
54
+ '**/target/release/**',
55
+
56
+ // ── Java / JVM ─────────────────────────────────────────
57
+ '**/.gradle/**',
58
+ '**/.m2/**',
59
+
60
+ // ── Ruby ───────────────────────────────────────────────
61
+ '**/.bundle/**',
62
+
63
+ // ── C / C++ ────────────────────────────────────────────
64
+ '**/cmake-build-*/**',
65
+ '**/CMakeFiles/**',
66
+
67
+ // ── Go ─────────────────────────────────────────────────
68
+ '**/vendor/**',
69
+
70
+ // ── Build output & generated ───────────────────────────
71
+ '**/dist/**',
72
+ '**/build/**',
73
+ '**/out/**',
74
+ '**/coverage/**',
75
+
76
+ // ── Frameworks ─────────────────────────────────────────
46
77
  '**/.next/**',
47
78
  '**/.nuxt/**',
48
-
49
- // iOS / mobile vendor
79
+ '**/.output/**',
80
+ '**/.svelte-kit/**',
81
+ '**/.angular/**',
82
+ '**/.turbo/**',
83
+ '**/.parcel-cache/**',
84
+ '**/.cache/**',
85
+
86
+ // ── iOS / mobile ───────────────────────────────────────
50
87
  '**/Pods/**',
51
88
  '**/*.xcframework/**',
52
89
 
53
- // UI component libraries (shadcn, etc.) — high fan-in but rarely investigation targets.
90
+ // ── Infrastructure / deploy ────────────────────────────
91
+ '**/.terraform/**',
92
+ '**/.serverless/**',
93
+ '**/cdk.out/**',
94
+ '**/.vercel/**',
95
+ '**/.netlify/**',
96
+
97
+ // ── VCS & tool caches ──────────────────────────────────
98
+ '**/.git/**',
99
+ '**/.code-index/**',
100
+ '**/.claude/**',
101
+ '**/.cursor/**',
102
+ '**/.nx/**',
103
+
104
+ // ── UI component libraries ─────────────────────────────
105
+ // shadcn, etc. — high fan-in but rarely investigation targets.
54
106
  // To re-include, add "!**/components/ui/**" in .code-index/config.json ignore list,
55
107
  // or pass --ignore '!**/components/ui/**' on the CLI.
56
108
  '**/components/ui/**',
57
109
 
58
- // Scratch / temp
110
+ // ── Scratch / temp ─────────────────────────────────────
59
111
  'tmp/**',
60
112
  ];
61
113
 
package/src/index.js CHANGED
@@ -78,13 +78,13 @@ class CodeIndex {
78
78
  * @param {boolean} [opts.count] - If true, return only { total }
79
79
  * @returns {{content, shownFiles, shownSymbols, totalFiles, totalSymbols}|{total: number}}
80
80
  */
81
- async map({ focusFiles = [], offset, limit, count = false } = {}) {
81
+ async map({ focusFiles = [], offset, limit, count = false, structured = false } = {}) {
82
82
  await this._ensureReady();
83
83
  const graph = this.cache.getGraph();
84
84
  if (!graph || graph.order === 0) {
85
- return count
86
- ? { total: 0 }
87
- : { content: '(empty index)', shownFiles: 0, shownSymbols: 0, totalFiles: 0, totalSymbols: 0 };
85
+ if (count) return { total: 0 };
86
+ if (structured) return { files: [], shownFiles: 0, shownSymbols: 0, totalFiles: 0, totalSymbols: 0 };
87
+ return { content: '(empty index)', shownFiles: 0, shownSymbols: 0, totalFiles: 0, totalSymbols: 0 };
88
88
  }
89
89
 
90
90
  // Count totals from the graph
@@ -97,7 +97,7 @@ class CodeIndex {
97
97
 
98
98
  const ranked = this._getRanked(focusFiles);
99
99
 
100
- // Collect all symbol entries ranked by PageRank
100
+ // Collect all symbol entries ranked by PageRank (rich data for both formats)
101
101
  const allEntries = [];
102
102
  for (const [symbolKey, _score] of ranked) {
103
103
  let attrs;
@@ -108,18 +108,48 @@ class CodeIndex {
108
108
  }
109
109
  if (attrs.type !== 'symbol') continue;
110
110
 
111
- const line = ` ${String(attrs.lineStart).padStart(4)}│ ${attrs.signature}`;
112
- allEntries.push({ file: attrs.file, line });
111
+ allEntries.push({
112
+ file: attrs.file,
113
+ name: attrs.name,
114
+ kind: attrs.kind,
115
+ lineStart: attrs.lineStart,
116
+ lineEnd: attrs.lineEnd,
117
+ signature: attrs.signature,
118
+ });
113
119
  }
114
120
 
115
121
  if (count) return { total: allEntries.length };
116
122
 
117
123
  const { items } = paginate(allEntries, { offset, limit });
118
124
 
125
+ // Structured format: return file objects with nested symbol arrays
126
+ if (structured) {
127
+ const fileGroups = new Map();
128
+ for (const entry of items) {
129
+ if (!fileGroups.has(entry.file)) fileGroups.set(entry.file, []);
130
+ fileGroups.get(entry.file).push({
131
+ name: entry.name,
132
+ kind: entry.kind,
133
+ lineStart: entry.lineStart,
134
+ lineEnd: entry.lineEnd,
135
+ signature: entry.signature,
136
+ });
137
+ }
138
+ return {
139
+ files: [...fileGroups.entries()].map(([path, symbols]) => ({ path, symbols })),
140
+ shownFiles: fileGroups.size,
141
+ shownSymbols: items.length,
142
+ totalFiles,
143
+ totalSymbols,
144
+ };
145
+ }
146
+
147
+ // Text format (CLI output)
119
148
  const fileGroups = new Map();
120
149
  for (const entry of items) {
121
150
  if (!fileGroups.has(entry.file)) fileGroups.set(entry.file, []);
122
- fileGroups.get(entry.file).push(entry.line);
151
+ const line = ` ${String(entry.lineStart).padStart(4)}│ ${entry.signature}`;
152
+ fileGroups.get(entry.file).push(line);
123
153
  }
124
154
 
125
155
  const lines = [];
package/src/parser.js CHANGED
@@ -3,25 +3,37 @@ const require = createRequire(import.meta.url);
3
3
 
4
4
  const Parser = require('tree-sitter');
5
5
 
6
- // Load native grammars eagerly — no WASM, no async init needed
6
+ function tryRequire(mod) {
7
+ try { return require(mod); } catch { return null; }
8
+ }
9
+
10
+ // Core grammars (always installed)
7
11
  const tsGrammars = require('tree-sitter-typescript');
8
- const phpModule = require('tree-sitter-php');
9
12
 
10
13
  const GRAMMARS = {
11
14
  javascript: require('tree-sitter-javascript'),
12
15
  typescript: tsGrammars.typescript,
13
16
  tsx: tsGrammars.tsx,
14
17
  python: require('tree-sitter-python'),
15
- rust: require('tree-sitter-rust'),
16
- go: require('tree-sitter-go'),
17
- ruby: require('tree-sitter-ruby'),
18
- java: require('tree-sitter-java'),
19
- c: require('tree-sitter-c'),
20
- cpp: require('tree-sitter-cpp'),
21
- c_sharp: require('tree-sitter-c-sharp'),
22
- php: phpModule.php || phpModule,
23
18
  };
24
19
 
20
+ // Optional grammars (install individually if needed)
21
+ const optGrammars = [
22
+ ['rust', 'tree-sitter-rust'],
23
+ ['go', 'tree-sitter-go'],
24
+ ['ruby', 'tree-sitter-ruby'],
25
+ ['java', 'tree-sitter-java'],
26
+ ['c', 'tree-sitter-c'],
27
+ ['cpp', 'tree-sitter-cpp'],
28
+ ['c_sharp', 'tree-sitter-c-sharp'],
29
+ ];
30
+ for (const [name, mod] of optGrammars) {
31
+ const g = tryRequire(mod);
32
+ if (g) GRAMMARS[name] = g;
33
+ }
34
+ const phpModule = tryRequire('tree-sitter-php');
35
+ if (phpModule) GRAMMARS.php = phpModule.php || phpModule;
36
+
25
37
  const LANG_MAP = {
26
38
  '.js': 'javascript',
27
39
  '.mjs': 'javascript',
@@ -43,7 +55,11 @@ const LANG_MAP = {
43
55
  '.php': 'php',
44
56
  };
45
57
 
46
- const SUPPORTED_EXTENSIONS = Object.keys(LANG_MAP);
58
+ // Only include extensions for grammars that are actually installed
59
+ const SUPPORTED_EXTENSIONS = Object.keys(LANG_MAP).filter(ext => {
60
+ const lang = LANG_MAP[ext];
61
+ return GRAMMARS[lang] != null;
62
+ });
47
63
 
48
64
  function getLanguage(ext) {
49
65
  const langName = LANG_MAP[ext];
package/src/server.js CHANGED
@@ -139,10 +139,12 @@ const routes = {
139
139
  const p = params(req.url);
140
140
  const focus = p.get('focus', '');
141
141
  const focusFiles = focus ? focus.split(',') : [];
142
+ const format = p.get('format', 'text');
142
143
  const result = await currentIndex.map({
143
144
  focusFiles,
144
145
  offset: p.getInt('offset', undefined),
145
146
  limit: p.getInt('limit', 50),
147
+ structured: format === 'structured',
146
148
  });
147
149
  json(res, result);
148
150
  },
package/src/ui.html CHANGED
@@ -475,6 +475,100 @@
475
475
  border-color: var(--accent);
476
476
  background: rgba(37, 99, 235, 0.08);
477
477
  }
478
+
479
+ /* Expandable map cards */
480
+ .map-card {
481
+ border-bottom: 1px solid var(--border);
482
+ }
483
+ .map-card-header {
484
+ display: flex;
485
+ align-items: center;
486
+ gap: 8px;
487
+ padding: 12px 16px 4px;
488
+ cursor: pointer;
489
+ border-radius: 6px 6px 0 0;
490
+ transition: background 0.1s;
491
+ }
492
+ .map-card-header:hover { background: var(--bg-raised); }
493
+ .map-chevron {
494
+ font-size: 10px;
495
+ color: var(--text-faint);
496
+ transition: transform 0.2s ease;
497
+ flex-shrink: 0;
498
+ width: 14px;
499
+ text-align: center;
500
+ user-select: none;
501
+ }
502
+ .map-card.expanded .map-chevron { transform: rotate(90deg); }
503
+ .map-symbols {
504
+ padding: 2px 16px 10px 38px;
505
+ }
506
+ .map-expand {
507
+ display: none;
508
+ padding: 10px 16px 14px 38px;
509
+ background: var(--bg-raised);
510
+ margin: 0 8px 8px;
511
+ border-radius: 6px;
512
+ }
513
+ .map-card.expanded .map-expand { display: block; }
514
+ .map-expand-section {
515
+ margin-bottom: 10px;
516
+ }
517
+ .map-expand-section:last-child { margin-bottom: 0; }
518
+ .map-expand-title {
519
+ font-family: var(--mono);
520
+ font-size: 11px;
521
+ font-weight: 600;
522
+ letter-spacing: 0.3px;
523
+ color: var(--text-dim);
524
+ margin-bottom: 4px;
525
+ }
526
+ .map-expand-title .arrow { margin-right: 2px; }
527
+ .map-dep-item {
528
+ font-family: var(--mono);
529
+ font-size: 12px;
530
+ padding: 2px 0;
531
+ color: var(--accent);
532
+ cursor: pointer;
533
+ transition: color 0.1s;
534
+ }
535
+ .map-dep-item:hover { text-decoration: underline; }
536
+ .map-expand-empty {
537
+ font-family: var(--mono);
538
+ font-size: 11px;
539
+ color: var(--text-faint);
540
+ font-style: italic;
541
+ padding: 2px 0;
542
+ }
543
+ .map-expand-loading {
544
+ font-family: var(--mono);
545
+ font-size: 11px;
546
+ color: var(--text-dim);
547
+ padding: 4px 0;
548
+ }
549
+ .map-sym-item {
550
+ font-family: var(--mono);
551
+ font-size: 12px;
552
+ padding: 2px 0;
553
+ color: var(--text-dim);
554
+ cursor: pointer;
555
+ transition: color 0.1s;
556
+ display: flex;
557
+ align-items: baseline;
558
+ gap: 6px;
559
+ }
560
+ .map-sym-item:hover { color: var(--text); }
561
+ .map-sym-item .result-kind {
562
+ opacity: 0.55;
563
+ font-size: 9px;
564
+ flex-shrink: 0;
565
+ }
566
+ .map-sym-item .sym-line {
567
+ color: var(--text-faint);
568
+ min-width: 32px;
569
+ text-align: right;
570
+ flex-shrink: 0;
571
+ }
478
572
  </style>
479
573
  </head>
480
574
  <body>
@@ -588,6 +682,7 @@ let pageSize = 20;
588
682
  let totalResults = 0;
589
683
  let indexed = false;
590
684
  let loading = false;
685
+ const expandCache = new Map(); // file -> { dependents, deps } for map expansion
591
686
 
592
687
  // Elements
593
688
  const repoInput = $('#repoPath');
@@ -786,10 +881,11 @@ btnIndex.onclick = async () => {
786
881
 
787
882
  addRecentRepo(root);
788
883
  indexed = true;
884
+ expandCache.clear();
789
885
  showStats(data.stats, data.elapsed);
790
886
  await loadPickerData();
791
- resultsEl.innerHTML = `<div class="empty-state"><p>Indexed <strong>${data.stats.files}</strong> files, <strong>${data.stats.symbols}</strong> symbols in ${data.elapsed}ms.<br>Start typing to search.</p></div>`;
792
- queryInput.focus();
887
+ // Auto-switch to Map tab to show the ranked overview
888
+ document.querySelector('[data-tool="map"]').click();
793
889
  } catch (e) {
794
890
  resultsEl.innerHTML = `<div class="error-msg">${esc(e.message)}</div>`;
795
891
  } finally {
@@ -991,7 +1087,7 @@ async function runQuery() {
991
1087
  if (kindFilter.value) url += `&kind=${kindFilter.value}`;
992
1088
  break;
993
1089
  case 'map':
994
- url = `${API}/api/map?offset=${offset}&limit=${pageSize}`;
1090
+ url = `${API}/api/map?offset=${offset}&limit=${pageSize}&format=structured`;
995
1091
  if (query) url += `&focus=${enc(query)}`;
996
1092
  break;
997
1093
  case 'symbols':
@@ -1098,34 +1194,138 @@ function renderMap(data) {
1098
1194
  totalResults = data.totalSymbols || 0;
1099
1195
  resultCount.textContent = `${data.shownSymbols} of ${data.totalSymbols} symbols across ${data.shownFiles} of ${data.totalFiles} files`;
1100
1196
 
1101
- if (!data.content || data.content === '(empty index)') {
1197
+ if (!data.files || data.files.length === 0) {
1102
1198
  resultsEl.innerHTML = '<div class="empty-state"><p>Empty index.</p></div>';
1103
1199
  pagination.style.display = 'none';
1104
1200
  return;
1105
1201
  }
1106
1202
 
1107
- // Parse map output into structured cards
1108
- const lines = data.content.split('\n');
1203
+ resultsEl.innerHTML = data.files.map(f => {
1204
+ const symsHtml = f.symbols.map(s =>
1205
+ `<div class="result-sig" style="font-size:12px">` +
1206
+ `<span class="result-kind kind-${s.kind || 'variable'}" style="font-size:9px">${s.kind || '?'}</span>` +
1207
+ `<span class="result-line">${s.lineStart}</span> ${highlightSig(s.signature || s.name)}` +
1208
+ `</div>`
1209
+ ).join('');
1210
+
1211
+ return `<div class="map-card" data-file="${esc(f.path)}">` +
1212
+ `<div class="map-card-header">` +
1213
+ `<span class="map-chevron">&#x25B8;</span>` +
1214
+ `<a class="result-file" href="#" onclick="openFile('${escJs(f.path)}'); event.stopPropagation(); return false" title="Open in editor">${esc(f.path)}</a>` +
1215
+ `</div>` +
1216
+ `<div class="map-symbols">${symsHtml}</div>` +
1217
+ `<div class="map-expand"></div>` +
1218
+ `</div>`;
1219
+ }).join('');
1220
+
1221
+ // Attach expand/collapse handlers via delegation
1222
+ resultsEl.querySelectorAll('.map-card-header').forEach(header => {
1223
+ header.onclick = (e) => {
1224
+ if (e.target.closest('.result-file')) return;
1225
+ const card = header.closest('.map-card');
1226
+ toggleMapExpand(card);
1227
+ };
1228
+ });
1229
+
1230
+ updatePagination();
1231
+ }
1232
+
1233
+ async function toggleMapExpand(card) {
1234
+ const file = card.dataset.file;
1235
+ const expandEl = card.querySelector('.map-expand');
1236
+ const isExpanded = card.classList.contains('expanded');
1237
+
1238
+ if (isExpanded) {
1239
+ card.classList.remove('expanded');
1240
+ return;
1241
+ }
1242
+
1243
+ card.classList.add('expanded');
1244
+
1245
+ // Check cache
1246
+ if (expandCache.has(file)) {
1247
+ renderExpandContent(expandEl, expandCache.get(file));
1248
+ return;
1249
+ }
1250
+
1251
+ expandEl.innerHTML = '<div class="map-expand-loading">Loading dependencies...</div>';
1252
+
1253
+ try {
1254
+ const [depsRes, dependentsRes, symbolsRes] = await Promise.all([
1255
+ fetch(`${API}/api/deps?file=${enc(file)}&limit=15`).then(r => r.json()),
1256
+ fetch(`${API}/api/dependents?file=${enc(file)}&limit=15`).then(r => r.json()),
1257
+ fetch(`${API}/api/symbols?file=${enc(file)}&limit=500`).then(r => r.json()),
1258
+ ]);
1259
+
1260
+ const data = {
1261
+ dependents: dependentsRes.results || [],
1262
+ dependentsTotal: dependentsRes.total || 0,
1263
+ deps: depsRes.results || [],
1264
+ depsTotal: depsRes.total || 0,
1265
+ symbols: (symbolsRes.results || []).sort((a, b) => a.lineStart - b.lineStart),
1266
+ };
1267
+ expandCache.set(file, data);
1268
+ renderExpandContent(expandEl, data);
1269
+ } catch (e) {
1270
+ expandEl.innerHTML = `<div class="error-msg">${esc(e.message)}</div>`;
1271
+ }
1272
+ }
1273
+
1274
+ function renderExpandContent(el, data) {
1109
1275
  let html = '';
1110
- let currentFile = null;
1111
-
1112
- for (const line of lines) {
1113
- if (line.endsWith(':') && !line.startsWith(' ')) {
1114
- currentFile = line.slice(0, -1);
1115
- html += `<div class="result-card" style="padding-bottom:4px"><a class="result-file" href="#" onclick="openFile('${currentFile}'); return false" title="Open in editor">${esc(currentFile)}</a>`;
1116
- } else if (line.trim() === '') {
1117
- if (currentFile) { html += '</div>'; currentFile = null; }
1118
- } else if (currentFile) {
1119
- const match = line.match(/^\s*(\d+)\│\s*(.*)$/);
1120
- if (match) {
1121
- html += `<div class="result-sig" style="margin-left:12px;font-size:12px"><span class="result-line">${match[1]}</span> ${highlightSig(match[2])}</div>`;
1122
- }
1276
+
1277
+ // Imported by (dependents)
1278
+ html += '<div class="map-expand-section">';
1279
+ html += `<div class="map-expand-title"><span class="arrow">&larr;</span> Imported by (${data.dependentsTotal})</div>`;
1280
+ if (data.dependents.length === 0) {
1281
+ html += '<div class="map-expand-empty">No dependents</div>';
1282
+ } else {
1283
+ for (const f of data.dependents) {
1284
+ const fp = typeof f === 'string' ? f : f.file;
1285
+ html += `<div class="map-dep-item" onclick="openFile('${escJs(fp)}')">${esc(fp)}</div>`;
1286
+ }
1287
+ if (data.dependentsTotal > data.dependents.length) {
1288
+ html += `<div class="map-expand-empty">...and ${data.dependentsTotal - data.dependents.length} more</div>`;
1123
1289
  }
1124
1290
  }
1125
- if (currentFile) html += '</div>';
1291
+ html += '</div>';
1126
1292
 
1127
- resultsEl.innerHTML = html;
1128
- updatePagination();
1293
+ // Depends on (deps)
1294
+ html += '<div class="map-expand-section">';
1295
+ html += `<div class="map-expand-title"><span class="arrow">&rarr;</span> Depends on (${data.depsTotal})</div>`;
1296
+ if (data.deps.length === 0) {
1297
+ html += '<div class="map-expand-empty">No dependencies</div>';
1298
+ } else {
1299
+ for (const f of data.deps) {
1300
+ const fp = typeof f === 'string' ? f : f.file;
1301
+ html += `<div class="map-dep-item" onclick="openFile('${escJs(fp)}')">${esc(fp)}</div>`;
1302
+ }
1303
+ if (data.depsTotal > data.deps.length) {
1304
+ html += `<div class="map-expand-empty">...and ${data.depsTotal - data.deps.length} more</div>`;
1305
+ }
1306
+ }
1307
+ html += '</div>';
1308
+
1309
+ // All symbols in source order
1310
+ if (data.symbols && data.symbols.length > 0) {
1311
+ html += '<div class="map-expand-section">';
1312
+ html += `<div class="map-expand-title">All symbols (${data.symbols.length})</div>`;
1313
+ for (const s of data.symbols) {
1314
+ html += `<div class="map-sym-item" onclick="openFile('${escJs(s.file)}', ${s.lineStart})">` +
1315
+ `<span class="result-kind kind-${s.kind || 'variable'}">${s.kind || '?'}</span>` +
1316
+ `<span class="sym-line">${s.lineStart}</span> ${esc(s.signature || s.name)}` +
1317
+ `</div>`;
1318
+ }
1319
+ html += '</div>';
1320
+ }
1321
+
1322
+ el.innerHTML = html;
1323
+ }
1324
+
1325
+ // Escape for use in JS string literals (single-quoted onclick handlers)
1326
+ function escJs(s) {
1327
+ if (!s) return '';
1328
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
1129
1329
  }
1130
1330
 
1131
1331
  function renderNeighborhood(data) {