@mishasinitcyn/betterrank 0.1.6 → 0.1.8
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 +1 -1
- package/src/cache.js +85 -20
- package/src/cli.js +221 -8
- package/src/index.js +137 -14
- package/src/server.js +11 -0
- package/src/ui.html +652 -28
package/src/ui.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>betterrank</title>
|
|
7
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
9
10
|
--bg: #0c0c0c;
|
|
@@ -475,6 +476,180 @@
|
|
|
475
476
|
border-color: var(--accent);
|
|
476
477
|
background: rgba(37, 99, 235, 0.08);
|
|
477
478
|
}
|
|
479
|
+
|
|
480
|
+
/* Expandable map cards */
|
|
481
|
+
.map-card {
|
|
482
|
+
border-bottom: 1px solid var(--border);
|
|
483
|
+
}
|
|
484
|
+
.map-card-header {
|
|
485
|
+
display: flex;
|
|
486
|
+
align-items: center;
|
|
487
|
+
gap: 8px;
|
|
488
|
+
padding: 12px 16px 4px;
|
|
489
|
+
cursor: pointer;
|
|
490
|
+
border-radius: 6px 6px 0 0;
|
|
491
|
+
transition: background 0.1s;
|
|
492
|
+
}
|
|
493
|
+
.map-card-header:hover { background: var(--bg-raised); }
|
|
494
|
+
.map-chevron {
|
|
495
|
+
font-size: 10px;
|
|
496
|
+
color: var(--text-faint);
|
|
497
|
+
transition: transform 0.2s ease;
|
|
498
|
+
flex-shrink: 0;
|
|
499
|
+
width: 14px;
|
|
500
|
+
text-align: center;
|
|
501
|
+
user-select: none;
|
|
502
|
+
}
|
|
503
|
+
.map-card.expanded .map-chevron { transform: rotate(90deg); }
|
|
504
|
+
.map-symbols {
|
|
505
|
+
padding: 2px 16px 10px 38px;
|
|
506
|
+
}
|
|
507
|
+
.map-expand {
|
|
508
|
+
display: none;
|
|
509
|
+
padding: 10px 16px 14px 38px;
|
|
510
|
+
background: var(--bg-raised);
|
|
511
|
+
margin: 0 8px 8px;
|
|
512
|
+
border-radius: 6px;
|
|
513
|
+
}
|
|
514
|
+
.map-card.expanded .map-expand { display: block; }
|
|
515
|
+
.map-expand-section {
|
|
516
|
+
margin-bottom: 10px;
|
|
517
|
+
}
|
|
518
|
+
.map-expand-section:last-child { margin-bottom: 0; }
|
|
519
|
+
.map-expand-title {
|
|
520
|
+
font-family: var(--mono);
|
|
521
|
+
font-size: 11px;
|
|
522
|
+
font-weight: 600;
|
|
523
|
+
letter-spacing: 0.3px;
|
|
524
|
+
color: var(--text-dim);
|
|
525
|
+
margin-bottom: 4px;
|
|
526
|
+
}
|
|
527
|
+
.map-expand-title .arrow { margin-right: 2px; }
|
|
528
|
+
.map-dep-item {
|
|
529
|
+
font-family: var(--mono);
|
|
530
|
+
font-size: 12px;
|
|
531
|
+
padding: 2px 0;
|
|
532
|
+
color: var(--accent);
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
transition: color 0.1s;
|
|
535
|
+
}
|
|
536
|
+
.map-dep-item:hover { text-decoration: underline; }
|
|
537
|
+
.map-expand-empty {
|
|
538
|
+
font-family: var(--mono);
|
|
539
|
+
font-size: 11px;
|
|
540
|
+
color: var(--text-faint);
|
|
541
|
+
font-style: italic;
|
|
542
|
+
padding: 2px 0;
|
|
543
|
+
}
|
|
544
|
+
.map-expand-loading {
|
|
545
|
+
font-family: var(--mono);
|
|
546
|
+
font-size: 11px;
|
|
547
|
+
color: var(--text-dim);
|
|
548
|
+
padding: 4px 0;
|
|
549
|
+
}
|
|
550
|
+
.map-sym-item {
|
|
551
|
+
font-family: var(--mono);
|
|
552
|
+
font-size: 12px;
|
|
553
|
+
padding: 2px 0;
|
|
554
|
+
color: var(--text-dim);
|
|
555
|
+
cursor: pointer;
|
|
556
|
+
transition: color 0.1s;
|
|
557
|
+
display: flex;
|
|
558
|
+
align-items: baseline;
|
|
559
|
+
gap: 6px;
|
|
560
|
+
}
|
|
561
|
+
.map-sym-item:hover { color: var(--text); }
|
|
562
|
+
.map-sym-item .result-kind {
|
|
563
|
+
opacity: 0.55;
|
|
564
|
+
font-size: 9px;
|
|
565
|
+
flex-shrink: 0;
|
|
566
|
+
}
|
|
567
|
+
.map-sym-item .sym-line {
|
|
568
|
+
color: var(--text-faint);
|
|
569
|
+
min-width: 32px;
|
|
570
|
+
text-align: right;
|
|
571
|
+
flex-shrink: 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* Graph view — inline within results area */
|
|
575
|
+
.graph-wrap {
|
|
576
|
+
position: relative;
|
|
577
|
+
height: calc(100vh - 280px);
|
|
578
|
+
min-height: 400px;
|
|
579
|
+
background: var(--bg);
|
|
580
|
+
border: 1px solid var(--border);
|
|
581
|
+
border-radius: var(--radius);
|
|
582
|
+
overflow: hidden;
|
|
583
|
+
}
|
|
584
|
+
.graph-wrap svg { width: 100%; height: 100%; display: block; }
|
|
585
|
+
.graph-filters {
|
|
586
|
+
position: absolute; top: 12px; right: 12px; z-index: 10;
|
|
587
|
+
background: rgba(22,22,22,0.9); backdrop-filter: blur(8px);
|
|
588
|
+
border: 1px solid var(--border); border-radius: 10px;
|
|
589
|
+
padding: 10px 12px; max-width: 240px;
|
|
590
|
+
}
|
|
591
|
+
.graph-filters-title { font-size: 10px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
|
592
|
+
.graph-filter-badges { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
593
|
+
.graph-badge {
|
|
594
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
595
|
+
padding: 3px 10px; border-radius: 999px;
|
|
596
|
+
font-size: 11px; font-weight: 500; cursor: pointer;
|
|
597
|
+
border: 1px solid var(--border); background: transparent;
|
|
598
|
+
color: var(--text-dim); transition: all 0.15s; user-select: none;
|
|
599
|
+
}
|
|
600
|
+
.graph-badge.active { background: var(--bg-raised); color: var(--text); }
|
|
601
|
+
.graph-badge .dot { width: 7px; height: 7px; border-radius: 50%; }
|
|
602
|
+
.graph-badge:hover { border-color: #3f3f46; }
|
|
603
|
+
.graph-detail {
|
|
604
|
+
position: absolute; bottom: 12px; left: 12px; z-index: 10;
|
|
605
|
+
background: rgba(22,22,22,0.95); backdrop-filter: blur(8px);
|
|
606
|
+
border: 1px solid var(--border); border-radius: 10px;
|
|
607
|
+
padding: 14px 18px; min-width: 260px; max-width: 340px;
|
|
608
|
+
display: none;
|
|
609
|
+
}
|
|
610
|
+
.graph-detail.visible { display: block; }
|
|
611
|
+
.graph-detail-cat { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
612
|
+
.graph-detail-name { font-size: 15px; font-weight: 600; margin-bottom: 2px; color: var(--text); }
|
|
613
|
+
.graph-detail-path { font-size: 11px; color: var(--text-faint); font-family: var(--mono); margin-bottom: 10px; cursor: pointer; }
|
|
614
|
+
.graph-detail-path:hover { color: var(--accent); text-decoration: underline; }
|
|
615
|
+
.graph-detail-section { margin-top: 8px; }
|
|
616
|
+
.graph-detail-section-title { font-size: 10px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
|
|
617
|
+
.graph-detail-item { font-size: 11px; color: var(--text-dim); padding: 2px 0; display: flex; align-items: center; gap: 6px; }
|
|
618
|
+
.graph-detail-item .dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
|
619
|
+
.graph-tooltip {
|
|
620
|
+
position: fixed; z-index: 70; pointer-events: none;
|
|
621
|
+
background: var(--bg-raised); border: 1px solid var(--border); border-radius: 6px;
|
|
622
|
+
padding: 6px 10px; font-size: 11px; font-family: var(--mono); display: none;
|
|
623
|
+
color: var(--text); box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
624
|
+
}
|
|
625
|
+
.graph-search {
|
|
626
|
+
position: absolute; bottom: 12px; right: 12px; z-index: 10;
|
|
627
|
+
background: rgba(22,22,22,0.95); backdrop-filter: blur(8px);
|
|
628
|
+
border: 1px solid var(--border); border-radius: 10px;
|
|
629
|
+
padding: 10px 12px; width: 280px;
|
|
630
|
+
}
|
|
631
|
+
.graph-search input {
|
|
632
|
+
width: 100%; background: var(--bg-raised); border: 1px solid var(--border); border-radius: 6px;
|
|
633
|
+
padding: 7px 10px; font-size: 13px; color: var(--text); outline: none;
|
|
634
|
+
font-family: var(--mono);
|
|
635
|
+
}
|
|
636
|
+
.graph-search input::placeholder { color: var(--text-faint); }
|
|
637
|
+
.graph-search input:focus { border-color: var(--accent-dim); }
|
|
638
|
+
.graph-search-results { max-height: 200px; overflow-y: auto; margin-top: 8px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
|
|
639
|
+
.graph-search-results:empty { display: none; }
|
|
640
|
+
.graph-search-result {
|
|
641
|
+
padding: 6px 8px; border-radius: 6px; cursor: pointer;
|
|
642
|
+
transition: background 0.1s; font-size: 12px; font-family: var(--mono);
|
|
643
|
+
color: var(--text-dim);
|
|
644
|
+
}
|
|
645
|
+
.graph-search-result:hover { background: var(--bg-hover); color: var(--text); }
|
|
646
|
+
.graph-link { stroke-opacity: 0.15; }
|
|
647
|
+
.graph-link.highlighted { stroke-opacity: 0.7; stroke-width: 2px !important; }
|
|
648
|
+
.graph-node circle { stroke-width: 1.5; cursor: pointer; transition: r 0.15s; }
|
|
649
|
+
.graph-node text { fill: var(--text-dim); font-size: 9px; pointer-events: none; font-family: var(--mono); }
|
|
650
|
+
.graph-node.dimmed circle { opacity: 0.15; }
|
|
651
|
+
.graph-node.dimmed text { opacity: 0.1; }
|
|
652
|
+
.graph-node.highlighted circle { stroke-width: 3; }
|
|
478
653
|
</style>
|
|
479
654
|
</head>
|
|
480
655
|
<body>
|
|
@@ -503,9 +678,9 @@
|
|
|
503
678
|
</div>
|
|
504
679
|
</div>
|
|
505
680
|
|
|
506
|
-
<!--
|
|
681
|
+
<!-- Indexed repos -->
|
|
507
682
|
<div class="recent-repos" id="recentRepos" style="display:none">
|
|
508
|
-
<div class="recent-label">
|
|
683
|
+
<div class="recent-label">Indexed</div>
|
|
509
684
|
<div class="recent-list" id="recentList"></div>
|
|
510
685
|
</div>
|
|
511
686
|
|
|
@@ -531,6 +706,7 @@
|
|
|
531
706
|
<button class="tab" data-tool="dependents">Dependents</button>
|
|
532
707
|
<button class="tab" data-tool="neighborhood">Neighborhood</button>
|
|
533
708
|
<button class="tab" data-tool="structure">Structure</button>
|
|
709
|
+
<button class="tab" data-tool="graph">Graph</button>
|
|
534
710
|
</div>
|
|
535
711
|
|
|
536
712
|
<!-- Search area -->
|
|
@@ -559,9 +735,9 @@
|
|
|
559
735
|
|
|
560
736
|
<!-- Results -->
|
|
561
737
|
<div class="results" id="results">
|
|
562
|
-
<div class="empty-state">
|
|
738
|
+
<div class="empty-state" id="emptyState">
|
|
563
739
|
<div class="icon">📂</div>
|
|
564
|
-
<p>Drop a folder here, click <strong>Browse</strong>, or type a path above.<br>
|
|
740
|
+
<p id="emptyMsg">Drop a folder here, click <strong>Browse</strong>, or type a path above.<br>
|
|
565
741
|
Then search symbols, explore the map, or trace callers.<br><br>
|
|
566
742
|
<kbd>⌘O</kbd> browse <kbd>⌘K</kbd> focus search</p>
|
|
567
743
|
</div>
|
|
@@ -576,6 +752,9 @@
|
|
|
576
752
|
|
|
577
753
|
</div>
|
|
578
754
|
|
|
755
|
+
<!-- Graph tooltip (needs to be outside .app for fixed positioning) -->
|
|
756
|
+
<div class="graph-tooltip" id="graphTooltip"></div>
|
|
757
|
+
|
|
579
758
|
<script>
|
|
580
759
|
const $ = s => document.querySelector(s);
|
|
581
760
|
const $$ = s => document.querySelectorAll(s);
|
|
@@ -588,6 +767,7 @@ let pageSize = 20;
|
|
|
588
767
|
let totalResults = 0;
|
|
589
768
|
let indexed = false;
|
|
590
769
|
let loading = false;
|
|
770
|
+
const expandCache = new Map(); // file -> { dependents, deps } for map expansion
|
|
591
771
|
|
|
592
772
|
// Elements
|
|
593
773
|
const repoInput = $('#repoPath');
|
|
@@ -627,11 +807,13 @@ function removeRecentRepo(path) {
|
|
|
627
807
|
const recent = getRecentRepos().filter(r => r !== path);
|
|
628
808
|
localStorage.setItem(RECENT_KEY, JSON.stringify(recent));
|
|
629
809
|
renderRecentRepos();
|
|
810
|
+
updateEmptyState();
|
|
630
811
|
}
|
|
631
812
|
|
|
632
813
|
function clearRecentRepos() {
|
|
633
814
|
localStorage.removeItem(RECENT_KEY);
|
|
634
815
|
renderRecentRepos();
|
|
816
|
+
updateEmptyState();
|
|
635
817
|
}
|
|
636
818
|
|
|
637
819
|
function renderRecentRepos() {
|
|
@@ -786,10 +968,11 @@ btnIndex.onclick = async () => {
|
|
|
786
968
|
|
|
787
969
|
addRecentRepo(root);
|
|
788
970
|
indexed = true;
|
|
971
|
+
expandCache.clear();
|
|
789
972
|
showStats(data.stats, data.elapsed);
|
|
790
973
|
await loadPickerData();
|
|
791
|
-
|
|
792
|
-
|
|
974
|
+
// Auto-switch to Map tab to show the ranked overview
|
|
975
|
+
document.querySelector('[data-tool="map"]').click();
|
|
793
976
|
} catch (e) {
|
|
794
977
|
resultsEl.innerHTML = `<div class="error-msg">${esc(e.message)}</div>`;
|
|
795
978
|
} finally {
|
|
@@ -917,9 +1100,15 @@ $$('.tab').forEach(tab => {
|
|
|
917
1100
|
tab.classList.add('active');
|
|
918
1101
|
currentTool = tab.dataset.tool;
|
|
919
1102
|
currentPage = 0;
|
|
1103
|
+
|
|
1104
|
+
// Stop any running graph simulation when leaving graph tab
|
|
1105
|
+
if (currentTool !== 'graph' && graphSimulation) {
|
|
1106
|
+
graphSimulation.stop();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
920
1109
|
updateSearchUI();
|
|
921
|
-
// Auto-run: map/structure run server query, others show pickers or run
|
|
922
|
-
if (['map', 'structure', 'symbols'].includes(currentTool)) {
|
|
1110
|
+
// Auto-run: map/structure/graph run server query, others show pickers or run
|
|
1111
|
+
if (['map', 'structure', 'symbols', 'graph'].includes(currentTool)) {
|
|
923
1112
|
runQuery();
|
|
924
1113
|
} else {
|
|
925
1114
|
queryInput.value = '';
|
|
@@ -934,6 +1123,7 @@ function updateSearchUI() {
|
|
|
934
1123
|
const needsKind = ['search', 'symbols'].includes(currentTool);
|
|
935
1124
|
|
|
936
1125
|
$('#searchArea').style.display = needsQuery ? 'block' : 'none';
|
|
1126
|
+
pagination.style.display = currentTool === 'graph' ? 'none' : pagination.style.display;
|
|
937
1127
|
kindFilter.style.display = needsKind ? 'inline-block' : 'none';
|
|
938
1128
|
|
|
939
1129
|
const placeholders = {
|
|
@@ -991,7 +1181,7 @@ async function runQuery() {
|
|
|
991
1181
|
if (kindFilter.value) url += `&kind=${kindFilter.value}`;
|
|
992
1182
|
break;
|
|
993
1183
|
case 'map':
|
|
994
|
-
url = `${API}/api/map?offset=${offset}&limit=${pageSize}`;
|
|
1184
|
+
url = `${API}/api/map?offset=${offset}&limit=${pageSize}&format=structured`;
|
|
995
1185
|
if (query) url += `&focus=${enc(query)}`;
|
|
996
1186
|
break;
|
|
997
1187
|
case 'symbols':
|
|
@@ -1022,6 +1212,9 @@ async function runQuery() {
|
|
|
1022
1212
|
case 'structure':
|
|
1023
1213
|
url = `${API}/api/structure?depth=4`;
|
|
1024
1214
|
break;
|
|
1215
|
+
case 'graph':
|
|
1216
|
+
url = `${API}/api/graph?limit=500`;
|
|
1217
|
+
break;
|
|
1025
1218
|
}
|
|
1026
1219
|
|
|
1027
1220
|
const res = await fetch(url);
|
|
@@ -1048,6 +1241,7 @@ function render(data) {
|
|
|
1048
1241
|
case 'dependents': return renderFileList(data.results, data.total);
|
|
1049
1242
|
case 'neighborhood': return renderNeighborhood(data);
|
|
1050
1243
|
case 'structure': return renderPre(data.content);
|
|
1244
|
+
case 'graph': return renderGraph(data);
|
|
1051
1245
|
}
|
|
1052
1246
|
}
|
|
1053
1247
|
|
|
@@ -1098,34 +1292,138 @@ function renderMap(data) {
|
|
|
1098
1292
|
totalResults = data.totalSymbols || 0;
|
|
1099
1293
|
resultCount.textContent = `${data.shownSymbols} of ${data.totalSymbols} symbols across ${data.shownFiles} of ${data.totalFiles} files`;
|
|
1100
1294
|
|
|
1101
|
-
if (!data.
|
|
1295
|
+
if (!data.files || data.files.length === 0) {
|
|
1102
1296
|
resultsEl.innerHTML = '<div class="empty-state"><p>Empty index.</p></div>';
|
|
1103
1297
|
pagination.style.display = 'none';
|
|
1104
1298
|
return;
|
|
1105
1299
|
}
|
|
1106
1300
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1301
|
+
resultsEl.innerHTML = data.files.map(f => {
|
|
1302
|
+
const symsHtml = f.symbols.map(s =>
|
|
1303
|
+
`<div class="result-sig" style="font-size:12px">` +
|
|
1304
|
+
`<span class="result-kind kind-${s.kind || 'variable'}" style="font-size:9px">${s.kind || '?'}</span>` +
|
|
1305
|
+
`<span class="result-line">${s.lineStart}</span> ${highlightSig(s.signature || s.name)}` +
|
|
1306
|
+
`</div>`
|
|
1307
|
+
).join('');
|
|
1308
|
+
|
|
1309
|
+
return `<div class="map-card" data-file="${esc(f.path)}">` +
|
|
1310
|
+
`<div class="map-card-header">` +
|
|
1311
|
+
`<span class="map-chevron">▸</span>` +
|
|
1312
|
+
`<a class="result-file" href="#" onclick="openFile('${escJs(f.path)}'); event.stopPropagation(); return false" title="Open in editor">${esc(f.path)}</a>` +
|
|
1313
|
+
`</div>` +
|
|
1314
|
+
`<div class="map-symbols">${symsHtml}</div>` +
|
|
1315
|
+
`<div class="map-expand"></div>` +
|
|
1316
|
+
`</div>`;
|
|
1317
|
+
}).join('');
|
|
1318
|
+
|
|
1319
|
+
// Attach expand/collapse handlers via delegation
|
|
1320
|
+
resultsEl.querySelectorAll('.map-card-header').forEach(header => {
|
|
1321
|
+
header.onclick = (e) => {
|
|
1322
|
+
if (e.target.closest('.result-file')) return;
|
|
1323
|
+
const card = header.closest('.map-card');
|
|
1324
|
+
toggleMapExpand(card);
|
|
1325
|
+
};
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
updatePagination();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function toggleMapExpand(card) {
|
|
1332
|
+
const file = card.dataset.file;
|
|
1333
|
+
const expandEl = card.querySelector('.map-expand');
|
|
1334
|
+
const isExpanded = card.classList.contains('expanded');
|
|
1335
|
+
|
|
1336
|
+
if (isExpanded) {
|
|
1337
|
+
card.classList.remove('expanded');
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
card.classList.add('expanded');
|
|
1342
|
+
|
|
1343
|
+
// Check cache
|
|
1344
|
+
if (expandCache.has(file)) {
|
|
1345
|
+
renderExpandContent(expandEl, expandCache.get(file));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
expandEl.innerHTML = '<div class="map-expand-loading">Loading dependencies...</div>';
|
|
1350
|
+
|
|
1351
|
+
try {
|
|
1352
|
+
const [depsRes, dependentsRes, symbolsRes] = await Promise.all([
|
|
1353
|
+
fetch(`${API}/api/deps?file=${enc(file)}&limit=15`).then(r => r.json()),
|
|
1354
|
+
fetch(`${API}/api/dependents?file=${enc(file)}&limit=15`).then(r => r.json()),
|
|
1355
|
+
fetch(`${API}/api/symbols?file=${enc(file)}&limit=500`).then(r => r.json()),
|
|
1356
|
+
]);
|
|
1357
|
+
|
|
1358
|
+
const data = {
|
|
1359
|
+
dependents: dependentsRes.results || [],
|
|
1360
|
+
dependentsTotal: dependentsRes.total || 0,
|
|
1361
|
+
deps: depsRes.results || [],
|
|
1362
|
+
depsTotal: depsRes.total || 0,
|
|
1363
|
+
symbols: (symbolsRes.results || []).sort((a, b) => a.lineStart - b.lineStart),
|
|
1364
|
+
};
|
|
1365
|
+
expandCache.set(file, data);
|
|
1366
|
+
renderExpandContent(expandEl, data);
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
expandEl.innerHTML = `<div class="error-msg">${esc(e.message)}</div>`;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function renderExpandContent(el, data) {
|
|
1109
1373
|
let html = '';
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1374
|
+
|
|
1375
|
+
// Imported by (dependents)
|
|
1376
|
+
html += '<div class="map-expand-section">';
|
|
1377
|
+
html += `<div class="map-expand-title"><span class="arrow">←</span> Imported by (${data.dependentsTotal})</div>`;
|
|
1378
|
+
if (data.dependents.length === 0) {
|
|
1379
|
+
html += '<div class="map-expand-empty">No dependents</div>';
|
|
1380
|
+
} else {
|
|
1381
|
+
for (const f of data.dependents) {
|
|
1382
|
+
const fp = typeof f === 'string' ? f : f.file;
|
|
1383
|
+
html += `<div class="map-dep-item" onclick="openFile('${escJs(fp)}')">${esc(fp)}</div>`;
|
|
1384
|
+
}
|
|
1385
|
+
if (data.dependentsTotal > data.dependents.length) {
|
|
1386
|
+
html += `<div class="map-expand-empty">...and ${data.dependentsTotal - data.dependents.length} more</div>`;
|
|
1123
1387
|
}
|
|
1124
1388
|
}
|
|
1125
|
-
|
|
1389
|
+
html += '</div>';
|
|
1126
1390
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1391
|
+
// Depends on (deps)
|
|
1392
|
+
html += '<div class="map-expand-section">';
|
|
1393
|
+
html += `<div class="map-expand-title"><span class="arrow">→</span> Depends on (${data.depsTotal})</div>`;
|
|
1394
|
+
if (data.deps.length === 0) {
|
|
1395
|
+
html += '<div class="map-expand-empty">No dependencies</div>';
|
|
1396
|
+
} else {
|
|
1397
|
+
for (const f of data.deps) {
|
|
1398
|
+
const fp = typeof f === 'string' ? f : f.file;
|
|
1399
|
+
html += `<div class="map-dep-item" onclick="openFile('${escJs(fp)}')">${esc(fp)}</div>`;
|
|
1400
|
+
}
|
|
1401
|
+
if (data.depsTotal > data.deps.length) {
|
|
1402
|
+
html += `<div class="map-expand-empty">...and ${data.depsTotal - data.deps.length} more</div>`;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
html += '</div>';
|
|
1406
|
+
|
|
1407
|
+
// All symbols in source order
|
|
1408
|
+
if (data.symbols && data.symbols.length > 0) {
|
|
1409
|
+
html += '<div class="map-expand-section">';
|
|
1410
|
+
html += `<div class="map-expand-title">All symbols (${data.symbols.length})</div>`;
|
|
1411
|
+
for (const s of data.symbols) {
|
|
1412
|
+
html += `<div class="map-sym-item" onclick="openFile('${escJs(s.file)}', ${s.lineStart})">` +
|
|
1413
|
+
`<span class="result-kind kind-${s.kind || 'variable'}">${s.kind || '?'}</span>` +
|
|
1414
|
+
`<span class="sym-line">${s.lineStart}</span> ${esc(s.signature || s.name)}` +
|
|
1415
|
+
`</div>`;
|
|
1416
|
+
}
|
|
1417
|
+
html += '</div>';
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
el.innerHTML = html;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Escape for use in JS string literals (single-quoted onclick handlers)
|
|
1424
|
+
function escJs(s) {
|
|
1425
|
+
if (!s) return '';
|
|
1426
|
+
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
1129
1427
|
}
|
|
1130
1428
|
|
|
1131
1429
|
function renderNeighborhood(data) {
|
|
@@ -1244,6 +1542,332 @@ document.addEventListener('keydown', (e) => {
|
|
|
1244
1542
|
repoInput.onkeydown = (e) => {
|
|
1245
1543
|
if (e.key === 'Enter') btnIndex.click();
|
|
1246
1544
|
};
|
|
1545
|
+
|
|
1546
|
+
// --- Graph visualization (inline) ---
|
|
1547
|
+
const graphTooltip = $('#graphTooltip');
|
|
1548
|
+
|
|
1549
|
+
const GRAPH_PALETTE = [
|
|
1550
|
+
'#3b82f6', '#a855f7', '#06b6d4', '#22c55e', '#f59e0b',
|
|
1551
|
+
'#14b8a6', '#ec4899', '#eab308', '#f97316', '#f43f5e',
|
|
1552
|
+
'#6366f1', '#84cc16', '#0ea5e9', '#d946ef', '#fb7185',
|
|
1553
|
+
];
|
|
1554
|
+
let graphCategoryColors = {};
|
|
1555
|
+
let graphData = null;
|
|
1556
|
+
let graphSimulation = null;
|
|
1557
|
+
let graphActiveCategories = new Set();
|
|
1558
|
+
let graphG = null;
|
|
1559
|
+
let graphZoom = null;
|
|
1560
|
+
let graphLinkGroup = null;
|
|
1561
|
+
let graphNodeGroup = null;
|
|
1562
|
+
|
|
1563
|
+
function getCategoryColor(cat) {
|
|
1564
|
+
if (!graphCategoryColors[cat]) {
|
|
1565
|
+
const idx = Object.keys(graphCategoryColors).length % GRAPH_PALETTE.length;
|
|
1566
|
+
graphCategoryColors[cat] = GRAPH_PALETTE[idx];
|
|
1567
|
+
}
|
|
1568
|
+
return graphCategoryColors[cat];
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function renderGraph(data) {
|
|
1572
|
+
totalResults = 0;
|
|
1573
|
+
pagination.style.display = 'none';
|
|
1574
|
+
graphData = data;
|
|
1575
|
+
|
|
1576
|
+
if (!data.nodes || data.nodes.length === 0) {
|
|
1577
|
+
resultsEl.innerHTML = '<div class="empty-state"><p>No graph data. Index a repository first.</p></div>';
|
|
1578
|
+
resultCount.textContent = '';
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
resultCount.textContent = `${data.nodes.length} modules, ${data.edges.length} edges`;
|
|
1583
|
+
|
|
1584
|
+
// Assign category colors
|
|
1585
|
+
graphCategoryColors = {};
|
|
1586
|
+
const categories = [...new Set(data.nodes.map(n => n.category))].sort();
|
|
1587
|
+
categories.forEach(c => getCategoryColor(c));
|
|
1588
|
+
graphActiveCategories = new Set(categories);
|
|
1589
|
+
|
|
1590
|
+
// Build the inline graph HTML
|
|
1591
|
+
resultsEl.innerHTML = `
|
|
1592
|
+
<div class="graph-wrap" id="graphWrap">
|
|
1593
|
+
<div class="graph-filters">
|
|
1594
|
+
<div class="graph-filters-title">Categories</div>
|
|
1595
|
+
<div class="graph-filter-badges" id="graphFilterBadges"></div>
|
|
1596
|
+
</div>
|
|
1597
|
+
<div class="graph-detail" id="graphDetail">
|
|
1598
|
+
<div class="graph-detail-cat" id="graphDetailCat"></div>
|
|
1599
|
+
<div class="graph-detail-name" id="graphDetailName"></div>
|
|
1600
|
+
<div class="graph-detail-path" id="graphDetailPath"></div>
|
|
1601
|
+
<div class="graph-detail-section">
|
|
1602
|
+
<div class="graph-detail-section-title">Imports (<span id="graphDetailImportsCount">0</span>)</div>
|
|
1603
|
+
<div id="graphDetailImports"></div>
|
|
1604
|
+
</div>
|
|
1605
|
+
<div class="graph-detail-section">
|
|
1606
|
+
<div class="graph-detail-section-title">Imported By (<span id="graphDetailImportedByCount">0</span>)</div>
|
|
1607
|
+
<div id="graphDetailImportedBy"></div>
|
|
1608
|
+
</div>
|
|
1609
|
+
</div>
|
|
1610
|
+
<div class="graph-search">
|
|
1611
|
+
<input type="text" id="graphSearchInput" placeholder="Search files..." autocomplete="off" spellcheck="false" />
|
|
1612
|
+
<div class="graph-search-results" id="graphSearchResults"></div>
|
|
1613
|
+
</div>
|
|
1614
|
+
<svg id="graphSvg"></svg>
|
|
1615
|
+
</div>
|
|
1616
|
+
`;
|
|
1617
|
+
|
|
1618
|
+
// Build filter badges
|
|
1619
|
+
const badgeContainer = $('#graphFilterBadges');
|
|
1620
|
+
categories.forEach(cat => {
|
|
1621
|
+
const badge = document.createElement('div');
|
|
1622
|
+
badge.className = 'graph-badge active';
|
|
1623
|
+
badge.dataset.cat = cat;
|
|
1624
|
+
badge.innerHTML = `<span class="dot" style="background:${getCategoryColor(cat)}"></span>${esc(cat)}`;
|
|
1625
|
+
badge.addEventListener('click', () => {
|
|
1626
|
+
if (graphActiveCategories.has(cat)) {
|
|
1627
|
+
graphActiveCategories.delete(cat);
|
|
1628
|
+
badge.classList.remove('active');
|
|
1629
|
+
} else {
|
|
1630
|
+
graphActiveCategories.add(cat);
|
|
1631
|
+
badge.classList.add('active');
|
|
1632
|
+
}
|
|
1633
|
+
updateGraphViz();
|
|
1634
|
+
});
|
|
1635
|
+
badgeContainer.appendChild(badge);
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
// SVG setup
|
|
1639
|
+
const wrap = $('#graphWrap');
|
|
1640
|
+
const width = wrap.clientWidth;
|
|
1641
|
+
const height = wrap.clientHeight;
|
|
1642
|
+
const svg = d3.select('#graphSvg').attr('width', width).attr('height', height);
|
|
1643
|
+
|
|
1644
|
+
graphG = svg.append('g');
|
|
1645
|
+
graphZoom = d3.zoom()
|
|
1646
|
+
.scaleExtent([0.05, 5])
|
|
1647
|
+
.on('zoom', (e) => graphG.attr('transform', e.transform));
|
|
1648
|
+
svg.call(graphZoom);
|
|
1649
|
+
|
|
1650
|
+
svg.append('defs').append('marker')
|
|
1651
|
+
.attr('id', 'graph-arrow')
|
|
1652
|
+
.attr('viewBox', '0 -3 6 6')
|
|
1653
|
+
.attr('refX', 16).attr('refY', 0)
|
|
1654
|
+
.attr('markerWidth', 5).attr('markerHeight', 5)
|
|
1655
|
+
.attr('orient', 'auto')
|
|
1656
|
+
.append('path')
|
|
1657
|
+
.attr('d', 'M0,-3L6,0L0,3')
|
|
1658
|
+
.attr('fill', '#555');
|
|
1659
|
+
|
|
1660
|
+
graphLinkGroup = graphG.append('g');
|
|
1661
|
+
graphNodeGroup = graphG.append('g');
|
|
1662
|
+
|
|
1663
|
+
// Click empty space to dismiss detail
|
|
1664
|
+
svg.on('click', () => {
|
|
1665
|
+
const detail = $('#graphDetail');
|
|
1666
|
+
if (detail) detail.classList.remove('visible');
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
updateGraphViz();
|
|
1670
|
+
|
|
1671
|
+
// Fit to view after simulation settles
|
|
1672
|
+
setTimeout(() => {
|
|
1673
|
+
if (!graphG || !graphG.node()) return;
|
|
1674
|
+
const bounds = graphG.node().getBBox();
|
|
1675
|
+
if (bounds.width === 0) return;
|
|
1676
|
+
const dx = bounds.width, dy = bounds.height, x = bounds.x, y = bounds.y;
|
|
1677
|
+
const scale = 0.85 / Math.max(dx / width, dy / height);
|
|
1678
|
+
const translate = [width / 2 - scale * (x + dx / 2), height / 2 - scale * (y + dy / 2)];
|
|
1679
|
+
svg.transition().duration(750).call(
|
|
1680
|
+
graphZoom.transform,
|
|
1681
|
+
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
|
|
1682
|
+
);
|
|
1683
|
+
}, 3000);
|
|
1684
|
+
|
|
1685
|
+
// Wire up inline search
|
|
1686
|
+
const searchInput = $('#graphSearchInput');
|
|
1687
|
+
const searchResultsEl = $('#graphSearchResults');
|
|
1688
|
+
if (searchInput) {
|
|
1689
|
+
searchInput.oninput = () => {
|
|
1690
|
+
const q = searchInput.value.trim().toLowerCase();
|
|
1691
|
+
if (!q || !graphData) {
|
|
1692
|
+
searchResultsEl.innerHTML = '';
|
|
1693
|
+
graphNodeGroup.selectAll('g.graph-node').classed('dimmed', false);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
const matches = graphData.nodes.filter(n => n.id.toLowerCase().includes(q)).slice(0, 20);
|
|
1697
|
+
const matchIds = new Set(matches.map(m => m.id));
|
|
1698
|
+
graphNodeGroup.selectAll('g.graph-node').classed('dimmed', n => !matchIds.has(n.id));
|
|
1699
|
+
searchResultsEl.innerHTML = matches.map(m =>
|
|
1700
|
+
`<div class="graph-search-result" data-id="${esc(m.id)}">${esc(m.id)}</div>`
|
|
1701
|
+
).join('');
|
|
1702
|
+
searchResultsEl.querySelectorAll('.graph-search-result').forEach(el => {
|
|
1703
|
+
el.onclick = () => {
|
|
1704
|
+
const node = graphData.nodes.find(n => n.id === el.dataset.id);
|
|
1705
|
+
if (!node || node.x == null) return;
|
|
1706
|
+
const s = 1.5;
|
|
1707
|
+
const t = [width / 2 - s * node.x, height / 2 - s * node.y];
|
|
1708
|
+
svg.transition().duration(500).call(
|
|
1709
|
+
graphZoom.transform, d3.zoomIdentity.translate(t[0], t[1]).scale(s)
|
|
1710
|
+
);
|
|
1711
|
+
onGraphNodeClick({ stopPropagation: () => {} }, node);
|
|
1712
|
+
};
|
|
1713
|
+
});
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function updateGraphViz() {
|
|
1719
|
+
if (!graphData) return;
|
|
1720
|
+
const wrap = $('#graphWrap');
|
|
1721
|
+
if (!wrap) return;
|
|
1722
|
+
const width = wrap.clientWidth;
|
|
1723
|
+
const height = wrap.clientHeight;
|
|
1724
|
+
|
|
1725
|
+
const nodes = graphData.nodes.filter(n => graphActiveCategories.has(n.category));
|
|
1726
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
1727
|
+
const edges = graphData.edges.filter(e =>
|
|
1728
|
+
nodeIds.has(e.source?.id || e.source) && nodeIds.has(e.target?.id || e.target)
|
|
1729
|
+
);
|
|
1730
|
+
|
|
1731
|
+
resultCount.textContent = `${nodes.length} modules, ${edges.length} edges`;
|
|
1732
|
+
|
|
1733
|
+
// In-degree for sizing
|
|
1734
|
+
const inDegree = {};
|
|
1735
|
+
edges.forEach(e => {
|
|
1736
|
+
const tid = e.target?.id || e.target;
|
|
1737
|
+
inDegree[tid] = (inDegree[tid] || 0) + 1;
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
// Links
|
|
1741
|
+
const links = graphLinkGroup.selectAll('line').data(edges, d => `${d.source?.id||d.source}-${d.target?.id||d.target}`);
|
|
1742
|
+
links.exit().remove();
|
|
1743
|
+
links.enter().append('line')
|
|
1744
|
+
.attr('class', 'graph-link')
|
|
1745
|
+
.attr('stroke', '#555')
|
|
1746
|
+
.attr('stroke-width', 1)
|
|
1747
|
+
.attr('marker-end', 'url(#graph-arrow)');
|
|
1748
|
+
|
|
1749
|
+
// Nodes
|
|
1750
|
+
const nodeData = graphNodeGroup.selectAll('g.graph-node').data(nodes, d => d.id);
|
|
1751
|
+
nodeData.exit().remove();
|
|
1752
|
+
const nodeEnter = nodeData.enter().append('g').attr('class', 'graph-node');
|
|
1753
|
+
|
|
1754
|
+
nodeEnter.append('circle')
|
|
1755
|
+
.attr('r', d => Math.max(5, Math.min(14, 4 + (inDegree[d.id] || 0) * 1.5)))
|
|
1756
|
+
.attr('fill', d => getCategoryColor(d.category))
|
|
1757
|
+
.attr('stroke', d => d3.color(getCategoryColor(d.category)).brighter(0.5))
|
|
1758
|
+
.on('mouseover', onGraphNodeHover)
|
|
1759
|
+
.on('mouseout', onGraphNodeOut)
|
|
1760
|
+
.on('click', onGraphNodeClick);
|
|
1761
|
+
|
|
1762
|
+
nodeEnter.append('text')
|
|
1763
|
+
.attr('dx', d => Math.max(5, Math.min(14, 4 + (inDegree[d.id] || 0) * 1.5)) + 4)
|
|
1764
|
+
.attr('dy', 3)
|
|
1765
|
+
.text(d => d.label);
|
|
1766
|
+
|
|
1767
|
+
nodeData.select('circle')
|
|
1768
|
+
.attr('r', d => Math.max(5, Math.min(14, 4 + (inDegree[d.id] || 0) * 1.5)))
|
|
1769
|
+
.attr('fill', d => getCategoryColor(d.category));
|
|
1770
|
+
|
|
1771
|
+
nodeEnter.call(d3.drag()
|
|
1772
|
+
.on('start', (e, d) => { if (!e.active) graphSimulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
1773
|
+
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
|
1774
|
+
.on('end', (e, d) => { if (!e.active) graphSimulation.alphaTarget(0); d.fx = null; d.fy = null; })
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
const allLinks = graphLinkGroup.selectAll('line');
|
|
1778
|
+
const allNodes = graphNodeGroup.selectAll('g.graph-node');
|
|
1779
|
+
|
|
1780
|
+
if (graphSimulation) graphSimulation.stop();
|
|
1781
|
+
graphSimulation = d3.forceSimulation(nodes)
|
|
1782
|
+
.force('link', d3.forceLink(edges).id(d => d.id).distance(80))
|
|
1783
|
+
.force('charge', d3.forceManyBody().strength(-200))
|
|
1784
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
1785
|
+
.force('collision', d3.forceCollide(25))
|
|
1786
|
+
.on('tick', () => {
|
|
1787
|
+
allLinks
|
|
1788
|
+
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
|
1789
|
+
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
1790
|
+
allNodes.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function onGraphNodeHover(event, d) {
|
|
1795
|
+
graphTooltip.style.display = 'block';
|
|
1796
|
+
graphTooltip.style.left = (event.clientX + 12) + 'px';
|
|
1797
|
+
graphTooltip.style.top = (event.clientY - 8) + 'px';
|
|
1798
|
+
graphTooltip.textContent = d.id;
|
|
1799
|
+
|
|
1800
|
+
const connected = new Set([d.id]);
|
|
1801
|
+
graphData.edges.forEach(e => {
|
|
1802
|
+
const sid = e.source?.id || e.source;
|
|
1803
|
+
const tid = e.target?.id || e.target;
|
|
1804
|
+
if (sid === d.id) connected.add(tid);
|
|
1805
|
+
if (tid === d.id) connected.add(sid);
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
graphNodeGroup.selectAll('g.graph-node')
|
|
1809
|
+
.classed('dimmed', n => !connected.has(n.id))
|
|
1810
|
+
.classed('highlighted', n => n.id === d.id);
|
|
1811
|
+
graphLinkGroup.selectAll('line').classed('highlighted', e => {
|
|
1812
|
+
const sid = e.source?.id || e.source;
|
|
1813
|
+
const tid = e.target?.id || e.target;
|
|
1814
|
+
return sid === d.id || tid === d.id;
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
function onGraphNodeOut() {
|
|
1819
|
+
graphTooltip.style.display = 'none';
|
|
1820
|
+
graphNodeGroup.selectAll('g.graph-node').classed('dimmed', false).classed('highlighted', false);
|
|
1821
|
+
graphLinkGroup.selectAll('line').classed('highlighted', false);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function onGraphNodeClick(event, d) {
|
|
1825
|
+
event.stopPropagation();
|
|
1826
|
+
const detail = $('#graphDetail');
|
|
1827
|
+
if (!detail) return;
|
|
1828
|
+
detail.classList.add('visible');
|
|
1829
|
+
$('#graphDetailCat').textContent = d.category;
|
|
1830
|
+
$('#graphDetailCat').style.color = getCategoryColor(d.category);
|
|
1831
|
+
$('#graphDetailName').textContent = d.label;
|
|
1832
|
+
const pathEl = $('#graphDetailPath');
|
|
1833
|
+
pathEl.textContent = d.id;
|
|
1834
|
+
pathEl.onclick = () => openFile(d.id);
|
|
1835
|
+
|
|
1836
|
+
const imports = graphData.edges
|
|
1837
|
+
.filter(e => (e.source?.id || e.source) === d.id)
|
|
1838
|
+
.map(e => graphData.nodes.find(n => n.id === (e.target?.id || e.target)))
|
|
1839
|
+
.filter(Boolean);
|
|
1840
|
+
const importedBy = graphData.edges
|
|
1841
|
+
.filter(e => (e.target?.id || e.target) === d.id)
|
|
1842
|
+
.map(e => graphData.nodes.find(n => n.id === (e.source?.id || e.source)))
|
|
1843
|
+
.filter(Boolean);
|
|
1844
|
+
|
|
1845
|
+
$('#graphDetailImportsCount').textContent = imports.length;
|
|
1846
|
+
$('#graphDetailImports').innerHTML = imports.map(n =>
|
|
1847
|
+
`<div class="graph-detail-item"><span class="dot" style="background:${getCategoryColor(n.category)}"></span>${esc(n.label)}</div>`
|
|
1848
|
+
).join('');
|
|
1849
|
+
$('#graphDetailImportedByCount').textContent = importedBy.length;
|
|
1850
|
+
$('#graphDetailImportedBy').innerHTML = importedBy.map(n =>
|
|
1851
|
+
`<div class="graph-detail-item"><span class="dot" style="background:${getCategoryColor(n.category)}"></span>${esc(n.label)}</div>`
|
|
1852
|
+
).join('');
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// --- Context-aware empty state ---
|
|
1856
|
+
function updateEmptyState() {
|
|
1857
|
+
const emptyMsg = $('#emptyMsg');
|
|
1858
|
+
if (!emptyMsg) return;
|
|
1859
|
+
const hasRecent = getRecentRepos().length > 0;
|
|
1860
|
+
if (hasRecent) {
|
|
1861
|
+
emptyMsg.innerHTML = `Select an indexed repository above, or add a new one.<br>
|
|
1862
|
+
Drop a folder, click <strong>Browse</strong>, or type a path.<br><br>
|
|
1863
|
+
<kbd>⌘O</kbd> browse <kbd>⌘K</kbd> focus search`;
|
|
1864
|
+
} else {
|
|
1865
|
+
emptyMsg.innerHTML = `Drop a folder here, click <strong>Browse</strong>, or type a path above.<br>
|
|
1866
|
+
Then search symbols, explore the map, or trace callers.<br><br>
|
|
1867
|
+
<kbd>⌘O</kbd> browse <kbd>⌘K</kbd> focus search`;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
updateEmptyState();
|
|
1247
1871
|
</script>
|
|
1248
1872
|
</body>
|
|
1249
1873
|
</html>
|