@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 +8 -6
- package/src/cache.js +68 -16
- package/src/index.js +38 -8
- package/src/parser.js +27 -11
- package/src/server.js +2 -0
- package/src/ui.html +222 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
//
|
|
27
|
+
// ── JS / Node ──────────────────────────────────────────
|
|
28
28
|
'**/node_modules/**',
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'**/
|
|
29
|
+
'**/.npm/**',
|
|
30
|
+
'**/.yarn/**',
|
|
31
|
+
'**/.pnp.*',
|
|
32
|
+
'**/bower_components/**',
|
|
33
33
|
'**/*.min.js',
|
|
34
34
|
'**/*.bundle.js',
|
|
35
35
|
'**/*.map',
|
|
36
36
|
|
|
37
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
|
|
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.
|
|
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
|
-
|
|
1108
|
-
|
|
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">▸</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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1276
|
+
|
|
1277
|
+
// Imported by (dependents)
|
|
1278
|
+
html += '<div class="map-expand-section">';
|
|
1279
|
+
html += `<div class="map-expand-title"><span class="arrow">←</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
|
-
|
|
1291
|
+
html += '</div>';
|
|
1126
1292
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1293
|
+
// Depends on (deps)
|
|
1294
|
+
html += '<div class="map-expand-section">';
|
|
1295
|
+
html += `<div class="map-expand-title"><span class="arrow">→</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) {
|