@happy-nut/monacori 0.1.21 → 0.1.22
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/dist/app-main.js +39 -4
- package/dist/assets.js +8 -1
- package/dist/build.js +5 -1
- package/dist/i18n.js +54 -12
- package/dist/preload.cjs +8 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.js +71 -30
- package/dist/util.d.ts +5 -0
- package/dist/util.js +21 -0
- package/dist/viewer.client.js +273 -35
- package/dist/viewer.client.min.js +1 -0
- package/dist/viewer.css +153 -67
- package/package.json +2 -1
package/dist/viewer.client.js
CHANGED
|
@@ -19,6 +19,27 @@ if (REVIEW_LAZY) {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
var diffBootDone = false;
|
|
22
|
+
// Rebuild the hunk index from the CURRENT diff DOM. `hunks`/`hunkPeers`/`hunkMeta` are captured once at
|
|
23
|
+
// init; after an in-place watch swap (applyDiffUpdate) the DOM holds new wrappers/rows, so without this
|
|
24
|
+
// hunkTotal()/hunkPathAt() keep reporting the OLD build — F7 and showDiffView then target vanished indices
|
|
25
|
+
// and the diff pane goes blank. Mutates the const arrays in place so existing references stay valid.
|
|
26
|
+
function refreshHunkIndex() {
|
|
27
|
+
if (REVIEW_LAZY) {
|
|
28
|
+
hunkMeta.length = 0;
|
|
29
|
+
Array.prototype.forEach.call(document.querySelectorAll('#diff2html-container .d2h-file-wrapper'), function (w) {
|
|
30
|
+
var base = parseInt(w.dataset.firstHunk || '0', 10) || 0;
|
|
31
|
+
var cnt = parseInt(w.dataset.hunkCount || '0', 10) || 0;
|
|
32
|
+
var p = w.dataset.path || ((w.querySelector('.d2h-file-name') || {}).textContent || '').trim();
|
|
33
|
+
for (var k = 0; k < cnt; k++) hunkMeta[base + k] = { path: p };
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
prepareDiff2HtmlHunks(); // (re)tag .hunk/.hunk-peer rows + file ids on the new DOM
|
|
37
|
+
hunks.length = 0;
|
|
38
|
+
Array.prototype.push.apply(hunks, document.querySelectorAll('.hunk'));
|
|
39
|
+
hunkPeers.length = 0;
|
|
40
|
+
Array.prototype.push.apply(hunkPeers, document.querySelectorAll('.hunk-peer'));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
22
43
|
function hunkTotal() { return REVIEW_LAZY ? hunkMeta.length : hunks.length; }
|
|
23
44
|
function hunkPathAt(i) { return REVIEW_LAZY ? (hunkMeta[i] ? hunkMeta[i].path : '') : (hunks[i] ? hunks[i].dataset.file : ''); }
|
|
24
45
|
function hunkRowAt(i) {
|
|
@@ -256,6 +277,7 @@ const quickOpen = document.getElementById('quick-open');
|
|
|
256
277
|
const quickInput = document.getElementById('quick-open-input');
|
|
257
278
|
const quickResults = document.getElementById('quick-open-results');
|
|
258
279
|
const quickModeLabel = document.getElementById('quick-open-mode');
|
|
280
|
+
const quickFilterEl = document.getElementById('quick-open-filter');
|
|
259
281
|
let current = -1;
|
|
260
282
|
let checkingForUpdates = false;
|
|
261
283
|
let lastShiftAt = 0;
|
|
@@ -263,6 +285,7 @@ let lastShiftSide = 0;
|
|
|
263
285
|
let quickMode = 'all';
|
|
264
286
|
let quickItems = [];
|
|
265
287
|
let quickActive = 0;
|
|
288
|
+
let recentFilter = ''; // IntelliJ-style speed-search: typed letters narrow the Recent list (no search box)
|
|
266
289
|
let usageItems = []; // find-usages results for the Cmd+B-on-declaration popup
|
|
267
290
|
let usageActive = 0;
|
|
268
291
|
let viewerCursor = null;
|
|
@@ -660,14 +683,19 @@ function next(delta) {
|
|
|
660
683
|
// File boundary: no more change blocks in this file. Forward F7 announces "last change — press F7 again
|
|
661
684
|
// to go to the next file" on the FIRST press (a beat to mark-viewed) and only crosses on the SECOND
|
|
662
685
|
// consecutive press. Already-viewed files (and backward nav) cross immediately — no announcement.
|
|
663
|
-
|
|
686
|
+
// The `hunkPathAt(current) === diffCursor.path` guard skips the announcement while a cross is still in
|
|
687
|
+
// flight: after setActive moves `current` to the next file but BEFORE its (async, lazy-loaded) caret lands,
|
|
688
|
+
// diffCursor still points at the OLD file — without the guard a quick second F7 re-announced that old
|
|
689
|
+
// boundary instead of letting the cross finish (the "press F7 twice more, no caret" bug).
|
|
690
|
+
if (delta > 0 && diffCursor && isDiffViewVisible() && !isFileViewed(diffCursor.path) && hunkPathAt(current) === diffCursor.path) {
|
|
664
691
|
if (pendingFileBoundary !== diffCursor.path) {
|
|
665
692
|
pendingFileBoundary = diffCursor.path;
|
|
666
|
-
|
|
693
|
+
showCaretHint(t('diff.lastHunk'));
|
|
667
694
|
return;
|
|
668
695
|
}
|
|
669
696
|
pendingFileBoundary = null; // second consecutive press on the same file → fall through and cross
|
|
670
697
|
}
|
|
698
|
+
hideCaretHint(); // about to cross files — drop the hint NOW (before the async body load) so it can't cover the next file
|
|
671
699
|
// hunk-level nav to the next/prev unviewed file.
|
|
672
700
|
const caretHunk = hunkIndexAtCaret();
|
|
673
701
|
const base = caretHunk >= 0 ? caretHunk : current;
|
|
@@ -716,9 +744,22 @@ function openQuickOpen(mode) {
|
|
|
716
744
|
quickMode = mode;
|
|
717
745
|
quickModeLabel.textContent = mode === 'recent' ? t('quickopen.recent') : mode === 'content' ? t('quickopen.findInFiles') : t('quickopen.searchFiles');
|
|
718
746
|
quickOpen.classList.remove('hidden');
|
|
747
|
+
// Recent files needs no search box — it's just the latest files. Hide the input and let typed letters
|
|
748
|
+
// narrow the list (IntelliJ-style speed search); the global keydown routes keys to handleQuickOpenKey.
|
|
749
|
+
quickOpen.classList.toggle('quick-recent', mode === 'recent');
|
|
750
|
+
recentFilter = '';
|
|
719
751
|
quickInput.value = '';
|
|
752
|
+
updateRecentFilterDisplay();
|
|
720
753
|
renderQuickOpenResults();
|
|
721
|
-
|
|
754
|
+
if (mode === 'recent') { if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); }
|
|
755
|
+
else setTimeout(() => quickInput.focus(), 0);
|
|
756
|
+
}
|
|
757
|
+
// Title-row indicator for the Recent speed-search: the typed letters, or a muted "type to filter" hint.
|
|
758
|
+
function updateRecentFilterDisplay() {
|
|
759
|
+
if (!quickFilterEl) return;
|
|
760
|
+
if (quickMode !== 'recent') { quickFilterEl.textContent = ''; quickFilterEl.className = 'quick-open-filter'; return; }
|
|
761
|
+
if (recentFilter) { quickFilterEl.textContent = recentFilter; quickFilterEl.className = 'quick-open-filter has-filter'; }
|
|
762
|
+
else { quickFilterEl.textContent = t('quickopen.typeToFilter'); quickFilterEl.className = 'quick-open-filter is-hint'; }
|
|
722
763
|
}
|
|
723
764
|
|
|
724
765
|
function closeQuickOpen() {
|
|
@@ -728,6 +769,8 @@ function closeQuickOpen() {
|
|
|
728
769
|
function handleQuickOpenKey(event) {
|
|
729
770
|
if (event.key === 'Escape') {
|
|
730
771
|
event.preventDefault();
|
|
772
|
+
// Recent speed-search: first Esc clears the typed filter, a second Esc closes (IntelliJ behavior).
|
|
773
|
+
if (quickMode === 'recent' && recentFilter) { recentFilter = ''; updateRecentFilterDisplay(); renderQuickOpenResults(); return true; }
|
|
731
774
|
closeQuickOpen();
|
|
732
775
|
return true;
|
|
733
776
|
}
|
|
@@ -748,15 +791,31 @@ function handleQuickOpenKey(event) {
|
|
|
748
791
|
openQuickItem(quickItems[quickActive]);
|
|
749
792
|
return true;
|
|
750
793
|
}
|
|
794
|
+
// Recent files has no input box: type letters to filter the list, Backspace to delete (speed search).
|
|
795
|
+
if (quickMode === 'recent') {
|
|
796
|
+
if (event.key === 'Backspace') {
|
|
797
|
+
event.preventDefault();
|
|
798
|
+
if (recentFilter) { recentFilter = recentFilter.slice(0, -1); updateRecentFilterDisplay(); renderQuickOpenResults(); }
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
802
|
+
event.preventDefault();
|
|
803
|
+
recentFilter += event.key;
|
|
804
|
+
updateRecentFilterDisplay();
|
|
805
|
+
renderQuickOpenResults();
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
751
809
|
return false;
|
|
752
810
|
}
|
|
753
811
|
|
|
754
812
|
function renderQuickOpenResults() {
|
|
755
813
|
if (!quickResults) return;
|
|
756
|
-
|
|
757
|
-
const
|
|
814
|
+
// Recent mode filters its own list by the typed speed-search string; other modes use the search box.
|
|
815
|
+
const isRecent = quickMode === 'recent';
|
|
816
|
+
const query = (isRecent ? recentFilter : (quickInput?.value || '')).trim().toLowerCase();
|
|
817
|
+
const candidates = isRecent ? recentItems() : allQuickItems();
|
|
758
818
|
quickItems = candidates
|
|
759
|
-
.filter((item) => quickMode !== 'recent' || query.length > 0 || item.recent)
|
|
760
819
|
.filter((item) => {
|
|
761
820
|
if (query.length === 0) return true;
|
|
762
821
|
if (quickMode === 'content') {
|
|
@@ -1096,17 +1155,17 @@ document.addEventListener('keydown', (event) => {
|
|
|
1096
1155
|
// and +. open the merged views; Cmd/Ctrl+Shift+N toggles the memo. (Match event.code so IME/layout never
|
|
1097
1156
|
// swallows the combo.) Settings is a true overlay, so these stand down while it is up.
|
|
1098
1157
|
var settingsUp = (function () { var s = document.getElementById('settings-modal'); return !!(s && !s.classList.contains('hidden')); })();
|
|
1099
|
-
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'Quote') {
|
|
1158
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.code === 'Quote') {
|
|
1100
1159
|
event.preventDefault();
|
|
1101
1160
|
toggleDockMaximized();
|
|
1102
1161
|
return;
|
|
1103
1162
|
}
|
|
1104
|
-
if (!settingsUp && (event.metaKey || event.ctrlKey) && (event.code === 'Slash' || event.code === 'Period' || event.key === '?' || event.key === '>')) {
|
|
1163
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && !event.altKey && (event.code === 'Slash' || event.code === 'Period' || event.key === '?' || event.key === '>')) {
|
|
1105
1164
|
event.preventDefault();
|
|
1106
1165
|
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
1107
1166
|
return;
|
|
1108
1167
|
}
|
|
1109
|
-
if (!settingsUp && (event.metaKey || event.ctrlKey) && event.shiftKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
1168
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
1110
1169
|
event.preventDefault();
|
|
1111
1170
|
openMemoView();
|
|
1112
1171
|
return;
|
|
@@ -1116,7 +1175,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1116
1175
|
// shortcuts (Cmd+1, F7, Cmd+[/], Cmd+B, …). Each has its own Esc + editing handlers.
|
|
1117
1176
|
if (isFloatingModalOpen()) return;
|
|
1118
1177
|
|
|
1119
|
-
if ((event.metaKey || event.ctrlKey) && event.key === '1') {
|
|
1178
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '1') {
|
|
1120
1179
|
event.preventDefault();
|
|
1121
1180
|
// Coming from the diff: open the file you were viewing as source so Cmd+1 lands ON it (not a stale/blank
|
|
1122
1181
|
// source pane), and the tree below points at the same file. Capture the path BEFORE openSourceFile flips
|
|
@@ -1131,7 +1190,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1131
1190
|
focusOpenFileInTree();
|
|
1132
1191
|
return;
|
|
1133
1192
|
}
|
|
1134
|
-
if ((event.metaKey || event.ctrlKey) && event.key === '0') {
|
|
1193
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '0') {
|
|
1135
1194
|
event.preventDefault();
|
|
1136
1195
|
setTab('changes');
|
|
1137
1196
|
focusOpenFileInTree();
|
|
@@ -1217,7 +1276,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1217
1276
|
// PageUp/Down scroll the diff/source view. There's no focusable scroller (the diff caret is a JS cursor),
|
|
1218
1277
|
// and d2h-file-side-diff's horizontal scrollport even swallows vertical wheel, so handle paging explicitly.
|
|
1219
1278
|
// Only when the tree isn't focused — the tree pages itself in handleTreeKey below.
|
|
1220
|
-
if (treeFocusIndex < 0 && (event.key === 'PageDown' || event.key === 'PageUp') && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
1279
|
+
if (treeFocusIndex < 0 && (event.key === 'PageDown' || event.key === 'PageUp') && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) {
|
|
1221
1280
|
var psc = isDiffViewVisible() ? document.getElementById('diff2html-container') : (isSourceViewerVisible() ? document.getElementById('source-body') : null);
|
|
1222
1281
|
if (psc) { event.preventDefault(); psc.scrollTop += (event.key === 'PageDown' ? 0.9 : -0.9) * psc.clientHeight; return; }
|
|
1223
1282
|
}
|
|
@@ -1248,12 +1307,12 @@ document.addEventListener('keydown', (event) => {
|
|
|
1248
1307
|
lastShiftSide = side;
|
|
1249
1308
|
}
|
|
1250
1309
|
|
|
1251
|
-
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === 'f') {
|
|
1310
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.key.toLowerCase() === 'f') {
|
|
1252
1311
|
event.preventDefault();
|
|
1253
1312
|
openQuickOpen('content');
|
|
1254
1313
|
return;
|
|
1255
1314
|
}
|
|
1256
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'e') {
|
|
1315
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key.toLowerCase() === 'e') {
|
|
1257
1316
|
event.preventDefault();
|
|
1258
1317
|
openQuickOpen('recent');
|
|
1259
1318
|
return;
|
|
@@ -1268,14 +1327,14 @@ document.addEventListener('keydown', (event) => {
|
|
|
1268
1327
|
}
|
|
1269
1328
|
}
|
|
1270
1329
|
|
|
1271
|
-
if ((event.metaKey || event.ctrlKey) && event.key === 'ArrowDown') {
|
|
1330
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === 'ArrowDown') {
|
|
1272
1331
|
event.preventDefault();
|
|
1273
1332
|
if (isSourceViewerVisible()) goToSymbolUnderCursor();
|
|
1274
1333
|
else openDiffFileAtCaret();
|
|
1275
1334
|
return;
|
|
1276
1335
|
}
|
|
1277
1336
|
|
|
1278
|
-
if ((event.metaKey || event.ctrlKey) && (event.key === 'b' || event.key === 'B')) {
|
|
1337
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'b' || event.key === 'B')) {
|
|
1279
1338
|
var aeB = document.activeElement;
|
|
1280
1339
|
if (aeB && (aeB.tagName === 'INPUT' || aeB.tagName === 'TEXTAREA' || aeB.tagName === 'SELECT')) return;
|
|
1281
1340
|
event.preventDefault();
|
|
@@ -1337,7 +1396,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1337
1396
|
}
|
|
1338
1397
|
}
|
|
1339
1398
|
|
|
1340
|
-
if (event.key === 'F7') {
|
|
1399
|
+
if (event.key === 'F7' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
1341
1400
|
event.preventDefault();
|
|
1342
1401
|
const delta = event.shiftKey ? -1 : 1;
|
|
1343
1402
|
const sourceViewer = document.getElementById('source-viewer');
|
|
@@ -1414,6 +1473,19 @@ document.querySelectorAll('.tab').forEach((button) => {
|
|
|
1414
1473
|
button.addEventListener('click', () => setTab(button.dataset.tab || 'changes'));
|
|
1415
1474
|
});
|
|
1416
1475
|
|
|
1476
|
+
// Activity rail (IntelliJ-style): click an icon to navigate/toggle its view. Terminal + settings buttons
|
|
1477
|
+
// carry no data-view — they keep their own id-based handlers (terminal toggle / settings gear).
|
|
1478
|
+
document.querySelector('.activity-rail')?.addEventListener('click', (event) => {
|
|
1479
|
+
const btn = event.target.closest && event.target.closest('.rail-btn[data-view]');
|
|
1480
|
+
if (!btn) return;
|
|
1481
|
+
const view = btn.dataset.view;
|
|
1482
|
+
if (view === 'changes') { setTab('changes'); if (!isDiffViewVisible()) showDiffView(false); }
|
|
1483
|
+
else if (view === 'files') { setTab('files'); }
|
|
1484
|
+
else if (view === 'q' || view === 'c') { toggleMergedRail(view); }
|
|
1485
|
+
else if (view === 'memo') { openMemoView(); } // openMemoView already toggles
|
|
1486
|
+
syncRail();
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1417
1489
|
document.getElementById('back-to-diff')?.addEventListener('click', () => showDiffView(true));
|
|
1418
1490
|
document.getElementById('source-tabs')?.addEventListener('click', function (event) {
|
|
1419
1491
|
var closeBtn = event.target && event.target.closest && event.target.closest('.source-tab-close');
|
|
@@ -1450,6 +1522,7 @@ if (!restored) {
|
|
|
1450
1522
|
else openDefaultSourceFile();
|
|
1451
1523
|
}
|
|
1452
1524
|
initSourceTreeFolds();
|
|
1525
|
+
syncRail(); // reflect the initial view on the activity rail
|
|
1453
1526
|
// Electron receives live updates over IPC (monacoriMenu.onDiffUpdate); only serve/browser needs the HTTP
|
|
1454
1527
|
// poller. Under file:// its fetch just fails every 1.5s for the app's whole life, so skip it in Electron.
|
|
1455
1528
|
if (watchEnabled && !(window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function')) {
|
|
@@ -1485,7 +1558,10 @@ window.addEventListener('beforeunload', saveUiState);
|
|
|
1485
1558
|
});
|
|
1486
1559
|
document.addEventListener('mousemove', (event) => {
|
|
1487
1560
|
if (!resizing) return;
|
|
1488
|
-
|
|
1561
|
+
// Subtract the activity rail's width: the sidebar starts to its right, so its width is the cursor X
|
|
1562
|
+
// minus the rail offset (not clientX itself, which would over-size it by the rail width).
|
|
1563
|
+
const railW = parseFloat(getComputedStyle(document.body).getPropertyValue('--rail-width')) || 0;
|
|
1564
|
+
const width = Math.min(640, Math.max(180, event.clientX - railW));
|
|
1489
1565
|
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
|
|
1490
1566
|
});
|
|
1491
1567
|
document.addEventListener('mouseup', () => {
|
|
@@ -1671,6 +1747,7 @@ function setDiffCursor(path, side, rowIndex, column, reveal) {
|
|
|
1671
1747
|
var col = Math.max(0, Math.min(column, diffLineText(rows[ri]).length));
|
|
1672
1748
|
diffCursor = { path: path, side: side, rowIndex: ri, column: col };
|
|
1673
1749
|
pendingFileBoundary = null; // any caret move re-arms the last-change announcement for the next F7 (see next)
|
|
1750
|
+
hideCaretHint(); // caret moved (incl. crossing to the next file) → drop the "last change" hint so it never covers the new file
|
|
1674
1751
|
diffSelectionAnchor = null; // any direct caret placement (click/F7/Cmd-arrow) drops the selection; Shift+Arrow re-sets it
|
|
1675
1752
|
if (reveal) {
|
|
1676
1753
|
// Render the caret AND scroll in the SAME animation frame. A fast key-repeat queues several ArrowDowns
|
|
@@ -1911,6 +1988,28 @@ function showToast(message) {
|
|
|
1911
1988
|
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
|
|
1912
1989
|
}, 4500);
|
|
1913
1990
|
}
|
|
1991
|
+
// Inline hint anchored just under the diff caret — used for the F7 "last change" boundary announcement so the
|
|
1992
|
+
// message appears where the user is looking and fades on its own (unlike the corner toast). Falls back to the
|
|
1993
|
+
// corner toast when there's no on-screen caret (e.g. source view).
|
|
1994
|
+
var caretHintEl = null, caretHintTimer = 0;
|
|
1995
|
+
function showCaretHint(message) {
|
|
1996
|
+
var row = activeDiffRow || document.querySelector('#diff2html-container .diff-active-row');
|
|
1997
|
+
if (!row || !row.getBoundingClientRect) { showToast(message); return; }
|
|
1998
|
+
if (!caretHintEl) { caretHintEl = document.createElement('div'); caretHintEl.className = 'mc-caret-hint'; document.body.appendChild(caretHintEl); }
|
|
1999
|
+
caretHintEl.textContent = message;
|
|
2000
|
+
var r = row.getBoundingClientRect();
|
|
2001
|
+
caretHintEl.style.left = Math.round(Math.max(8, r.left)) + 'px';
|
|
2002
|
+
caretHintEl.style.top = Math.round(r.bottom + 4) + 'px';
|
|
2003
|
+
caretHintEl.classList.remove('show');
|
|
2004
|
+
void caretHintEl.offsetWidth; // reflow so the fade-in re-triggers on rapid repeat presses
|
|
2005
|
+
caretHintEl.classList.add('show');
|
|
2006
|
+
if (caretHintTimer) clearTimeout(caretHintTimer);
|
|
2007
|
+
caretHintTimer = setTimeout(function () { if (caretHintEl) caretHintEl.classList.remove('show'); }, 2000);
|
|
2008
|
+
}
|
|
2009
|
+
function hideCaretHint() {
|
|
2010
|
+
if (caretHintTimer) { clearTimeout(caretHintTimer); caretHintTimer = 0; }
|
|
2011
|
+
if (caretHintEl) caretHintEl.classList.remove('show');
|
|
2012
|
+
}
|
|
1914
2013
|
// Follow each comment to its snapshot line (c.code) in the current content: same line if unchanged, else the
|
|
1915
2014
|
// nearest exact match of that line. A comment is NEVER auto-deleted. If its line can't be found we leave it
|
|
1916
2015
|
// where it is — this happens routinely WITHOUT the file changing: a comment anchored to a deleted/old-side
|
|
@@ -2405,6 +2504,12 @@ function applyDockMaximized() {
|
|
|
2405
2504
|
document.body.classList.toggle('dock-maximized', dockMaximized);
|
|
2406
2505
|
}
|
|
2407
2506
|
function toggleDockMaximized() {
|
|
2507
|
+
// Maximize only the panel you're FOCUSED in: the merged/memo dock (.dock-panel) or the terminal
|
|
2508
|
+
// (.terminal-panel). From the sidebar tree (treeFocusIndex >= 0) or the diff/source content this is a
|
|
2509
|
+
// no-op — pressing it there must NOT maximize a terminal you aren't actually in.
|
|
2510
|
+
if (treeFocusIndex >= 0) return;
|
|
2511
|
+
var ae = document.activeElement;
|
|
2512
|
+
if (!(ae && ae.closest && (ae.closest('.dock-panel') || ae.closest('.terminal-panel')))) return;
|
|
2408
2513
|
if (!activeDockPanel()) return; // nothing docked -> nothing to maximize
|
|
2409
2514
|
dockMaximized = !dockMaximized;
|
|
2410
2515
|
applyDockMaximized();
|
|
@@ -2423,6 +2528,7 @@ function closeMergedMemoDocks() {
|
|
|
2423
2528
|
// terminal dock but never for these floating panels.
|
|
2424
2529
|
document.body.classList.toggle('floating-dock', !!(document.getElementById('mc-merged-panel') || document.getElementById('mc-memo-panel')));
|
|
2425
2530
|
applyDockMaximized();
|
|
2531
|
+
if (typeof syncRail === 'function') syncRail(); // clear the rail icon for the closed dock(s)
|
|
2426
2532
|
}
|
|
2427
2533
|
window.__monacoriCloseDocks = closeMergedMemoDocks;
|
|
2428
2534
|
// Retry-focus a docked field (Electron async-restores focus to <body>, so a one-shot focus can lose the race).
|
|
@@ -2507,6 +2613,7 @@ function mountDock(id, titleText) {
|
|
|
2507
2613
|
document.body.classList.add('dock-open');
|
|
2508
2614
|
document.body.classList.add('floating-dock'); // scopes the maximize CSS so it doesn't hide the diff
|
|
2509
2615
|
applyDockMaximized();
|
|
2616
|
+
if (typeof syncRail === 'function') syncRail(); // light up the rail icon for the opened dock
|
|
2510
2617
|
return { panel: panel, body: body, bar: bar, close: close };
|
|
2511
2618
|
}
|
|
2512
2619
|
|
|
@@ -2681,6 +2788,7 @@ refreshComments();
|
|
|
2681
2788
|
|
|
2682
2789
|
function setActive(p) {
|
|
2683
2790
|
active = p;
|
|
2791
|
+
if (p && p.labelEl) p.labelEl.classList.remove('has-bell'); // viewing the pane clears its bell badge
|
|
2684
2792
|
panes.forEach(function (q) {
|
|
2685
2793
|
q.el.classList.toggle('is-active', q === p);
|
|
2686
2794
|
// 2+ panes: dim every pane but the active one (no border, just a clean focus cue). A lone pane stays full.
|
|
@@ -2694,6 +2802,11 @@ refreshComments();
|
|
|
2694
2802
|
});
|
|
2695
2803
|
}
|
|
2696
2804
|
|
|
2805
|
+
function copyToClipboard(text) {
|
|
2806
|
+
if (!text) return;
|
|
2807
|
+
try { if (window.monacoriClipboard && window.monacoriClipboard.write) { window.monacoriClipboard.write(text); return; } } catch (e) {}
|
|
2808
|
+
try { if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text); } catch (e) {}
|
|
2809
|
+
}
|
|
2697
2810
|
function makePane() {
|
|
2698
2811
|
if (!ensureXterm()) return null; // xterm unavailable — leave the panel empty rather than throw
|
|
2699
2812
|
var el = document.createElement('div');
|
|
@@ -2729,6 +2842,9 @@ refreshComments();
|
|
|
2729
2842
|
// Match the PHYSICAL key (e.code), not e.key: under a non-Latin layout/IME (e.g. Korean 한글)
|
|
2730
2843
|
// Cmd+V reports e.key as 'ㅍ', so a key-based check misses it — blurring the terminal and
|
|
2731
2844
|
// breaking paste/copy/cut/select-all whenever the Korean input source is active.
|
|
2845
|
+
// Cmd+C with a terminal selection: copy it ourselves — xterm doesn't auto-copy and the menu/native
|
|
2846
|
+
// copy misses xterm's own selection, so Cmd+C silently did nothing. No selection -> fall through.
|
|
2847
|
+
if (e.code === 'KeyC' && term.hasSelection && term.hasSelection()) { copyToClipboard(term.getSelection()); return false; }
|
|
2732
2848
|
if (e.code === 'KeyC' || e.code === 'KeyV' || e.code === 'KeyX' || e.code === 'KeyA') return true;
|
|
2733
2849
|
try { term.blur(); } catch (x) {}
|
|
2734
2850
|
return false;
|
|
@@ -2736,6 +2852,14 @@ refreshComments();
|
|
|
2736
2852
|
return true;
|
|
2737
2853
|
});
|
|
2738
2854
|
term.onData(function (d) { if (pane.id != null) window.monacoriPty.write({ id: pane.id, data: d }); });
|
|
2855
|
+
// Bell from the pane's TUI (e.g. Claude Code finished a turn / needs input): badge the pane when it isn't
|
|
2856
|
+
// the one you're looking at, and ask the main process to raise a native notification when the whole window
|
|
2857
|
+
// isn't focused. Toggle in Settings ("Notify when a terminal task finishes").
|
|
2858
|
+
term.onBell(function () {
|
|
2859
|
+
if (pane !== active && pane.labelEl) pane.labelEl.classList.add('has-bell');
|
|
2860
|
+
if (persistRead('monacori-terminal-bell-notify') === false) return; // OS notifications disabled
|
|
2861
|
+
try { window.monacoriPty.bell({ title: 'monacori', body: pane.name + ' — ' + t('notify.bellBody') }); } catch (e) {}
|
|
2862
|
+
});
|
|
2739
2863
|
el.addEventListener('mousedown', function (e) { if (e.target !== labelEl) setActive(pane); });
|
|
2740
2864
|
labelEl.addEventListener('dblclick', function () { renamePane(pane); });
|
|
2741
2865
|
panes.push(pane);
|
|
@@ -2783,10 +2907,12 @@ refreshComments();
|
|
|
2783
2907
|
}
|
|
2784
2908
|
|
|
2785
2909
|
function removePane(id) {
|
|
2786
|
-
var
|
|
2787
|
-
|
|
2910
|
+
for (var k = 0; k < panes.length; k++) { if (panes[k].id === id) { removePaneRef(panes[k]); return; } }
|
|
2911
|
+
}
|
|
2912
|
+
// Remove a pane by object reference (handles panes whose pty id hasn't arrived yet — spawn is async).
|
|
2913
|
+
function removePaneRef(p) {
|
|
2914
|
+
var i = panes.indexOf(p);
|
|
2788
2915
|
if (i < 0) return;
|
|
2789
|
-
var p = panes[i];
|
|
2790
2916
|
try { p.term.dispose(); } catch (e) {}
|
|
2791
2917
|
if (p.el.parentNode) p.el.parentNode.removeChild(p.el);
|
|
2792
2918
|
panes.splice(i, 1);
|
|
@@ -2794,6 +2920,15 @@ refreshComments();
|
|
|
2794
2920
|
if (panes.length === 0) setOpen(false);
|
|
2795
2921
|
else fitAll();
|
|
2796
2922
|
}
|
|
2923
|
+
// Cmd/Ctrl+W inside the terminal: close just the FOCUSED pane (kill its pty), not the whole panel. The
|
|
2924
|
+
// last pane closing collapses the panel via removePaneRef -> setOpen(false). Remove the pane immediately
|
|
2925
|
+
// (don't wait for the pty's onExit) so the UI responds at once; the later onExit -> removePane no-ops.
|
|
2926
|
+
function closeActivePane() {
|
|
2927
|
+
var p = active || panes[panes.length - 1];
|
|
2928
|
+
if (!p) { setOpen(false); return; }
|
|
2929
|
+
if (p.id != null) { try { window.monacoriPty.kill({ id: p.id }); } catch (e) {} }
|
|
2930
|
+
removePaneRef(p);
|
|
2931
|
+
}
|
|
2797
2932
|
|
|
2798
2933
|
function split() {
|
|
2799
2934
|
if (panes.length >= MAX_PANES) return;
|
|
@@ -2927,8 +3062,12 @@ refreshComments();
|
|
|
2927
3062
|
}, true);
|
|
2928
3063
|
window.__monacoriTerminal = {
|
|
2929
3064
|
isOpen: isOpen,
|
|
3065
|
+
// True when keyboard focus is inside the terminal panel (a pane owns it) — Cmd/Ctrl+W uses this to
|
|
3066
|
+
// decide between closing a pane and closing a source tab.
|
|
3067
|
+
hasFocus: function () { var ae = document.activeElement; return !!(ae && panel.contains(ae)); },
|
|
2930
3068
|
open: function () { setOpen(true); },
|
|
2931
3069
|
paneCount: function () { return panes.length; },
|
|
3070
|
+
closeActivePane: closeActivePane,
|
|
2932
3071
|
enterSendMode: enterSendMode,
|
|
2933
3072
|
send: function (text) { writeToPane(active || panes[0], text); },
|
|
2934
3073
|
sendToPane: function (i, text) { writeToPane(panes[i] || active || panes[0], text); },
|
|
@@ -2956,10 +3095,11 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function
|
|
|
2956
3095
|
window.monacoriMenu.onDiffUpdate(function (html) { try { applyDiffUpdate(html); } catch (e) {} });
|
|
2957
3096
|
}
|
|
2958
3097
|
if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function') {
|
|
2959
|
-
// Cmd/Ctrl+W: close the
|
|
3098
|
+
// Cmd/Ctrl+W: close whatever the focus is on. A focused terminal pane closes just that pane (the last
|
|
3099
|
+
// pane collapses the panel); otherwise close the active Files-mode tab (no-op outside the source viewer).
|
|
2960
3100
|
window.monacoriMenu.onCloseTab(function () {
|
|
2961
|
-
|
|
2962
|
-
if (
|
|
3101
|
+
var term = window.__monacoriTerminal;
|
|
3102
|
+
if (term && term.isOpen() && term.hasFocus()) { term.closeActivePane(); return; }
|
|
2963
3103
|
if (isSourceViewerVisible()) closeActiveSourceTab();
|
|
2964
3104
|
});
|
|
2965
3105
|
}
|
|
@@ -3070,6 +3210,12 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
|
|
|
3070
3210
|
if (qta) qta.addEventListener('input', function () { saveMergePrompt('q', qta.value); flash(); });
|
|
3071
3211
|
if (cta) cta.addEventListener('input', function () { saveMergePrompt('c', cta.value); flash(); });
|
|
3072
3212
|
if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
|
|
3213
|
+
// Terminal-bell notification toggle (default ON — persistRead returns undefined when never set).
|
|
3214
|
+
var bellCb = document.getElementById('set-bell-notify');
|
|
3215
|
+
if (bellCb) {
|
|
3216
|
+
bellCb.checked = persistRead('monacori-terminal-bell-notify') !== false;
|
|
3217
|
+
bellCb.addEventListener('change', function () { persistSave('monacori-terminal-bell-notify', bellCb.checked); });
|
|
3218
|
+
}
|
|
3073
3219
|
// Language: live-switch the whole UI (no reload). Persist, re-apply the static chrome, then re-render
|
|
3074
3220
|
// any currently-shown dynamic text (open composer / merged modal / index status) so it follows too.
|
|
3075
3221
|
langSelectRef = setupCustomSelect('settings-language',
|
|
@@ -3103,6 +3249,29 @@ function setTab(name) {
|
|
|
3103
3249
|
});
|
|
3104
3250
|
document.getElementById('changes-panel')?.classList.toggle('hidden', name !== 'changes');
|
|
3105
3251
|
document.getElementById('files-panel')?.classList.toggle('hidden', name !== 'files');
|
|
3252
|
+
syncRail();
|
|
3253
|
+
}
|
|
3254
|
+
// Reflect the current view/dock state on the activity rail icons (active highlight). Terminal active is
|
|
3255
|
+
// kept in sync separately by the dock-terminal setOpen (it toggles is-active on #terminal-toggle).
|
|
3256
|
+
function syncRail() {
|
|
3257
|
+
var rail = document.querySelector('.activity-rail');
|
|
3258
|
+
if (!rail) return;
|
|
3259
|
+
var setOn = function (view, on) {
|
|
3260
|
+
var btn = rail.querySelector('[data-view="' + view + '"]');
|
|
3261
|
+
if (btn) btn.classList.toggle('is-active', !!on);
|
|
3262
|
+
};
|
|
3263
|
+
setOn('changes', !document.getElementById('changes-panel')?.classList.contains('hidden'));
|
|
3264
|
+
setOn('files', !document.getElementById('files-panel')?.classList.contains('hidden'));
|
|
3265
|
+
var merged = document.getElementById('mc-merged-panel');
|
|
3266
|
+
setOn('q', !!(merged && merged.dataset.kind === 'q'));
|
|
3267
|
+
setOn('c', !!(merged && merged.dataset.kind === 'c'));
|
|
3268
|
+
setOn('memo', !!document.getElementById('mc-memo-panel'));
|
|
3269
|
+
}
|
|
3270
|
+
// Rail click for the merged views toggles: a 2nd click on the open kind closes it (memo already toggles).
|
|
3271
|
+
function toggleMergedRail(kind) {
|
|
3272
|
+
var m = document.getElementById('mc-merged-panel');
|
|
3273
|
+
if (m && m.dataset.kind === kind) { closeMergedMemoDocks(); return; }
|
|
3274
|
+
openMergedView(kind);
|
|
3106
3275
|
}
|
|
3107
3276
|
// Big repos ship the source tree as an inert island (see render.ts); build it the first time the Files
|
|
3108
3277
|
// tab is opened so the (potentially huge) tree never blocks startup. No-op for inline (small) trees.
|
|
@@ -3218,6 +3387,10 @@ function applyDiffUpdate(u) {
|
|
|
3218
3387
|
var wasSource = isSourceViewerVisible();
|
|
3219
3388
|
var container = document.getElementById('diff2html-container');
|
|
3220
3389
|
var diffScrollTop = container ? container.scrollTop : 0;
|
|
3390
|
+
// The active hunk's file path BEFORE the swap (hunkMeta/hunks still hold the old build here). After a commit
|
|
3391
|
+
// the old active file can vanish from the new diff, so we re-anchor `current` to it below — otherwise it
|
|
3392
|
+
// dangles at a stale index and showDiffView renders blank with a stale breadcrumb.
|
|
3393
|
+
var prevActivePath = current >= 0 ? hunkPathAt(current) : '';
|
|
3221
3394
|
// Did the file the user is CURRENTLY viewing actually change in this build? If not, we must not re-render
|
|
3222
3395
|
// the source view — an unrelated file's edit would otherwise flicker the pane they're reading. Capture the
|
|
3223
3396
|
// open file's signature BEFORE fileSignatureByPath is rebuilt below.
|
|
@@ -3248,6 +3421,13 @@ function applyDiffUpdate(u) {
|
|
|
3248
3421
|
if (filesPanel && (!REVIEW_LAZY || filesPanel.innerHTML.trim())) filesPanel.innerHTML = u.filesTree || '';
|
|
3249
3422
|
var statusEl = document.querySelector('.review-status');
|
|
3250
3423
|
if (statusEl) statusEl.innerHTML = u.reviewStatus || '';
|
|
3424
|
+
// Branch can change between watch ticks (checkout/commit) — keep the sidebar chip current.
|
|
3425
|
+
var branchName = document.getElementById('brand-branch-name');
|
|
3426
|
+
if (branchName) {
|
|
3427
|
+
branchName.textContent = u.branch || '';
|
|
3428
|
+
var branchChip = branchName.closest && branchName.closest('.brand-branch');
|
|
3429
|
+
if (branchChip) branchChip.classList.toggle('hidden', !u.branch);
|
|
3430
|
+
}
|
|
3251
3431
|
if (reviewMeta) { reviewMeta.setAttribute('data-signature', u.signature); if (u.generatedAt) reviewMeta.setAttribute('data-generated-at', u.generatedAt); }
|
|
3252
3432
|
|
|
3253
3433
|
// 2) Re-derive module-level state directly from the payload objects.
|
|
@@ -3264,6 +3444,16 @@ function applyDiffUpdate(u) {
|
|
|
3264
3444
|
links = Array.from(document.querySelectorAll('#changes-panel .file-link'));
|
|
3265
3445
|
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
3266
3446
|
|
|
3447
|
+
// Reconcile the active hunk against the new build (uses the just-rebuilt `links`). A committed/removed file
|
|
3448
|
+
// reshuffles or shrinks the diff: re-anchor `current` to the same file's new hunk when it survives, else
|
|
3449
|
+
// drop to -1 so the diff lands on the first change rather than a dangling index that paints nothing.
|
|
3450
|
+
var activeFilePreserved = false;
|
|
3451
|
+
if (prevActivePath) {
|
|
3452
|
+
var reHunk = firstHunkForPath(prevActivePath);
|
|
3453
|
+
if (reHunk >= 0) { current = reHunk; activeFilePreserved = true; }
|
|
3454
|
+
else current = -1;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3267
3457
|
// 3) Reset lazy-materialize + index state so the new diff bodies / source / symbols rebuild on demand.
|
|
3268
3458
|
// bodyCache is keyed by file INDEX, not content — after a watch rebuild the same index maps to the new
|
|
3269
3459
|
// body, so it MUST be dropped too. Clearing only bodyPromise left loadBodyHtml() returning the cached
|
|
@@ -3277,14 +3467,11 @@ function applyDiffUpdate(u) {
|
|
|
3277
3467
|
// sourceBodyPath so the already-painted (unchanged) source view is left exactly as-is — no flicker.
|
|
3278
3468
|
if (openFileChanged) sourceBodyPath = null;
|
|
3279
3469
|
symbolIndex = null;
|
|
3280
|
-
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
3281
|
-
else { prepareDiff2HtmlHunks(); diffBootDone = true; }
|
|
3282
|
-
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0);
|
|
3283
3470
|
|
|
3284
3471
|
// 3b) Re-fill UNCHANGED files' bodies synchronously from the snapshot so they don't blank-then-reload (the
|
|
3285
|
-
// flicker).
|
|
3286
|
-
//
|
|
3287
|
-
//
|
|
3472
|
+
// flicker). Runs BEFORE setupLazyDiff so the IntersectionObserver sees them already materialized and never
|
|
3473
|
+
// re-fetches them. The fresh wrapper carries the correct data-first-hunk + file index, so materializeBody
|
|
3474
|
+
// numbers hunks exactly as a normal lazy load would. Changed/new files stay shells and lazy-load as usual.
|
|
3288
3475
|
if (REVIEW_LAZY && container) {
|
|
3289
3476
|
container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
|
|
3290
3477
|
var p = diffWrapperPathKey(w);
|
|
@@ -3298,6 +3485,10 @@ function applyDiffUpdate(u) {
|
|
|
3298
3485
|
bodyPromise[idx] = Promise.resolve(w);
|
|
3299
3486
|
});
|
|
3300
3487
|
}
|
|
3488
|
+
refreshHunkIndex(); // rebuild hunks/hunkMeta from the swapped-in DOM so hunkTotal()/hunkPathAt() aren't stale
|
|
3489
|
+
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
3490
|
+
else { diffBootDone = true; }
|
|
3491
|
+
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0);
|
|
3301
3492
|
|
|
3302
3493
|
// 4) Re-run the DOM-dependent bootstrap steps.
|
|
3303
3494
|
applyI18n();
|
|
@@ -3312,7 +3503,10 @@ function applyDiffUpdate(u) {
|
|
|
3312
3503
|
if (openFileChanged) openSourceFile(openPath, false);
|
|
3313
3504
|
} else if (container) {
|
|
3314
3505
|
showDiffView(false);
|
|
3315
|
-
|
|
3506
|
+
// Same active file survived → keep the user's exact scroll. If it was committed away (current reset to
|
|
3507
|
+
// -1, showDiffView landed on the first change), restoring the old, now-out-of-range scrollTop would push
|
|
3508
|
+
// the shorter new diff off-screen and look blank — so reset to the top instead.
|
|
3509
|
+
container.scrollTop = activeFilePreserved ? diffScrollTop : 0;
|
|
3316
3510
|
}
|
|
3317
3511
|
return true;
|
|
3318
3512
|
}
|
|
@@ -3961,6 +4155,39 @@ function showUsages(name, count) {
|
|
|
3961
4155
|
if (title) title.textContent = count + ' usage' + (count === 1 ? '' : 's') + ' of ' + name;
|
|
3962
4156
|
renderUsages();
|
|
3963
4157
|
box.classList.remove('hidden');
|
|
4158
|
+
positionUsagesAtCaret();
|
|
4159
|
+
}
|
|
4160
|
+
// Anchor the usages popup just below (or above, if cramped) the live caret — source OR diff both render a
|
|
4161
|
+
// `.code-cursor` span. No caret on screen → leave the centered overlay fallback in place.
|
|
4162
|
+
function positionUsagesAtCaret() {
|
|
4163
|
+
var box = document.getElementById('usages');
|
|
4164
|
+
if (!box) return;
|
|
4165
|
+
var panel = box.querySelector('.quick-open-panel');
|
|
4166
|
+
if (!panel) return;
|
|
4167
|
+
resetUsagesAnchor(box, panel); // measure from a clean slate
|
|
4168
|
+
var caret = document.querySelector('#source-body .code-cursor') || document.querySelector('#diff2html-container .code-cursor');
|
|
4169
|
+
if (!caret) return;
|
|
4170
|
+
var rect = caret.getBoundingClientRect();
|
|
4171
|
+
if (!rect.height && !rect.width && !rect.top) return; // detached / off-layout
|
|
4172
|
+
var vw = window.innerWidth, vh = window.innerHeight, gap = 6, margin = 8;
|
|
4173
|
+
var pw = Math.min(560, vw - margin * 2);
|
|
4174
|
+
var left = Math.min(Math.max(margin, rect.left), vw - pw - margin);
|
|
4175
|
+
box.classList.add('anchored');
|
|
4176
|
+
panel.style.width = pw + 'px';
|
|
4177
|
+
panel.style.left = left + 'px';
|
|
4178
|
+
var spaceBelow = vh - rect.bottom - gap - margin;
|
|
4179
|
+
var spaceAbove = rect.top - gap - margin;
|
|
4180
|
+
if (spaceBelow >= 200 || spaceBelow >= spaceAbove) {
|
|
4181
|
+
panel.style.top = (rect.bottom + gap) + 'px';
|
|
4182
|
+
panel.style.maxHeight = Math.max(120, spaceBelow) + 'px';
|
|
4183
|
+
} else {
|
|
4184
|
+
panel.style.bottom = (vh - rect.top + gap) + 'px';
|
|
4185
|
+
panel.style.maxHeight = Math.max(120, spaceAbove) + 'px';
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
function resetUsagesAnchor(box, panel) {
|
|
4189
|
+
box.classList.remove('anchored');
|
|
4190
|
+
panel.style.left = panel.style.top = panel.style.bottom = panel.style.width = panel.style.maxHeight = '';
|
|
3964
4191
|
}
|
|
3965
4192
|
function renderUsages() {
|
|
3966
4193
|
var results = document.getElementById('usages-results');
|
|
@@ -3998,7 +4225,11 @@ function openUsageItem(item) {
|
|
|
3998
4225
|
openSourceAt(item.path, item.lineIndex, item.column);
|
|
3999
4226
|
}
|
|
4000
4227
|
function closeUsages() {
|
|
4001
|
-
document.getElementById('usages')
|
|
4228
|
+
var box = document.getElementById('usages');
|
|
4229
|
+
if (!box) return;
|
|
4230
|
+
box.classList.add('hidden');
|
|
4231
|
+
var panel = box.querySelector('.quick-open-panel');
|
|
4232
|
+
if (panel) resetUsagesAnchor(box, panel); // clear inline anchoring so the next open re-measures cleanly
|
|
4002
4233
|
}
|
|
4003
4234
|
|
|
4004
4235
|
var symbolIndex = null; // Map<name, [{path,lineIndex,column}]>; built off-thread by a Web Worker, null until ready
|
|
@@ -4176,11 +4407,18 @@ function renderSourceTabs(activePath) {
|
|
|
4176
4407
|
var active = p === activePath;
|
|
4177
4408
|
return '<div class="source-tab' + (active ? ' active' : '') + '" data-tab-path="' + escapeHtml(p) + '" title="' + escapeHtml(p) + '">'
|
|
4178
4409
|
+ '<span class="source-tab-name">' + escapeHtml(sourceTabLabel(p)) + '</span>'
|
|
4179
|
-
+ '<button type="button" class="source-tab-close" data-close-path="' + escapeHtml(p) + '" aria-label="Close tab" title="Close (
|
|
4410
|
+
+ '<button type="button" class="source-tab-close" data-close-path="' + escapeHtml(p) + '" aria-label="Close tab" title="Close (⌘W)">×</button>'
|
|
4180
4411
|
+ '</div>';
|
|
4181
4412
|
}).join('');
|
|
4413
|
+
// Scroll the tab bar HORIZONTALLY only. scrollIntoView() walks every scrollable ancestor — on rapid
|
|
4414
|
+
// Cmd+Shift+[/] cycling it nudged a vertical ancestor and clipped the tab strip at the top. Adjusting
|
|
4415
|
+
// bar.scrollLeft directly keeps the active tab in view without ever touching vertical scroll.
|
|
4182
4416
|
var act = bar.querySelector('.source-tab.active');
|
|
4183
|
-
if (act
|
|
4417
|
+
if (act) {
|
|
4418
|
+
var bl = bar.getBoundingClientRect(), al = act.getBoundingClientRect();
|
|
4419
|
+
if (al.left < bl.left) bar.scrollLeft -= (bl.left - al.left) + 8;
|
|
4420
|
+
else if (al.right > bl.right) bar.scrollLeft += (al.right - bl.right) + 8;
|
|
4421
|
+
}
|
|
4184
4422
|
}
|
|
4185
4423
|
function closeSourceTab(path) {
|
|
4186
4424
|
var idx = sourceTabs.indexOf(path);
|
|
@@ -4653,7 +4891,7 @@ function renderHttpTable(file) {
|
|
|
4653
4891
|
const reqIdx = hasRun ? runAtLine[index] : -1;
|
|
4654
4892
|
const isCursorLine = Boolean(cursor && cursor.lineIndex === index);
|
|
4655
4893
|
const gutter = hasRun
|
|
4656
|
-
? '<button type="button" class="http-run" data-req="' + reqIdx + '" title="Run request (
|
|
4894
|
+
? '<button type="button" class="http-run" data-req="' + reqIdx + '" title="Run request (⌘Enter / ⌥Enter)" aria-label="Run request">▶</button>'
|
|
4657
4895
|
: '';
|
|
4658
4896
|
rows += '<tr class="source-row http-row' + (hasRun ? ' http-request-line' : '') + (isCursorLine ? ' cursor-line' : '') + '" data-line-index="' + index + '">'
|
|
4659
4897
|
+ '<td class="num http-gutter">' + gutter + '<span class="num-text">' + (index + 1) + '</span></td>'
|