@happy-nut/monacori 0.1.21 → 0.1.23
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 +95 -4
- package/dist/assets.js +8 -1
- package/dist/build.js +5 -1
- package/dist/git-log.d.ts +23 -0
- package/dist/git-log.js +60 -0
- package/dist/i18n.js +88 -12
- package/dist/preload.cjs +16 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.js +91 -30
- package/dist/util.d.ts +5 -0
- package/dist/util.js +21 -0
- package/dist/viewer.client.js +633 -35
- package/dist/viewer.client.min.js +1 -0
- package/dist/viewer.css +208 -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') {
|
|
@@ -1041,6 +1100,11 @@ function handleTreeKey(event) {
|
|
|
1041
1100
|
if (event.key === 'ArrowUp') { event.preventDefault(); focusTree(treeFocusIndex - 1); return true; }
|
|
1042
1101
|
if (event.key === 'PageDown') { event.preventDefault(); focusTree(treeFocusIndex + treePageSize()); return true; }
|
|
1043
1102
|
if (event.key === 'PageUp') { event.preventDefault(); focusTree(treeFocusIndex - treePageSize()); return true; }
|
|
1103
|
+
if (event.key === 'Enter' && event.altKey) {
|
|
1104
|
+
event.preventDefault();
|
|
1105
|
+
if (row && typeof openTreeRowMenu === 'function') openTreeRowMenu(row); // copy path / Finder / terminal
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1044
1108
|
if (event.key === 'Enter') {
|
|
1045
1109
|
event.preventDefault();
|
|
1046
1110
|
if (row && row.classList.contains('file-link')) { row.click(); clearTreeFocus(); }
|
|
@@ -1078,6 +1142,9 @@ function handleTreeKey(event) {
|
|
|
1078
1142
|
function isFloatingModalOpen() {
|
|
1079
1143
|
var sm = document.getElementById('settings-modal');
|
|
1080
1144
|
if (sm && !sm.classList.contains('hidden')) return true;
|
|
1145
|
+
var hv = document.getElementById('history-view');
|
|
1146
|
+
if (hv && !hv.classList.contains('hidden')) return true; // history overlay owns the keys (Esc/filter/click)
|
|
1147
|
+
if (document.getElementById('goto-line')) return true; // go-to-line prompt owns the keys until Enter/Esc
|
|
1081
1148
|
// The merged/memo panels are now docked (inline), not overlays — but while one OWNS focus we still stand
|
|
1082
1149
|
// down the global nav shortcuts so typing / ▲▼ inside it isn't hijacked. Focus elsewhere -> shortcuts run.
|
|
1083
1150
|
return isDockFocused();
|
|
@@ -1096,27 +1163,33 @@ document.addEventListener('keydown', (event) => {
|
|
|
1096
1163
|
// and +. open the merged views; Cmd/Ctrl+Shift+N toggles the memo. (Match event.code so IME/layout never
|
|
1097
1164
|
// swallows the combo.) Settings is a true overlay, so these stand down while it is up.
|
|
1098
1165
|
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') {
|
|
1166
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.code === 'Quote') {
|
|
1100
1167
|
event.preventDefault();
|
|
1101
1168
|
toggleDockMaximized();
|
|
1102
1169
|
return;
|
|
1103
1170
|
}
|
|
1104
|
-
if (!settingsUp && (event.metaKey || event.ctrlKey) && (event.code === 'Slash' || event.code === 'Period' || event.key === '?' || event.key === '>')) {
|
|
1171
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && !event.altKey && (event.code === 'Slash' || event.code === 'Period' || event.key === '?' || event.key === '>')) {
|
|
1105
1172
|
event.preventDefault();
|
|
1106
1173
|
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
1107
1174
|
return;
|
|
1108
1175
|
}
|
|
1109
|
-
if (!settingsUp && (event.metaKey || event.ctrlKey) && event.shiftKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
1176
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
1110
1177
|
event.preventDefault();
|
|
1111
1178
|
openMemoView();
|
|
1112
1179
|
return;
|
|
1113
1180
|
}
|
|
1181
|
+
// Cmd/Ctrl+9 toggles the git history view (above the focus guard so a 2nd press closes it from inside).
|
|
1182
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.code === 'Digit9' || event.key === '9') && typeof toggleHistory === 'function') {
|
|
1183
|
+
event.preventDefault();
|
|
1184
|
+
toggleHistory();
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1114
1187
|
|
|
1115
1188
|
// Settings overlay (or a focused merged/memo dock) captures keys: stand down the rest of the global
|
|
1116
1189
|
// shortcuts (Cmd+1, F7, Cmd+[/], Cmd+B, …). Each has its own Esc + editing handlers.
|
|
1117
1190
|
if (isFloatingModalOpen()) return;
|
|
1118
1191
|
|
|
1119
|
-
if ((event.metaKey || event.ctrlKey) && event.key === '1') {
|
|
1192
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '1') {
|
|
1120
1193
|
event.preventDefault();
|
|
1121
1194
|
// Coming from the diff: open the file you were viewing as source so Cmd+1 lands ON it (not a stale/blank
|
|
1122
1195
|
// source pane), and the tree below points at the same file. Capture the path BEFORE openSourceFile flips
|
|
@@ -1131,7 +1204,25 @@ document.addEventListener('keydown', (event) => {
|
|
|
1131
1204
|
focusOpenFileInTree();
|
|
1132
1205
|
return;
|
|
1133
1206
|
}
|
|
1134
|
-
|
|
1207
|
+
// Cmd/Ctrl+L = go to line (numeric prompt); Cmd/Ctrl+K = copy the caret's file:line. Skip when an
|
|
1208
|
+
// editable field owns focus (a comment composer textarea) so we don't hijack the user's typing.
|
|
1209
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'l' || event.key === 'L')) {
|
|
1210
|
+
var lkae = document.activeElement;
|
|
1211
|
+
if (!(lkae && (lkae.tagName === 'INPUT' || lkae.tagName === 'TEXTAREA' || lkae.tagName === 'SELECT'))) {
|
|
1212
|
+
event.preventDefault();
|
|
1213
|
+
openGotoLine();
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'k' || event.key === 'K')) {
|
|
1218
|
+
var kkae = document.activeElement;
|
|
1219
|
+
if (!(kkae && (kkae.tagName === 'INPUT' || kkae.tagName === 'TEXTAREA' || kkae.tagName === 'SELECT'))) {
|
|
1220
|
+
event.preventDefault();
|
|
1221
|
+
copyCaretLocation();
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '0') {
|
|
1135
1226
|
event.preventDefault();
|
|
1136
1227
|
setTab('changes');
|
|
1137
1228
|
focusOpenFileInTree();
|
|
@@ -1217,7 +1308,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1217
1308
|
// PageUp/Down scroll the diff/source view. There's no focusable scroller (the diff caret is a JS cursor),
|
|
1218
1309
|
// and d2h-file-side-diff's horizontal scrollport even swallows vertical wheel, so handle paging explicitly.
|
|
1219
1310
|
// 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) {
|
|
1311
|
+
if (treeFocusIndex < 0 && (event.key === 'PageDown' || event.key === 'PageUp') && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) {
|
|
1221
1312
|
var psc = isDiffViewVisible() ? document.getElementById('diff2html-container') : (isSourceViewerVisible() ? document.getElementById('source-body') : null);
|
|
1222
1313
|
if (psc) { event.preventDefault(); psc.scrollTop += (event.key === 'PageDown' ? 0.9 : -0.9) * psc.clientHeight; return; }
|
|
1223
1314
|
}
|
|
@@ -1248,12 +1339,12 @@ document.addEventListener('keydown', (event) => {
|
|
|
1248
1339
|
lastShiftSide = side;
|
|
1249
1340
|
}
|
|
1250
1341
|
|
|
1251
|
-
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key.toLowerCase() === 'f') {
|
|
1342
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.key.toLowerCase() === 'f') {
|
|
1252
1343
|
event.preventDefault();
|
|
1253
1344
|
openQuickOpen('content');
|
|
1254
1345
|
return;
|
|
1255
1346
|
}
|
|
1256
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'e') {
|
|
1347
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key.toLowerCase() === 'e') {
|
|
1257
1348
|
event.preventDefault();
|
|
1258
1349
|
openQuickOpen('recent');
|
|
1259
1350
|
return;
|
|
@@ -1268,14 +1359,14 @@ document.addEventListener('keydown', (event) => {
|
|
|
1268
1359
|
}
|
|
1269
1360
|
}
|
|
1270
1361
|
|
|
1271
|
-
if ((event.metaKey || event.ctrlKey) && event.key === 'ArrowDown') {
|
|
1362
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === 'ArrowDown') {
|
|
1272
1363
|
event.preventDefault();
|
|
1273
1364
|
if (isSourceViewerVisible()) goToSymbolUnderCursor();
|
|
1274
1365
|
else openDiffFileAtCaret();
|
|
1275
1366
|
return;
|
|
1276
1367
|
}
|
|
1277
1368
|
|
|
1278
|
-
if ((event.metaKey || event.ctrlKey) && (event.key === 'b' || event.key === 'B')) {
|
|
1369
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'b' || event.key === 'B')) {
|
|
1279
1370
|
var aeB = document.activeElement;
|
|
1280
1371
|
if (aeB && (aeB.tagName === 'INPUT' || aeB.tagName === 'TEXTAREA' || aeB.tagName === 'SELECT')) return;
|
|
1281
1372
|
event.preventDefault();
|
|
@@ -1337,7 +1428,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1337
1428
|
}
|
|
1338
1429
|
}
|
|
1339
1430
|
|
|
1340
|
-
if (event.key === 'F7') {
|
|
1431
|
+
if (event.key === 'F7' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
1341
1432
|
event.preventDefault();
|
|
1342
1433
|
const delta = event.shiftKey ? -1 : 1;
|
|
1343
1434
|
const sourceViewer = document.getElementById('source-viewer');
|
|
@@ -1414,6 +1505,20 @@ document.querySelectorAll('.tab').forEach((button) => {
|
|
|
1414
1505
|
button.addEventListener('click', () => setTab(button.dataset.tab || 'changes'));
|
|
1415
1506
|
});
|
|
1416
1507
|
|
|
1508
|
+
// Activity rail (IntelliJ-style): click an icon to navigate/toggle its view. Terminal + settings buttons
|
|
1509
|
+
// carry no data-view — they keep their own id-based handlers (terminal toggle / settings gear).
|
|
1510
|
+
document.querySelector('.activity-rail')?.addEventListener('click', (event) => {
|
|
1511
|
+
const btn = event.target.closest && event.target.closest('.rail-btn[data-view]');
|
|
1512
|
+
if (!btn) return;
|
|
1513
|
+
const view = btn.dataset.view;
|
|
1514
|
+
if (view === 'changes') { setTab('changes'); if (!isDiffViewVisible()) showDiffView(false); }
|
|
1515
|
+
else if (view === 'files') { setTab('files'); }
|
|
1516
|
+
else if (view === 'q' || view === 'c') { toggleMergedRail(view); }
|
|
1517
|
+
else if (view === 'memo') { openMemoView(); } // openMemoView already toggles
|
|
1518
|
+
else if (view === 'history') { toggleHistory(); }
|
|
1519
|
+
syncRail();
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1417
1522
|
document.getElementById('back-to-diff')?.addEventListener('click', () => showDiffView(true));
|
|
1418
1523
|
document.getElementById('source-tabs')?.addEventListener('click', function (event) {
|
|
1419
1524
|
var closeBtn = event.target && event.target.closest && event.target.closest('.source-tab-close');
|
|
@@ -1450,6 +1555,7 @@ if (!restored) {
|
|
|
1450
1555
|
else openDefaultSourceFile();
|
|
1451
1556
|
}
|
|
1452
1557
|
initSourceTreeFolds();
|
|
1558
|
+
syncRail(); // reflect the initial view on the activity rail
|
|
1453
1559
|
// Electron receives live updates over IPC (monacoriMenu.onDiffUpdate); only serve/browser needs the HTTP
|
|
1454
1560
|
// poller. Under file:// its fetch just fails every 1.5s for the app's whole life, so skip it in Electron.
|
|
1455
1561
|
if (watchEnabled && !(window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function')) {
|
|
@@ -1485,7 +1591,10 @@ window.addEventListener('beforeunload', saveUiState);
|
|
|
1485
1591
|
});
|
|
1486
1592
|
document.addEventListener('mousemove', (event) => {
|
|
1487
1593
|
if (!resizing) return;
|
|
1488
|
-
|
|
1594
|
+
// Subtract the activity rail's width: the sidebar starts to its right, so its width is the cursor X
|
|
1595
|
+
// minus the rail offset (not clientX itself, which would over-size it by the rail width).
|
|
1596
|
+
const railW = parseFloat(getComputedStyle(document.body).getPropertyValue('--rail-width')) || 0;
|
|
1597
|
+
const width = Math.min(640, Math.max(180, event.clientX - railW));
|
|
1489
1598
|
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
|
|
1490
1599
|
});
|
|
1491
1600
|
document.addEventListener('mouseup', () => {
|
|
@@ -1671,6 +1780,7 @@ function setDiffCursor(path, side, rowIndex, column, reveal) {
|
|
|
1671
1780
|
var col = Math.max(0, Math.min(column, diffLineText(rows[ri]).length));
|
|
1672
1781
|
diffCursor = { path: path, side: side, rowIndex: ri, column: col };
|
|
1673
1782
|
pendingFileBoundary = null; // any caret move re-arms the last-change announcement for the next F7 (see next)
|
|
1783
|
+
hideCaretHint(); // caret moved (incl. crossing to the next file) → drop the "last change" hint so it never covers the new file
|
|
1674
1784
|
diffSelectionAnchor = null; // any direct caret placement (click/F7/Cmd-arrow) drops the selection; Shift+Arrow re-sets it
|
|
1675
1785
|
if (reveal) {
|
|
1676
1786
|
// Render the caret AND scroll in the SAME animation frame. A fast key-repeat queues several ArrowDowns
|
|
@@ -1911,6 +2021,28 @@ function showToast(message) {
|
|
|
1911
2021
|
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
|
|
1912
2022
|
}, 4500);
|
|
1913
2023
|
}
|
|
2024
|
+
// Inline hint anchored just under the diff caret — used for the F7 "last change" boundary announcement so the
|
|
2025
|
+
// message appears where the user is looking and fades on its own (unlike the corner toast). Falls back to the
|
|
2026
|
+
// corner toast when there's no on-screen caret (e.g. source view).
|
|
2027
|
+
var caretHintEl = null, caretHintTimer = 0;
|
|
2028
|
+
function showCaretHint(message) {
|
|
2029
|
+
var row = activeDiffRow || document.querySelector('#diff2html-container .diff-active-row');
|
|
2030
|
+
if (!row || !row.getBoundingClientRect) { showToast(message); return; }
|
|
2031
|
+
if (!caretHintEl) { caretHintEl = document.createElement('div'); caretHintEl.className = 'mc-caret-hint'; document.body.appendChild(caretHintEl); }
|
|
2032
|
+
caretHintEl.textContent = message;
|
|
2033
|
+
var r = row.getBoundingClientRect();
|
|
2034
|
+
caretHintEl.style.left = Math.round(Math.max(8, r.left)) + 'px';
|
|
2035
|
+
caretHintEl.style.top = Math.round(r.bottom + 4) + 'px';
|
|
2036
|
+
caretHintEl.classList.remove('show');
|
|
2037
|
+
void caretHintEl.offsetWidth; // reflow so the fade-in re-triggers on rapid repeat presses
|
|
2038
|
+
caretHintEl.classList.add('show');
|
|
2039
|
+
if (caretHintTimer) clearTimeout(caretHintTimer);
|
|
2040
|
+
caretHintTimer = setTimeout(function () { if (caretHintEl) caretHintEl.classList.remove('show'); }, 2000);
|
|
2041
|
+
}
|
|
2042
|
+
function hideCaretHint() {
|
|
2043
|
+
if (caretHintTimer) { clearTimeout(caretHintTimer); caretHintTimer = 0; }
|
|
2044
|
+
if (caretHintEl) caretHintEl.classList.remove('show');
|
|
2045
|
+
}
|
|
1914
2046
|
// Follow each comment to its snapshot line (c.code) in the current content: same line if unchanged, else the
|
|
1915
2047
|
// nearest exact match of that line. A comment is NEVER auto-deleted. If its line can't be found we leave it
|
|
1916
2048
|
// where it is — this happens routinely WITHOUT the file changing: a comment anchored to a deleted/old-side
|
|
@@ -2405,6 +2537,12 @@ function applyDockMaximized() {
|
|
|
2405
2537
|
document.body.classList.toggle('dock-maximized', dockMaximized);
|
|
2406
2538
|
}
|
|
2407
2539
|
function toggleDockMaximized() {
|
|
2540
|
+
// Maximize only the panel you're FOCUSED in: the merged/memo dock (.dock-panel) or the terminal
|
|
2541
|
+
// (.terminal-panel). From the sidebar tree (treeFocusIndex >= 0) or the diff/source content this is a
|
|
2542
|
+
// no-op — pressing it there must NOT maximize a terminal you aren't actually in.
|
|
2543
|
+
if (treeFocusIndex >= 0) return;
|
|
2544
|
+
var ae = document.activeElement;
|
|
2545
|
+
if (!(ae && ae.closest && (ae.closest('.dock-panel') || ae.closest('.terminal-panel')))) return;
|
|
2408
2546
|
if (!activeDockPanel()) return; // nothing docked -> nothing to maximize
|
|
2409
2547
|
dockMaximized = !dockMaximized;
|
|
2410
2548
|
applyDockMaximized();
|
|
@@ -2423,6 +2561,7 @@ function closeMergedMemoDocks() {
|
|
|
2423
2561
|
// terminal dock but never for these floating panels.
|
|
2424
2562
|
document.body.classList.toggle('floating-dock', !!(document.getElementById('mc-merged-panel') || document.getElementById('mc-memo-panel')));
|
|
2425
2563
|
applyDockMaximized();
|
|
2564
|
+
if (typeof syncRail === 'function') syncRail(); // clear the rail icon for the closed dock(s)
|
|
2426
2565
|
}
|
|
2427
2566
|
window.__monacoriCloseDocks = closeMergedMemoDocks;
|
|
2428
2567
|
// Retry-focus a docked field (Electron async-restores focus to <body>, so a one-shot focus can lose the race).
|
|
@@ -2507,6 +2646,7 @@ function mountDock(id, titleText) {
|
|
|
2507
2646
|
document.body.classList.add('dock-open');
|
|
2508
2647
|
document.body.classList.add('floating-dock'); // scopes the maximize CSS so it doesn't hide the diff
|
|
2509
2648
|
applyDockMaximized();
|
|
2649
|
+
if (typeof syncRail === 'function') syncRail(); // light up the rail icon for the opened dock
|
|
2510
2650
|
return { panel: panel, body: body, bar: bar, close: close };
|
|
2511
2651
|
}
|
|
2512
2652
|
|
|
@@ -2681,6 +2821,7 @@ refreshComments();
|
|
|
2681
2821
|
|
|
2682
2822
|
function setActive(p) {
|
|
2683
2823
|
active = p;
|
|
2824
|
+
if (p && p.labelEl) p.labelEl.classList.remove('has-bell'); // viewing the pane clears its bell badge
|
|
2684
2825
|
panes.forEach(function (q) {
|
|
2685
2826
|
q.el.classList.toggle('is-active', q === p);
|
|
2686
2827
|
// 2+ panes: dim every pane but the active one (no border, just a clean focus cue). A lone pane stays full.
|
|
@@ -2694,6 +2835,11 @@ refreshComments();
|
|
|
2694
2835
|
});
|
|
2695
2836
|
}
|
|
2696
2837
|
|
|
2838
|
+
function copyToClipboard(text) {
|
|
2839
|
+
if (!text) return;
|
|
2840
|
+
try { if (window.monacoriClipboard && window.monacoriClipboard.write) { window.monacoriClipboard.write(text); return; } } catch (e) {}
|
|
2841
|
+
try { if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text); } catch (e) {}
|
|
2842
|
+
}
|
|
2697
2843
|
function makePane() {
|
|
2698
2844
|
if (!ensureXterm()) return null; // xterm unavailable — leave the panel empty rather than throw
|
|
2699
2845
|
var el = document.createElement('div');
|
|
@@ -2729,6 +2875,9 @@ refreshComments();
|
|
|
2729
2875
|
// Match the PHYSICAL key (e.code), not e.key: under a non-Latin layout/IME (e.g. Korean 한글)
|
|
2730
2876
|
// Cmd+V reports e.key as 'ㅍ', so a key-based check misses it — blurring the terminal and
|
|
2731
2877
|
// breaking paste/copy/cut/select-all whenever the Korean input source is active.
|
|
2878
|
+
// Cmd+C with a terminal selection: copy it ourselves — xterm doesn't auto-copy and the menu/native
|
|
2879
|
+
// copy misses xterm's own selection, so Cmd+C silently did nothing. No selection -> fall through.
|
|
2880
|
+
if (e.code === 'KeyC' && term.hasSelection && term.hasSelection()) { copyToClipboard(term.getSelection()); return false; }
|
|
2732
2881
|
if (e.code === 'KeyC' || e.code === 'KeyV' || e.code === 'KeyX' || e.code === 'KeyA') return true;
|
|
2733
2882
|
try { term.blur(); } catch (x) {}
|
|
2734
2883
|
return false;
|
|
@@ -2736,6 +2885,14 @@ refreshComments();
|
|
|
2736
2885
|
return true;
|
|
2737
2886
|
});
|
|
2738
2887
|
term.onData(function (d) { if (pane.id != null) window.monacoriPty.write({ id: pane.id, data: d }); });
|
|
2888
|
+
// Bell from the pane's TUI (e.g. Claude Code finished a turn / needs input): badge the pane when it isn't
|
|
2889
|
+
// the one you're looking at, and ask the main process to raise a native notification when the whole window
|
|
2890
|
+
// isn't focused. Toggle in Settings ("Notify when a terminal task finishes").
|
|
2891
|
+
term.onBell(function () {
|
|
2892
|
+
if (pane !== active && pane.labelEl) pane.labelEl.classList.add('has-bell');
|
|
2893
|
+
if (persistRead('monacori-terminal-bell-notify') === false) return; // OS notifications disabled
|
|
2894
|
+
try { window.monacoriPty.bell({ title: 'monacori', body: pane.name + ' — ' + t('notify.bellBody') }); } catch (e) {}
|
|
2895
|
+
});
|
|
2739
2896
|
el.addEventListener('mousedown', function (e) { if (e.target !== labelEl) setActive(pane); });
|
|
2740
2897
|
labelEl.addEventListener('dblclick', function () { renamePane(pane); });
|
|
2741
2898
|
panes.push(pane);
|
|
@@ -2783,10 +2940,12 @@ refreshComments();
|
|
|
2783
2940
|
}
|
|
2784
2941
|
|
|
2785
2942
|
function removePane(id) {
|
|
2786
|
-
var
|
|
2787
|
-
|
|
2943
|
+
for (var k = 0; k < panes.length; k++) { if (panes[k].id === id) { removePaneRef(panes[k]); return; } }
|
|
2944
|
+
}
|
|
2945
|
+
// Remove a pane by object reference (handles panes whose pty id hasn't arrived yet — spawn is async).
|
|
2946
|
+
function removePaneRef(p) {
|
|
2947
|
+
var i = panes.indexOf(p);
|
|
2788
2948
|
if (i < 0) return;
|
|
2789
|
-
var p = panes[i];
|
|
2790
2949
|
try { p.term.dispose(); } catch (e) {}
|
|
2791
2950
|
if (p.el.parentNode) p.el.parentNode.removeChild(p.el);
|
|
2792
2951
|
panes.splice(i, 1);
|
|
@@ -2794,6 +2953,15 @@ refreshComments();
|
|
|
2794
2953
|
if (panes.length === 0) setOpen(false);
|
|
2795
2954
|
else fitAll();
|
|
2796
2955
|
}
|
|
2956
|
+
// Cmd/Ctrl+W inside the terminal: close just the FOCUSED pane (kill its pty), not the whole panel. The
|
|
2957
|
+
// last pane closing collapses the panel via removePaneRef -> setOpen(false). Remove the pane immediately
|
|
2958
|
+
// (don't wait for the pty's onExit) so the UI responds at once; the later onExit -> removePane no-ops.
|
|
2959
|
+
function closeActivePane() {
|
|
2960
|
+
var p = active || panes[panes.length - 1];
|
|
2961
|
+
if (!p) { setOpen(false); return; }
|
|
2962
|
+
if (p.id != null) { try { window.monacoriPty.kill({ id: p.id }); } catch (e) {} }
|
|
2963
|
+
removePaneRef(p);
|
|
2964
|
+
}
|
|
2797
2965
|
|
|
2798
2966
|
function split() {
|
|
2799
2967
|
if (panes.length >= MAX_PANES) return;
|
|
@@ -2927,8 +3095,12 @@ refreshComments();
|
|
|
2927
3095
|
}, true);
|
|
2928
3096
|
window.__monacoriTerminal = {
|
|
2929
3097
|
isOpen: isOpen,
|
|
3098
|
+
// True when keyboard focus is inside the terminal panel (a pane owns it) — Cmd/Ctrl+W uses this to
|
|
3099
|
+
// decide between closing a pane and closing a source tab.
|
|
3100
|
+
hasFocus: function () { var ae = document.activeElement; return !!(ae && panel.contains(ae)); },
|
|
2930
3101
|
open: function () { setOpen(true); },
|
|
2931
3102
|
paneCount: function () { return panes.length; },
|
|
3103
|
+
closeActivePane: closeActivePane,
|
|
2932
3104
|
enterSendMode: enterSendMode,
|
|
2933
3105
|
send: function (text) { writeToPane(active || panes[0], text); },
|
|
2934
3106
|
sendToPane: function (i, text) { writeToPane(panes[i] || active || panes[0], text); },
|
|
@@ -2956,10 +3128,11 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function
|
|
|
2956
3128
|
window.monacoriMenu.onDiffUpdate(function (html) { try { applyDiffUpdate(html); } catch (e) {} });
|
|
2957
3129
|
}
|
|
2958
3130
|
if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function') {
|
|
2959
|
-
// Cmd/Ctrl+W: close the
|
|
3131
|
+
// Cmd/Ctrl+W: close whatever the focus is on. A focused terminal pane closes just that pane (the last
|
|
3132
|
+
// pane collapses the panel); otherwise close the active Files-mode tab (no-op outside the source viewer).
|
|
2960
3133
|
window.monacoriMenu.onCloseTab(function () {
|
|
2961
|
-
|
|
2962
|
-
if (
|
|
3134
|
+
var term = window.__monacoriTerminal;
|
|
3135
|
+
if (term && term.isOpen() && term.hasFocus()) { term.closeActivePane(); return; }
|
|
2963
3136
|
if (isSourceViewerVisible()) closeActiveSourceTab();
|
|
2964
3137
|
});
|
|
2965
3138
|
}
|
|
@@ -3070,6 +3243,12 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
|
|
|
3070
3243
|
if (qta) qta.addEventListener('input', function () { saveMergePrompt('q', qta.value); flash(); });
|
|
3071
3244
|
if (cta) cta.addEventListener('input', function () { saveMergePrompt('c', cta.value); flash(); });
|
|
3072
3245
|
if (resetBtn) resetBtn.addEventListener('click', function () { saveMergePrompt('q', ''); saveMergePrompt('c', ''); fill(); flash(); });
|
|
3246
|
+
// Terminal-bell notification toggle (default ON — persistRead returns undefined when never set).
|
|
3247
|
+
var bellCb = document.getElementById('set-bell-notify');
|
|
3248
|
+
if (bellCb) {
|
|
3249
|
+
bellCb.checked = persistRead('monacori-terminal-bell-notify') !== false;
|
|
3250
|
+
bellCb.addEventListener('change', function () { persistSave('monacori-terminal-bell-notify', bellCb.checked); });
|
|
3251
|
+
}
|
|
3073
3252
|
// Language: live-switch the whole UI (no reload). Persist, re-apply the static chrome, then re-render
|
|
3074
3253
|
// any currently-shown dynamic text (open composer / merged modal / index status) so it follows too.
|
|
3075
3254
|
langSelectRef = setupCustomSelect('settings-language',
|
|
@@ -3103,6 +3282,31 @@ function setTab(name) {
|
|
|
3103
3282
|
});
|
|
3104
3283
|
document.getElementById('changes-panel')?.classList.toggle('hidden', name !== 'changes');
|
|
3105
3284
|
document.getElementById('files-panel')?.classList.toggle('hidden', name !== 'files');
|
|
3285
|
+
syncRail();
|
|
3286
|
+
}
|
|
3287
|
+
// Reflect the current view/dock state on the activity rail icons (active highlight). Terminal active is
|
|
3288
|
+
// kept in sync separately by the dock-terminal setOpen (it toggles is-active on #terminal-toggle).
|
|
3289
|
+
function syncRail() {
|
|
3290
|
+
var rail = document.querySelector('.activity-rail');
|
|
3291
|
+
if (!rail) return;
|
|
3292
|
+
var setOn = function (view, on) {
|
|
3293
|
+
var btn = rail.querySelector('[data-view="' + view + '"]');
|
|
3294
|
+
if (btn) btn.classList.toggle('is-active', !!on);
|
|
3295
|
+
};
|
|
3296
|
+
setOn('changes', !document.getElementById('changes-panel')?.classList.contains('hidden'));
|
|
3297
|
+
setOn('files', !document.getElementById('files-panel')?.classList.contains('hidden'));
|
|
3298
|
+
var merged = document.getElementById('mc-merged-panel');
|
|
3299
|
+
setOn('q', !!(merged && merged.dataset.kind === 'q'));
|
|
3300
|
+
setOn('c', !!(merged && merged.dataset.kind === 'c'));
|
|
3301
|
+
setOn('memo', !!document.getElementById('mc-memo-panel'));
|
|
3302
|
+
var hv = document.getElementById('history-view');
|
|
3303
|
+
setOn('history', !!(hv && !hv.classList.contains('hidden')));
|
|
3304
|
+
}
|
|
3305
|
+
// Rail click for the merged views toggles: a 2nd click on the open kind closes it (memo already toggles).
|
|
3306
|
+
function toggleMergedRail(kind) {
|
|
3307
|
+
var m = document.getElementById('mc-merged-panel');
|
|
3308
|
+
if (m && m.dataset.kind === kind) { closeMergedMemoDocks(); return; }
|
|
3309
|
+
openMergedView(kind);
|
|
3106
3310
|
}
|
|
3107
3311
|
// Big repos ship the source tree as an inert island (see render.ts); build it the first time the Files
|
|
3108
3312
|
// tab is opened so the (potentially huge) tree never blocks startup. No-op for inline (small) trees.
|
|
@@ -3218,6 +3422,10 @@ function applyDiffUpdate(u) {
|
|
|
3218
3422
|
var wasSource = isSourceViewerVisible();
|
|
3219
3423
|
var container = document.getElementById('diff2html-container');
|
|
3220
3424
|
var diffScrollTop = container ? container.scrollTop : 0;
|
|
3425
|
+
// The active hunk's file path BEFORE the swap (hunkMeta/hunks still hold the old build here). After a commit
|
|
3426
|
+
// the old active file can vanish from the new diff, so we re-anchor `current` to it below — otherwise it
|
|
3427
|
+
// dangles at a stale index and showDiffView renders blank with a stale breadcrumb.
|
|
3428
|
+
var prevActivePath = current >= 0 ? hunkPathAt(current) : '';
|
|
3221
3429
|
// Did the file the user is CURRENTLY viewing actually change in this build? If not, we must not re-render
|
|
3222
3430
|
// the source view — an unrelated file's edit would otherwise flicker the pane they're reading. Capture the
|
|
3223
3431
|
// open file's signature BEFORE fileSignatureByPath is rebuilt below.
|
|
@@ -3248,6 +3456,13 @@ function applyDiffUpdate(u) {
|
|
|
3248
3456
|
if (filesPanel && (!REVIEW_LAZY || filesPanel.innerHTML.trim())) filesPanel.innerHTML = u.filesTree || '';
|
|
3249
3457
|
var statusEl = document.querySelector('.review-status');
|
|
3250
3458
|
if (statusEl) statusEl.innerHTML = u.reviewStatus || '';
|
|
3459
|
+
// Branch can change between watch ticks (checkout/commit) — keep the sidebar chip current.
|
|
3460
|
+
var branchName = document.getElementById('brand-branch-name');
|
|
3461
|
+
if (branchName) {
|
|
3462
|
+
branchName.textContent = u.branch || '';
|
|
3463
|
+
var branchChip = branchName.closest && branchName.closest('.brand-branch');
|
|
3464
|
+
if (branchChip) branchChip.classList.toggle('hidden', !u.branch);
|
|
3465
|
+
}
|
|
3251
3466
|
if (reviewMeta) { reviewMeta.setAttribute('data-signature', u.signature); if (u.generatedAt) reviewMeta.setAttribute('data-generated-at', u.generatedAt); }
|
|
3252
3467
|
|
|
3253
3468
|
// 2) Re-derive module-level state directly from the payload objects.
|
|
@@ -3264,6 +3479,16 @@ function applyDiffUpdate(u) {
|
|
|
3264
3479
|
links = Array.from(document.querySelectorAll('#changes-panel .file-link'));
|
|
3265
3480
|
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
3266
3481
|
|
|
3482
|
+
// Reconcile the active hunk against the new build (uses the just-rebuilt `links`). A committed/removed file
|
|
3483
|
+
// reshuffles or shrinks the diff: re-anchor `current` to the same file's new hunk when it survives, else
|
|
3484
|
+
// drop to -1 so the diff lands on the first change rather than a dangling index that paints nothing.
|
|
3485
|
+
var activeFilePreserved = false;
|
|
3486
|
+
if (prevActivePath) {
|
|
3487
|
+
var reHunk = firstHunkForPath(prevActivePath);
|
|
3488
|
+
if (reHunk >= 0) { current = reHunk; activeFilePreserved = true; }
|
|
3489
|
+
else current = -1;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3267
3492
|
// 3) Reset lazy-materialize + index state so the new diff bodies / source / symbols rebuild on demand.
|
|
3268
3493
|
// bodyCache is keyed by file INDEX, not content — after a watch rebuild the same index maps to the new
|
|
3269
3494
|
// body, so it MUST be dropped too. Clearing only bodyPromise left loadBodyHtml() returning the cached
|
|
@@ -3277,14 +3502,11 @@ function applyDiffUpdate(u) {
|
|
|
3277
3502
|
// sourceBodyPath so the already-painted (unchanged) source view is left exactly as-is — no flicker.
|
|
3278
3503
|
if (openFileChanged) sourceBodyPath = null;
|
|
3279
3504
|
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
3505
|
|
|
3284
3506
|
// 3b) Re-fill UNCHANGED files' bodies synchronously from the snapshot so they don't blank-then-reload (the
|
|
3285
|
-
// flicker).
|
|
3286
|
-
//
|
|
3287
|
-
//
|
|
3507
|
+
// flicker). Runs BEFORE setupLazyDiff so the IntersectionObserver sees them already materialized and never
|
|
3508
|
+
// re-fetches them. The fresh wrapper carries the correct data-first-hunk + file index, so materializeBody
|
|
3509
|
+
// numbers hunks exactly as a normal lazy load would. Changed/new files stay shells and lazy-load as usual.
|
|
3288
3510
|
if (REVIEW_LAZY && container) {
|
|
3289
3511
|
container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
|
|
3290
3512
|
var p = diffWrapperPathKey(w);
|
|
@@ -3298,6 +3520,10 @@ function applyDiffUpdate(u) {
|
|
|
3298
3520
|
bodyPromise[idx] = Promise.resolve(w);
|
|
3299
3521
|
});
|
|
3300
3522
|
}
|
|
3523
|
+
refreshHunkIndex(); // rebuild hunks/hunkMeta from the swapped-in DOM so hunkTotal()/hunkPathAt() aren't stale
|
|
3524
|
+
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
3525
|
+
else { diffBootDone = true; }
|
|
3526
|
+
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0);
|
|
3301
3527
|
|
|
3302
3528
|
// 4) Re-run the DOM-dependent bootstrap steps.
|
|
3303
3529
|
applyI18n();
|
|
@@ -3312,7 +3538,10 @@ function applyDiffUpdate(u) {
|
|
|
3312
3538
|
if (openFileChanged) openSourceFile(openPath, false);
|
|
3313
3539
|
} else if (container) {
|
|
3314
3540
|
showDiffView(false);
|
|
3315
|
-
|
|
3541
|
+
// Same active file survived → keep the user's exact scroll. If it was committed away (current reset to
|
|
3542
|
+
// -1, showDiffView landed on the first change), restoring the old, now-out-of-range scrollTop would push
|
|
3543
|
+
// the shorter new diff off-screen and look blank — so reset to the top instead.
|
|
3544
|
+
container.scrollTop = activeFilePreserved ? diffScrollTop : 0;
|
|
3316
3545
|
}
|
|
3317
3546
|
return true;
|
|
3318
3547
|
}
|
|
@@ -3961,6 +4190,39 @@ function showUsages(name, count) {
|
|
|
3961
4190
|
if (title) title.textContent = count + ' usage' + (count === 1 ? '' : 's') + ' of ' + name;
|
|
3962
4191
|
renderUsages();
|
|
3963
4192
|
box.classList.remove('hidden');
|
|
4193
|
+
positionUsagesAtCaret();
|
|
4194
|
+
}
|
|
4195
|
+
// Anchor the usages popup just below (or above, if cramped) the live caret — source OR diff both render a
|
|
4196
|
+
// `.code-cursor` span. No caret on screen → leave the centered overlay fallback in place.
|
|
4197
|
+
function positionUsagesAtCaret() {
|
|
4198
|
+
var box = document.getElementById('usages');
|
|
4199
|
+
if (!box) return;
|
|
4200
|
+
var panel = box.querySelector('.quick-open-panel');
|
|
4201
|
+
if (!panel) return;
|
|
4202
|
+
resetUsagesAnchor(box, panel); // measure from a clean slate
|
|
4203
|
+
var caret = document.querySelector('#source-body .code-cursor') || document.querySelector('#diff2html-container .code-cursor');
|
|
4204
|
+
if (!caret) return;
|
|
4205
|
+
var rect = caret.getBoundingClientRect();
|
|
4206
|
+
if (!rect.height && !rect.width && !rect.top) return; // detached / off-layout
|
|
4207
|
+
var vw = window.innerWidth, vh = window.innerHeight, gap = 6, margin = 8;
|
|
4208
|
+
var pw = Math.min(560, vw - margin * 2);
|
|
4209
|
+
var left = Math.min(Math.max(margin, rect.left), vw - pw - margin);
|
|
4210
|
+
box.classList.add('anchored');
|
|
4211
|
+
panel.style.width = pw + 'px';
|
|
4212
|
+
panel.style.left = left + 'px';
|
|
4213
|
+
var spaceBelow = vh - rect.bottom - gap - margin;
|
|
4214
|
+
var spaceAbove = rect.top - gap - margin;
|
|
4215
|
+
if (spaceBelow >= 200 || spaceBelow >= spaceAbove) {
|
|
4216
|
+
panel.style.top = (rect.bottom + gap) + 'px';
|
|
4217
|
+
panel.style.maxHeight = Math.max(120, spaceBelow) + 'px';
|
|
4218
|
+
} else {
|
|
4219
|
+
panel.style.bottom = (vh - rect.top + gap) + 'px';
|
|
4220
|
+
panel.style.maxHeight = Math.max(120, spaceAbove) + 'px';
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
function resetUsagesAnchor(box, panel) {
|
|
4224
|
+
box.classList.remove('anchored');
|
|
4225
|
+
panel.style.left = panel.style.top = panel.style.bottom = panel.style.width = panel.style.maxHeight = '';
|
|
3964
4226
|
}
|
|
3965
4227
|
function renderUsages() {
|
|
3966
4228
|
var results = document.getElementById('usages-results');
|
|
@@ -3998,7 +4260,11 @@ function openUsageItem(item) {
|
|
|
3998
4260
|
openSourceAt(item.path, item.lineIndex, item.column);
|
|
3999
4261
|
}
|
|
4000
4262
|
function closeUsages() {
|
|
4001
|
-
document.getElementById('usages')
|
|
4263
|
+
var box = document.getElementById('usages');
|
|
4264
|
+
if (!box) return;
|
|
4265
|
+
box.classList.add('hidden');
|
|
4266
|
+
var panel = box.querySelector('.quick-open-panel');
|
|
4267
|
+
if (panel) resetUsagesAnchor(box, panel); // clear inline anchoring so the next open re-measures cleanly
|
|
4002
4268
|
}
|
|
4003
4269
|
|
|
4004
4270
|
var symbolIndex = null; // Map<name, [{path,lineIndex,column}]>; built off-thread by a Web Worker, null until ready
|
|
@@ -4176,11 +4442,18 @@ function renderSourceTabs(activePath) {
|
|
|
4176
4442
|
var active = p === activePath;
|
|
4177
4443
|
return '<div class="source-tab' + (active ? ' active' : '') + '" data-tab-path="' + escapeHtml(p) + '" title="' + escapeHtml(p) + '">'
|
|
4178
4444
|
+ '<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 (
|
|
4445
|
+
+ '<button type="button" class="source-tab-close" data-close-path="' + escapeHtml(p) + '" aria-label="Close tab" title="Close (⌘W)">×</button>'
|
|
4180
4446
|
+ '</div>';
|
|
4181
4447
|
}).join('');
|
|
4448
|
+
// Scroll the tab bar HORIZONTALLY only. scrollIntoView() walks every scrollable ancestor — on rapid
|
|
4449
|
+
// Cmd+Shift+[/] cycling it nudged a vertical ancestor and clipped the tab strip at the top. Adjusting
|
|
4450
|
+
// bar.scrollLeft directly keeps the active tab in view without ever touching vertical scroll.
|
|
4182
4451
|
var act = bar.querySelector('.source-tab.active');
|
|
4183
|
-
if (act
|
|
4452
|
+
if (act) {
|
|
4453
|
+
var bl = bar.getBoundingClientRect(), al = act.getBoundingClientRect();
|
|
4454
|
+
if (al.left < bl.left) bar.scrollLeft -= (bl.left - al.left) + 8;
|
|
4455
|
+
else if (al.right > bl.right) bar.scrollLeft += (al.right - bl.right) + 8;
|
|
4456
|
+
}
|
|
4184
4457
|
}
|
|
4185
4458
|
function closeSourceTab(path) {
|
|
4186
4459
|
var idx = sourceTabs.indexOf(path);
|
|
@@ -4653,7 +4926,7 @@ function renderHttpTable(file) {
|
|
|
4653
4926
|
const reqIdx = hasRun ? runAtLine[index] : -1;
|
|
4654
4927
|
const isCursorLine = Boolean(cursor && cursor.lineIndex === index);
|
|
4655
4928
|
const gutter = hasRun
|
|
4656
|
-
? '<button type="button" class="http-run" data-req="' + reqIdx + '" title="Run request (
|
|
4929
|
+
? '<button type="button" class="http-run" data-req="' + reqIdx + '" title="Run request (⌘Enter / ⌥Enter)" aria-label="Run request">▶</button>'
|
|
4657
4930
|
: '';
|
|
4658
4931
|
rows += '<tr class="source-row http-row' + (hasRun ? ' http-request-line' : '') + (isCursorLine ? ' cursor-line' : '') + '" data-line-index="' + index + '">'
|
|
4659
4932
|
+ '<td class="num http-gutter">' + gutter + '<span class="num-text">' + (index + 1) + '</span></td>'
|
|
@@ -4937,3 +5210,328 @@ function formatBytes(bytes) {
|
|
|
4937
5210
|
if (kib < 1024) return kib.toFixed(1) + ' KiB';
|
|
4938
5211
|
return (kib / 1024).toFixed(1) + ' MiB';
|
|
4939
5212
|
}
|
|
5213
|
+
// ===== Git history view (Cmd+9): commit list with graph lanes + per-commit diff. =====
|
|
5214
|
+
// Data comes from the main process (window.monacoriGit.log / .commitDiff); the lane layout is computed
|
|
5215
|
+
// here from each commit's parents. Read-only — the per-commit diff is static diff2html HTML.
|
|
5216
|
+
|
|
5217
|
+
var HISTORY_LANE_W = 14, HISTORY_DOT_R = 3.5, HISTORY_ROW_H = 24;
|
|
5218
|
+
var HISTORY_COLORS = ['#6c9fd4', '#7faf6b', '#d4a857', '#c77dd4', '#d36c6c', '#5bb6b6', '#b0884f', '#8d8df0'];
|
|
5219
|
+
var historyCommits = [];
|
|
5220
|
+
var historyGraph = [];
|
|
5221
|
+
var historyMaxLane = 0;
|
|
5222
|
+
var historyActiveSha = '';
|
|
5223
|
+
var historyLoading = false;
|
|
5224
|
+
|
|
5225
|
+
// Lane layout. Walks commits newest-first, tracking open edges (lanes) by the hash each expects next.
|
|
5226
|
+
// Returns per-row { hash, myLane, color, topEdges, bottomEdges } using LANE INDICES + COLOR INDICES (px-free,
|
|
5227
|
+
// so it's unit-testable). First parent inherits the commit's color so a branch keeps one hue down its line.
|
|
5228
|
+
function computeHistoryGraph(commits) {
|
|
5229
|
+
var lanes = []; // lane index -> hash the lane is waiting to reach (open edge from above)
|
|
5230
|
+
var colorOf = {}; // hash -> color index
|
|
5231
|
+
var next = 0;
|
|
5232
|
+
function colorFor(h) { if (colorOf[h] == null) colorOf[h] = next++; return colorOf[h]; }
|
|
5233
|
+
function freeLane() { for (var i = 0; i < lanes.length; i++) if (lanes[i] == null) return i; lanes.push(null); return lanes.length - 1; }
|
|
5234
|
+
var rows = [];
|
|
5235
|
+
var maxLane = 0;
|
|
5236
|
+
for (var ci = 0; ci < commits.length; ci++) {
|
|
5237
|
+
var c = commits[ci];
|
|
5238
|
+
var incoming = lanes.slice();
|
|
5239
|
+
var myLane = lanes.indexOf(c.hash);
|
|
5240
|
+
if (myLane === -1) myLane = freeLane();
|
|
5241
|
+
var myColor = colorFor(c.hash);
|
|
5242
|
+
lanes[myLane] = c.hash;
|
|
5243
|
+
for (var i = 0; i < lanes.length; i++) if (i !== myLane && lanes[i] === c.hash) lanes[i] = null; // merge other edges in
|
|
5244
|
+
var parents = c.parents || [];
|
|
5245
|
+
var parentLanes = {};
|
|
5246
|
+
if (parents.length === 0) {
|
|
5247
|
+
lanes[myLane] = null; // root commit — the lane ends here
|
|
5248
|
+
} else {
|
|
5249
|
+
lanes[myLane] = parents[0];
|
|
5250
|
+
if (colorOf[parents[0]] == null) colorOf[parents[0]] = myColor; // first parent keeps the hue
|
|
5251
|
+
parentLanes[myLane] = true;
|
|
5252
|
+
for (var p = 1; p < parents.length; p++) {
|
|
5253
|
+
var ex = lanes.indexOf(parents[p]);
|
|
5254
|
+
var l = ex !== -1 ? ex : freeLane();
|
|
5255
|
+
lanes[l] = parents[p];
|
|
5256
|
+
colorFor(parents[p]);
|
|
5257
|
+
parentLanes[l] = true;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
var outgoing = lanes.slice();
|
|
5261
|
+
var topEdges = [];
|
|
5262
|
+
for (var a = 0; a < incoming.length; a++) {
|
|
5263
|
+
if (incoming[a] == null) continue;
|
|
5264
|
+
topEdges.push({ from: a, to: incoming[a] === c.hash ? myLane : a, color: colorOf[incoming[a]] });
|
|
5265
|
+
}
|
|
5266
|
+
var bottomEdges = [];
|
|
5267
|
+
for (var b = 0; b < outgoing.length; b++) {
|
|
5268
|
+
if (outgoing[b] == null) continue;
|
|
5269
|
+
bottomEdges.push({ from: parentLanes[b] ? myLane : b, to: b, color: colorOf[outgoing[b]] });
|
|
5270
|
+
}
|
|
5271
|
+
for (var m = 0; m < Math.max(incoming.length, outgoing.length); m++) {
|
|
5272
|
+
if (incoming[m] != null || outgoing[m] != null) maxLane = Math.max(maxLane, m);
|
|
5273
|
+
}
|
|
5274
|
+
maxLane = Math.max(maxLane, myLane);
|
|
5275
|
+
rows.push({ hash: c.hash, myLane: myLane, color: myColor, topEdges: topEdges, bottomEdges: bottomEdges });
|
|
5276
|
+
}
|
|
5277
|
+
rows.maxLane = maxLane;
|
|
5278
|
+
return rows;
|
|
5279
|
+
}
|
|
5280
|
+
if (typeof window !== 'undefined') window.computeHistoryGraph = computeHistoryGraph; // exposed for tests
|
|
5281
|
+
|
|
5282
|
+
function historyLaneX(l) { return 9 + l * HISTORY_LANE_W; }
|
|
5283
|
+
function historyColor(i) { return HISTORY_COLORS[i % HISTORY_COLORS.length]; }
|
|
5284
|
+
function historyRowSvg(row) {
|
|
5285
|
+
var w = historyLaneX(historyMaxLane) + 9, h = HISTORY_ROW_H, mid = h / 2;
|
|
5286
|
+
var s = '<svg class="hgraph" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '" aria-hidden="true">';
|
|
5287
|
+
var edge = function (e, y1, y2) {
|
|
5288
|
+
var x1 = historyLaneX(e.from), x2 = historyLaneX(e.to);
|
|
5289
|
+
var c1 = (y1 + y2) / 2;
|
|
5290
|
+
return '<path d="M' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + c1 + ', ' + x2 + ' ' + c1 + ', ' + x2 + ' ' + y2 + '" stroke="' + historyColor(e.color) + '" fill="none" stroke-width="1.6"/>';
|
|
5291
|
+
};
|
|
5292
|
+
row.topEdges.forEach(function (e) { s += edge(e, 0, mid); });
|
|
5293
|
+
row.bottomEdges.forEach(function (e) { s += edge(e, mid, h); });
|
|
5294
|
+
s += '<circle cx="' + historyLaneX(row.myLane) + '" cy="' + mid + '" r="' + HISTORY_DOT_R + '" fill="' + historyColor(row.color) + '"/></svg>';
|
|
5295
|
+
return s;
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
// "HEAD -> main, origin/main, tag: v1" -> small badges (HEAD/branch/tag styled distinctly).
|
|
5299
|
+
function historyRefBadges(refs) {
|
|
5300
|
+
if (!refs || !refs.trim()) return '';
|
|
5301
|
+
return refs.split(',').map(function (r) {
|
|
5302
|
+
r = r.trim();
|
|
5303
|
+
if (!r) return '';
|
|
5304
|
+
var cls = 'href-branch', label = r;
|
|
5305
|
+
if (r.indexOf('tag:') === 0) { cls = 'href-tag'; label = r.replace('tag:', '').trim(); }
|
|
5306
|
+
else if (r.indexOf('HEAD') === 0) { cls = 'href-head'; }
|
|
5307
|
+
else if (r.indexOf('origin/') === 0 || r.indexOf('/') !== -1) { cls = 'href-remote'; }
|
|
5308
|
+
return '<span class="href ' + cls + '">' + escapeHtml(label) + '</span>';
|
|
5309
|
+
}).join('');
|
|
5310
|
+
}
|
|
5311
|
+
|
|
5312
|
+
function historyShortDate(iso) {
|
|
5313
|
+
if (!iso) return '';
|
|
5314
|
+
// 2026-06-20T21:03:11+09:00 -> "2026-06-20 21:03"
|
|
5315
|
+
var m = String(iso).match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/);
|
|
5316
|
+
return m ? m[1] + ' ' + m[2] : String(iso).slice(0, 16);
|
|
5317
|
+
}
|
|
5318
|
+
|
|
5319
|
+
function renderHistoryList() {
|
|
5320
|
+
var list = document.getElementById('history-list');
|
|
5321
|
+
if (!list) return;
|
|
5322
|
+
if (!historyCommits.length) {
|
|
5323
|
+
list.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t(historyLoading ? 'history.loading' : 'history.empty')) + '</div>';
|
|
5324
|
+
return;
|
|
5325
|
+
}
|
|
5326
|
+
list.style.setProperty('--hgraph-w', (historyLaneX(historyMaxLane) + 9) + 'px');
|
|
5327
|
+
list.innerHTML = historyCommits.map(function (c, i) {
|
|
5328
|
+
return '<button type="button" class="hrow' + (c.hash === historyActiveSha ? ' active' : '') + '" data-sha="' + escapeHtml(c.hash) + '">'
|
|
5329
|
+
+ '<span class="hgraph-cell">' + historyRowSvg(historyGraph[i]) + '</span>'
|
|
5330
|
+
+ '<span class="hmsg">' + historyRefBadges(c.refs) + escapeHtml(c.subject) + '</span>'
|
|
5331
|
+
+ '<span class="hauthor">' + escapeHtml(c.author) + '</span>'
|
|
5332
|
+
+ '<span class="hdate">' + escapeHtml(historyShortDate(c.date)) + '</span>'
|
|
5333
|
+
+ '</button>';
|
|
5334
|
+
}).join('');
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
// Text filter (subject / author). The graph only reads right on the full contiguous history, so filtering
|
|
5338
|
+
// hides the graph column (IntelliJ does the same) and just shows matching rows.
|
|
5339
|
+
function applyHistoryFilter() {
|
|
5340
|
+
var input = document.getElementById('history-search');
|
|
5341
|
+
var list = document.getElementById('history-list');
|
|
5342
|
+
if (!list) return;
|
|
5343
|
+
var q = (input && input.value || '').trim().toLowerCase();
|
|
5344
|
+
list.classList.toggle('filtering', q.length > 0);
|
|
5345
|
+
var rows = list.querySelectorAll('.hrow');
|
|
5346
|
+
for (var i = 0; i < rows.length; i++) {
|
|
5347
|
+
var c = historyCommits[i];
|
|
5348
|
+
var hit = !q || (c.subject + '\n' + c.author + '\n' + c.hash).toLowerCase().indexOf(q) !== -1;
|
|
5349
|
+
rows[i].classList.toggle('hidden', !hit);
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
|
|
5353
|
+
function openHistoryCommit(sha) {
|
|
5354
|
+
if (!sha || !window.monacoriGit) return;
|
|
5355
|
+
historyActiveSha = sha;
|
|
5356
|
+
var list = document.getElementById('history-list');
|
|
5357
|
+
if (list) list.querySelectorAll('.hrow').forEach(function (r) { r.classList.toggle('active', r.dataset.sha === sha); });
|
|
5358
|
+
var detail = document.getElementById('history-detail');
|
|
5359
|
+
if (detail) detail.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('history.loading')) + '</div>';
|
|
5360
|
+
Promise.resolve(window.monacoriGit.commitDiff(sha)).then(function (d) {
|
|
5361
|
+
if (!d || historyActiveSha !== sha) return; // selection moved on while loading
|
|
5362
|
+
renderHistoryDetail(d);
|
|
5363
|
+
}, function () {});
|
|
5364
|
+
}
|
|
5365
|
+
|
|
5366
|
+
function renderHistoryDetail(d) {
|
|
5367
|
+
var detail = document.getElementById('history-detail');
|
|
5368
|
+
if (!detail) return;
|
|
5369
|
+
var head = '<div class="history-detail-head">'
|
|
5370
|
+
+ '<div class="hd-msg">' + escapeHtml(d.message || '').replace(/\n/g, '<br>') + '</div>'
|
|
5371
|
+
+ '<div class="hd-meta"><span class="hd-hash">' + escapeHtml((d.hash || '').slice(0, 10)) + '</span>'
|
|
5372
|
+
+ '<span class="hd-author">' + escapeHtml(d.author) + (d.email ? ' <' + escapeHtml(d.email) + '>' : '') + '</span>'
|
|
5373
|
+
+ '<span class="hd-date">' + escapeHtml(historyShortDate(d.date)) + '</span>'
|
|
5374
|
+
+ historyRefBadges(d.refs) + '</div></div>';
|
|
5375
|
+
var body = (d.diffHtml && d.diffHtml.trim())
|
|
5376
|
+
? '<div class="history-diff diff2html-container">' + d.diffHtml + '</div>'
|
|
5377
|
+
: '<div class="quick-open-empty">' + escapeHtml(t(d.isMerge ? 'history.merge' : 'history.noDiff')) + '</div>';
|
|
5378
|
+
detail.innerHTML = head + body;
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
function isHistoryOpen() {
|
|
5382
|
+
var v = document.getElementById('history-view');
|
|
5383
|
+
return !!(v && !v.classList.contains('hidden'));
|
|
5384
|
+
}
|
|
5385
|
+
function closeHistory() {
|
|
5386
|
+
var v = document.getElementById('history-view');
|
|
5387
|
+
if (v) v.classList.add('hidden');
|
|
5388
|
+
if (typeof syncRail === 'function') syncRail();
|
|
5389
|
+
}
|
|
5390
|
+
function openHistory() {
|
|
5391
|
+
var v = document.getElementById('history-view');
|
|
5392
|
+
if (!v) return;
|
|
5393
|
+
if (!window.monacoriGit) return; // browser/serve mode: no git bridge
|
|
5394
|
+
v.classList.remove('hidden');
|
|
5395
|
+
if (typeof syncRail === 'function') syncRail();
|
|
5396
|
+
var search = document.getElementById('history-search');
|
|
5397
|
+
if (search) { search.value = ''; }
|
|
5398
|
+
applyHistoryFilter();
|
|
5399
|
+
historyLoading = true;
|
|
5400
|
+
renderHistoryList();
|
|
5401
|
+
Promise.resolve(window.monacoriGit.log({ limit: 300 })).then(function (commits) {
|
|
5402
|
+
historyLoading = false;
|
|
5403
|
+
historyCommits = Array.isArray(commits) ? commits : [];
|
|
5404
|
+
historyGraph = computeHistoryGraph(historyCommits);
|
|
5405
|
+
historyMaxLane = historyGraph.maxLane || 0;
|
|
5406
|
+
renderHistoryList();
|
|
5407
|
+
var detail = document.getElementById('history-detail');
|
|
5408
|
+
if (detail) detail.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('history.selectCommit')) + '</div>';
|
|
5409
|
+
if (historyCommits[0]) openHistoryCommit(historyCommits[0].hash); // preview the newest commit
|
|
5410
|
+
if (search) setTimeout(function () { try { search.focus(); } catch (e) {} }, 0);
|
|
5411
|
+
}, function () { historyLoading = false; renderHistoryList(); });
|
|
5412
|
+
}
|
|
5413
|
+
function toggleHistory() { if (isHistoryOpen()) closeHistory(); else openHistory(); }
|
|
5414
|
+
if (typeof window !== 'undefined') window.__monacoriHistory = { open: openHistory, close: closeHistory, toggle: toggleHistory, isOpen: isHistoryOpen };
|
|
5415
|
+
|
|
5416
|
+
(function wireHistory() {
|
|
5417
|
+
var list = document.getElementById('history-list');
|
|
5418
|
+
if (list) list.addEventListener('click', function (e) {
|
|
5419
|
+
var row = e.target.closest && e.target.closest('.hrow[data-sha]');
|
|
5420
|
+
if (row) openHistoryCommit(row.dataset.sha);
|
|
5421
|
+
});
|
|
5422
|
+
var search = document.getElementById('history-search');
|
|
5423
|
+
if (search) search.addEventListener('input', applyHistoryFilter);
|
|
5424
|
+
var closeBtn = document.getElementById('history-close');
|
|
5425
|
+
if (closeBtn) closeBtn.addEventListener('click', closeHistory);
|
|
5426
|
+
var view = document.getElementById('history-view');
|
|
5427
|
+
if (view) view.addEventListener('keydown', function (e) {
|
|
5428
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeHistory(); }
|
|
5429
|
+
});
|
|
5430
|
+
})();
|
|
5431
|
+
// ===== Go-to-line (Cmd/Ctrl+L), copy caret location (Cmd/Ctrl+K), and the sidebar row action menu. =====
|
|
5432
|
+
|
|
5433
|
+
// Programmatic clipboard write. Electron's bridge is reliable on file://; navigator.clipboard is the fallback.
|
|
5434
|
+
function copyTextToClipboard(text) {
|
|
5435
|
+
try { if (window.monacoriClipboard && typeof window.monacoriClipboard.write === 'function') { window.monacoriClipboard.write(text); return true; } } catch (e) {}
|
|
5436
|
+
try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text); return true; } } catch (e) {}
|
|
5437
|
+
return false;
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
// "path:line" for the current caret — source view (the painted file) or the diff caret. '' if neither.
|
|
5441
|
+
function caretLocation() {
|
|
5442
|
+
if (typeof isSourceViewerVisible === 'function' && isSourceViewerVisible()) {
|
|
5443
|
+
var sv = document.getElementById('source-viewer');
|
|
5444
|
+
var p = (sv && sv.dataset.openPath) || '';
|
|
5445
|
+
if (p && typeof viewerCursor !== 'undefined' && viewerCursor && viewerCursor.path === p) return p + ':' + (viewerCursor.lineIndex + 1);
|
|
5446
|
+
if (p) return p;
|
|
5447
|
+
}
|
|
5448
|
+
if (typeof isDiffViewVisible === 'function' && isDiffViewVisible() && typeof diffCursor !== 'undefined' && diffCursor) {
|
|
5449
|
+
var wrap = diffWrapperByPath(diffCursor.path);
|
|
5450
|
+
var row = wrap ? diffRowAt(wrap, diffCursor.side, diffCursor.rowIndex) : null;
|
|
5451
|
+
var ln = row ? diffLineNumber(row) : null;
|
|
5452
|
+
return diffCursor.path + (ln ? ':' + ln : '');
|
|
5453
|
+
}
|
|
5454
|
+
return '';
|
|
5455
|
+
}
|
|
5456
|
+
|
|
5457
|
+
// Cmd/Ctrl+K — copy the caret's file:line to the clipboard.
|
|
5458
|
+
function copyCaretLocation() {
|
|
5459
|
+
var loc = caretLocation();
|
|
5460
|
+
if (!loc) return;
|
|
5461
|
+
if (copyTextToClipboard(loc) && typeof showToast === 'function') showToast(t('goto.copied') + ' ' + loc);
|
|
5462
|
+
}
|
|
5463
|
+
|
|
5464
|
+
// Diff view: place the caret on the row whose (new, then old) line number matches n, in the active file.
|
|
5465
|
+
function gotoDiffLine(n) {
|
|
5466
|
+
var path = (typeof diffCursor !== 'undefined' && diffCursor && diffCursor.path) || '';
|
|
5467
|
+
if (!path && typeof diffActiveWrapper === 'function') {
|
|
5468
|
+
var w = diffActiveWrapper();
|
|
5469
|
+
var nm = w && w.querySelector('.d2h-file-name');
|
|
5470
|
+
if (nm && nm.textContent) path = nm.textContent.trim();
|
|
5471
|
+
}
|
|
5472
|
+
var wrap = path && diffWrapperByPath(path);
|
|
5473
|
+
if (!wrap) return;
|
|
5474
|
+
var sides = [(diffCursor && diffCursor.side) || 'new', 'new', 'old'];
|
|
5475
|
+
for (var s = 0; s < sides.length; s++) {
|
|
5476
|
+
var rows = diffRowsOf(diffSideTable(wrap, sides[s]));
|
|
5477
|
+
for (var i = 0; i < rows.length; i++) {
|
|
5478
|
+
if (diffLineNumber(rows[i]) === n) { setDiffCursor(path, sides[s], i, 0, true); return; }
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
|
|
5483
|
+
function gotoLineJump(n) {
|
|
5484
|
+
if (!(n >= 1)) return;
|
|
5485
|
+
if (typeof isSourceViewerVisible === 'function' && isSourceViewerVisible()) {
|
|
5486
|
+
var sv = document.getElementById('source-viewer');
|
|
5487
|
+
var p = (sv && sv.dataset.openPath) || '';
|
|
5488
|
+
var f = p && sourceByPath.get(p);
|
|
5489
|
+
if (f && f.embedded && typeof f.content === 'string') {
|
|
5490
|
+
var max = f.content.split(/\r?\n/).length;
|
|
5491
|
+
setSourceCursor(p, Math.max(0, Math.min(max - 1, n - 1)), 0, true, -1);
|
|
5492
|
+
return;
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
if (typeof isDiffViewVisible === 'function' && isDiffViewVisible()) gotoDiffLine(n);
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
// Cmd/Ctrl+L — a small numeric prompt; Enter jumps, Esc closes.
|
|
5499
|
+
function openGotoLine() {
|
|
5500
|
+
if (!((typeof isSourceViewerVisible === 'function' && isSourceViewerVisible()) || (typeof isDiffViewVisible === 'function' && isDiffViewVisible()))) return;
|
|
5501
|
+
var prior = document.getElementById('goto-line');
|
|
5502
|
+
if (prior) prior.remove();
|
|
5503
|
+
var box = document.createElement('div');
|
|
5504
|
+
box.id = 'goto-line';
|
|
5505
|
+
box.className = 'goto-line';
|
|
5506
|
+
var input = document.createElement('input');
|
|
5507
|
+
input.type = 'text';
|
|
5508
|
+
input.inputMode = 'numeric';
|
|
5509
|
+
input.className = 'goto-line-input';
|
|
5510
|
+
input.placeholder = t('goto.placeholder');
|
|
5511
|
+
box.appendChild(input);
|
|
5512
|
+
document.body.appendChild(box);
|
|
5513
|
+
function close() { box.remove(); document.removeEventListener('keydown', onKey, true); }
|
|
5514
|
+
function onKey(e) {
|
|
5515
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); }
|
|
5516
|
+
else if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); var n = parseInt(input.value, 10); close(); if (n >= 1) gotoLineJump(n); }
|
|
5517
|
+
}
|
|
5518
|
+
// Capture phase so Enter/Esc are handled here before the global keymap (which is on bubble).
|
|
5519
|
+
document.addEventListener('keydown', onKey, true);
|
|
5520
|
+
setTimeout(function () { try { input.focus(); } catch (e) {} }, 0);
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
// Sidebar Opt+Enter: actions for a focused file row (copy path / reveal in Finder / open terminal here).
|
|
5524
|
+
function openTreeRowMenu(row) {
|
|
5525
|
+
if (!row) return;
|
|
5526
|
+
var path = row.dataset.sourceFile || row.dataset.file || '';
|
|
5527
|
+
if (!path) return;
|
|
5528
|
+
var r = row.getBoundingClientRect();
|
|
5529
|
+
var items = [
|
|
5530
|
+
{ label: t('menu.copyPath'), onSelect: function () { if (copyTextToClipboard(path) && typeof showToast === 'function') showToast(t('goto.copied') + ' ' + path); } },
|
|
5531
|
+
];
|
|
5532
|
+
if (window.monacoriApp && typeof window.monacoriApp.revealInFinder === 'function') {
|
|
5533
|
+
items.push({ label: t('menu.revealFinder'), onSelect: function () { try { window.monacoriApp.revealInFinder(path); } catch (e) {} } });
|
|
5534
|
+
items.push({ label: t('menu.openTerminal'), onSelect: function () { try { window.monacoriApp.openTerminalAt(path); } catch (e) {} } });
|
|
5535
|
+
}
|
|
5536
|
+
showCustomDropdown(Math.round(r.left + 14), Math.round(r.bottom + 2), items, Math.round(r.top));
|
|
5537
|
+
}
|