@happy-nut/monacori 0.1.20 → 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/assets/icon.icns +0 -0
- package/dist/app-main.js +441 -158
- package/dist/assets.js +8 -1
- package/dist/build.d.ts +1 -0
- package/dist/build.js +13 -7
- package/dist/diff.d.ts +2 -1
- package/dist/diff.js +3 -3
- package/dist/i18n.js +56 -8
- package/dist/preload.cjs +15 -0
- package/dist/render.d.ts +5 -0
- package/dist/render.js +154 -29
- package/dist/util.d.ts +5 -0
- package/dist/util.js +21 -0
- package/dist/viewer.client.js +582 -153
- package/dist/viewer.client.min.js +1 -0
- package/dist/viewer.css +202 -72
- package/package.json +10 -2
- package/scripts/patch-electron-name.mjs +23 -14
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) {
|
|
@@ -98,14 +119,19 @@ function whenFileReady(wrapper, cb) {
|
|
|
98
119
|
if (bodyPromise[idx]) { bodyPromise[idx].then(function () { cb(); }); return; }
|
|
99
120
|
cb();
|
|
100
121
|
}
|
|
122
|
+
var lazyIO = null; // remembered so each setupLazyDiff (re-run on every watch refresh) disconnects the prior
|
|
123
|
+
// observer instead of leaving a new one bound to detached wrappers — otherwise observers
|
|
124
|
+
// (and the old DOM they retain) pile up over a long-running session and slowly choke it.
|
|
101
125
|
function setupLazyDiff() {
|
|
102
126
|
var container = document.getElementById('diff2html-container');
|
|
103
127
|
if (!container) return;
|
|
128
|
+
if (lazyIO) { try { lazyIO.disconnect(); } catch (e) {} lazyIO = null; }
|
|
104
129
|
var wrappers = Array.prototype.slice.call(container.querySelectorAll('.d2h-file-wrapper'));
|
|
105
130
|
if (typeof IntersectionObserver !== 'undefined') {
|
|
106
131
|
var io = new IntersectionObserver(function (entries) {
|
|
107
132
|
entries.forEach(function (e) { if (e.isIntersecting) { ensureFileReady(e.target); io.unobserve(e.target); } });
|
|
108
133
|
}, { root: null, rootMargin: '600px 0px' });
|
|
134
|
+
lazyIO = io; // track this observer so the NEXT setupLazyDiff can disconnect it (callback keeps using local io)
|
|
109
135
|
wrappers.forEach(function (w) { io.observe(w); });
|
|
110
136
|
} else {
|
|
111
137
|
wrappers.forEach(function (w) { ensureFileReady(w); }); // no IntersectionObserver -> materialize all
|
|
@@ -251,6 +277,7 @@ const quickOpen = document.getElementById('quick-open');
|
|
|
251
277
|
const quickInput = document.getElementById('quick-open-input');
|
|
252
278
|
const quickResults = document.getElementById('quick-open-results');
|
|
253
279
|
const quickModeLabel = document.getElementById('quick-open-mode');
|
|
280
|
+
const quickFilterEl = document.getElementById('quick-open-filter');
|
|
254
281
|
let current = -1;
|
|
255
282
|
let checkingForUpdates = false;
|
|
256
283
|
let lastShiftAt = 0;
|
|
@@ -258,6 +285,7 @@ let lastShiftSide = 0;
|
|
|
258
285
|
let quickMode = 'all';
|
|
259
286
|
let quickItems = [];
|
|
260
287
|
let quickActive = 0;
|
|
288
|
+
let recentFilter = ''; // IntelliJ-style speed-search: typed letters narrow the Recent list (no search box)
|
|
261
289
|
let usageItems = []; // find-usages results for the Cmd+B-on-declaration popup
|
|
262
290
|
let usageActive = 0;
|
|
263
291
|
let viewerCursor = null;
|
|
@@ -508,7 +536,7 @@ function revealAt(el, scroller, fraction) {
|
|
|
508
536
|
}
|
|
509
537
|
// Scrolloff variant: scroll ONLY when `el` would otherwise leave the viewport, keeping it within `marginFrac`
|
|
510
538
|
// of the top/bottom edge. While the row moves comfortably inside that band the view stays put — continuous
|
|
511
|
-
// centering scrolled the file even when everything was visible (dizzying). Used by the diff caret.
|
|
539
|
+
// centering scrolled the file even when everything was visible (dizzying). Used by the diff caret and the sidebar tree.
|
|
512
540
|
function scrolloffReveal(el, scroller, marginFrac) {
|
|
513
541
|
if (!el || !scroller || !scroller.clientHeight) return;
|
|
514
542
|
var top = el.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
|
|
@@ -655,14 +683,19 @@ function next(delta) {
|
|
|
655
683
|
// File boundary: no more change blocks in this file. Forward F7 announces "last change — press F7 again
|
|
656
684
|
// to go to the next file" on the FIRST press (a beat to mark-viewed) and only crosses on the SECOND
|
|
657
685
|
// consecutive press. Already-viewed files (and backward nav) cross immediately — no announcement.
|
|
658
|
-
|
|
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) {
|
|
659
691
|
if (pendingFileBoundary !== diffCursor.path) {
|
|
660
692
|
pendingFileBoundary = diffCursor.path;
|
|
661
|
-
|
|
693
|
+
showCaretHint(t('diff.lastHunk'));
|
|
662
694
|
return;
|
|
663
695
|
}
|
|
664
696
|
pendingFileBoundary = null; // second consecutive press on the same file → fall through and cross
|
|
665
697
|
}
|
|
698
|
+
hideCaretHint(); // about to cross files — drop the hint NOW (before the async body load) so it can't cover the next file
|
|
666
699
|
// hunk-level nav to the next/prev unviewed file.
|
|
667
700
|
const caretHunk = hunkIndexAtCaret();
|
|
668
701
|
const base = caretHunk >= 0 ? caretHunk : current;
|
|
@@ -675,6 +708,22 @@ function next(delta) {
|
|
|
675
708
|
// Every changed file is marked viewed — nothing left to review, so F7/[/] stay put.
|
|
676
709
|
}
|
|
677
710
|
|
|
711
|
+
// Jump to the first change of the next unviewed file after `path` (wrapping). Used right after marking a
|
|
712
|
+
// file viewed: its diff body is now hidden, so staying would blank the content — we advance to the next
|
|
713
|
+
// change instead. Returns false when every changed file is viewed (nothing to advance to).
|
|
714
|
+
function gotoNextUnviewedFile(path) {
|
|
715
|
+
const total = hunkTotal();
|
|
716
|
+
if (total === 0) return false;
|
|
717
|
+
const start = firstHunkForPath(path);
|
|
718
|
+
let idx = (start >= 0 ? start : (current >= 0 ? current : 0)) + 1;
|
|
719
|
+
for (let step = 0; step < total; step++) {
|
|
720
|
+
const norm = ((idx % total) + total) % total;
|
|
721
|
+
if (!isFileViewed(hunkPathAt(norm) || '')) { setActive(norm); return true; }
|
|
722
|
+
idx += 1;
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
|
|
678
727
|
function initialHunkForNavigation(delta) {
|
|
679
728
|
const openPath = document.getElementById('source-viewer')?.dataset.openPath || '';
|
|
680
729
|
const sourceHunk = firstHunkForPath(openPath);
|
|
@@ -695,9 +744,22 @@ function openQuickOpen(mode) {
|
|
|
695
744
|
quickMode = mode;
|
|
696
745
|
quickModeLabel.textContent = mode === 'recent' ? t('quickopen.recent') : mode === 'content' ? t('quickopen.findInFiles') : t('quickopen.searchFiles');
|
|
697
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 = '';
|
|
698
751
|
quickInput.value = '';
|
|
752
|
+
updateRecentFilterDisplay();
|
|
699
753
|
renderQuickOpenResults();
|
|
700
|
-
|
|
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'; }
|
|
701
763
|
}
|
|
702
764
|
|
|
703
765
|
function closeQuickOpen() {
|
|
@@ -707,6 +769,8 @@ function closeQuickOpen() {
|
|
|
707
769
|
function handleQuickOpenKey(event) {
|
|
708
770
|
if (event.key === 'Escape') {
|
|
709
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; }
|
|
710
774
|
closeQuickOpen();
|
|
711
775
|
return true;
|
|
712
776
|
}
|
|
@@ -727,15 +791,31 @@ function handleQuickOpenKey(event) {
|
|
|
727
791
|
openQuickItem(quickItems[quickActive]);
|
|
728
792
|
return true;
|
|
729
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
|
+
}
|
|
730
809
|
return false;
|
|
731
810
|
}
|
|
732
811
|
|
|
733
812
|
function renderQuickOpenResults() {
|
|
734
813
|
if (!quickResults) return;
|
|
735
|
-
|
|
736
|
-
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();
|
|
737
818
|
quickItems = candidates
|
|
738
|
-
.filter((item) => quickMode !== 'recent' || query.length > 0 || item.recent)
|
|
739
819
|
.filter((item) => {
|
|
740
820
|
if (query.length === 0) return true;
|
|
741
821
|
if (quickMode === 'content') {
|
|
@@ -916,8 +996,10 @@ function focusTree(index) {
|
|
|
916
996
|
if (rows.length === 0) return;
|
|
917
997
|
treeFocusIndex = Math.max(0, Math.min(rows.length - 1, index));
|
|
918
998
|
// Render the focus class AND scroll in the SAME frame. A fast key-repeat queues many ArrowDowns before a
|
|
919
|
-
// frame; moving the focus class instantly while the coalesced scroll lags makes the panel jump
|
|
920
|
-
//
|
|
999
|
+
// frame; moving the focus class instantly while the coalesced scroll lags makes the panel jump. Coalescing
|
|
1000
|
+
// both keeps focus + scroll in lockstep, and scrolloffReveal scrolls ONLY when the focused row nears the
|
|
1001
|
+
// top/bottom edge — a row moving inside the visible band must never drag the whole panel (revealAt did,
|
|
1002
|
+
// re-centering on every move so even a mid-list row scrolled the sidebar).
|
|
921
1003
|
scheduleTreeFocus();
|
|
922
1004
|
}
|
|
923
1005
|
var treeFocusRaf = 0;
|
|
@@ -929,7 +1011,7 @@ function scheduleTreeFocus() {
|
|
|
929
1011
|
if (treeFocusIndex < 0 || treeFocusIndex >= rows.length) return;
|
|
930
1012
|
const el = rows[treeFocusIndex];
|
|
931
1013
|
document.querySelectorAll('.tree-focus').forEach((e) => { if (e !== el) e.classList.remove('tree-focus'); });
|
|
932
|
-
if (el) { el.classList.add('tree-focus');
|
|
1014
|
+
if (el) { el.classList.add('tree-focus'); scrolloffReveal(el, document.querySelector('.sidebar-scroll'), 0.15); }
|
|
933
1015
|
});
|
|
934
1016
|
}
|
|
935
1017
|
|
|
@@ -1053,9 +1135,11 @@ function handleTreeKey(event) {
|
|
|
1053
1135
|
// owns focus AND the only caret, so global shortcuts stand down until Esc/close — we must not navigate a
|
|
1054
1136
|
// panel the user can't even see behind the overlay (nor leave a second blinking caret in it).
|
|
1055
1137
|
function isFloatingModalOpen() {
|
|
1056
|
-
if (document.getElementById('mc-modal') || document.getElementById('mc-memo')) return true;
|
|
1057
1138
|
var sm = document.getElementById('settings-modal');
|
|
1058
|
-
|
|
1139
|
+
if (sm && !sm.classList.contains('hidden')) return true;
|
|
1140
|
+
// The merged/memo panels are now docked (inline), not overlays — but while one OWNS focus we still stand
|
|
1141
|
+
// down the global nav shortcuts so typing / ▲▼ inside it isn't hijacked. Focus elsewhere -> shortcuts run.
|
|
1142
|
+
return isDockFocused();
|
|
1059
1143
|
}
|
|
1060
1144
|
document.addEventListener('keydown', (event) => {
|
|
1061
1145
|
if (!quickOpen?.classList.contains('hidden')) {
|
|
@@ -1066,12 +1150,32 @@ document.addEventListener('keydown', (event) => {
|
|
|
1066
1150
|
if (handleUsagesKey(event)) return;
|
|
1067
1151
|
}
|
|
1068
1152
|
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1153
|
+
// Dock controls fire regardless of focus (terminal / merged / memo) — they sit ABOVE the focus guard so
|
|
1154
|
+
// they still work from inside a dock panel. Cmd/Ctrl+Shift+' maximizes the active dock; Cmd/Ctrl+Shift+/
|
|
1155
|
+
// and +. open the merged views; Cmd/Ctrl+Shift+N toggles the memo. (Match event.code so IME/layout never
|
|
1156
|
+
// swallows the combo.) Settings is a true overlay, so these stand down while it is up.
|
|
1157
|
+
var settingsUp = (function () { var s = document.getElementById('settings-modal'); return !!(s && !s.classList.contains('hidden')); })();
|
|
1158
|
+
if ((event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && event.code === 'Quote') {
|
|
1159
|
+
event.preventDefault();
|
|
1160
|
+
toggleDockMaximized();
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && !event.altKey && (event.code === 'Slash' || event.code === 'Period' || event.key === '?' || event.key === '>')) {
|
|
1164
|
+
event.preventDefault();
|
|
1165
|
+
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && event.shiftKey && !event.altKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
1169
|
+
event.preventDefault();
|
|
1170
|
+
openMemoView();
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Settings overlay (or a focused merged/memo dock) captures keys: stand down the rest of the global
|
|
1175
|
+
// shortcuts (Cmd+1, F7, Cmd+[/], Cmd+B, …). Each has its own Esc + editing handlers.
|
|
1072
1176
|
if (isFloatingModalOpen()) return;
|
|
1073
1177
|
|
|
1074
|
-
if ((event.metaKey || event.ctrlKey) && event.key === '1') {
|
|
1178
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '1') {
|
|
1075
1179
|
event.preventDefault();
|
|
1076
1180
|
// Coming from the diff: open the file you were viewing as source so Cmd+1 lands ON it (not a stale/blank
|
|
1077
1181
|
// source pane), and the tree below points at the same file. Capture the path BEFORE openSourceFile flips
|
|
@@ -1086,7 +1190,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1086
1190
|
focusOpenFileInTree();
|
|
1087
1191
|
return;
|
|
1088
1192
|
}
|
|
1089
|
-
if ((event.metaKey || event.ctrlKey) && event.key === '0') {
|
|
1193
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '0') {
|
|
1090
1194
|
event.preventDefault();
|
|
1091
1195
|
setTab('changes');
|
|
1092
1196
|
focusOpenFileInTree();
|
|
@@ -1121,21 +1225,8 @@ document.addEventListener('keydown', (event) => {
|
|
|
1121
1225
|
}
|
|
1122
1226
|
}
|
|
1123
1227
|
|
|
1124
|
-
// Merged
|
|
1125
|
-
//
|
|
1126
|
-
// Match the PHYSICAL key (event.code) so macOS/IME/layout never swallows the combo; fires in any focus.
|
|
1127
|
-
if ((event.metaKey || event.ctrlKey) && (event.code === 'Slash' || event.code === 'Period' || event.key === '?' || event.key === '>')) {
|
|
1128
|
-
event.preventDefault();
|
|
1129
|
-
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
// Cmd/Ctrl+Shift+N opens/closes the prompt memo. Electron also routes this via the Review menu; in the
|
|
1133
|
-
// browser/serve build (no menu) this keydown is the only path. Match the physical key so layout/IME never swallows it.
|
|
1134
|
-
if ((event.metaKey || event.ctrlKey) && event.shiftKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
1135
|
-
event.preventDefault();
|
|
1136
|
-
openMemoView();
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1228
|
+
// (Merged views Cmd/Ctrl+Shift+/ +. and the memo Cmd/Ctrl+Shift+N are handled above the focus guard so
|
|
1229
|
+
// they work from inside a dock too.)
|
|
1139
1230
|
// "?" = question, ">" = change-request composer on the current line/selection (no modifier).
|
|
1140
1231
|
if (!event.altKey && !event.metaKey && !event.ctrlKey && (event.key === '?' || event.key === '>')) {
|
|
1141
1232
|
const ce = document.activeElement;
|
|
@@ -1160,7 +1251,12 @@ document.addEventListener('keydown', (event) => {
|
|
|
1160
1251
|
}
|
|
1161
1252
|
if (vp && currentFileSignature(vp)) {
|
|
1162
1253
|
event.preventDefault();
|
|
1163
|
-
|
|
1254
|
+
const willView = !isFileViewed(vp);
|
|
1255
|
+
setFileViewed(vp, willView);
|
|
1256
|
+
// Marking viewed hides this file's diff body — don't strand the caret on the now-blank file.
|
|
1257
|
+
// Auto-advance to the next unviewed change (the user's flow: mark viewed -> jump to next).
|
|
1258
|
+
// Unmarking stays put. If every file is viewed, gotoNextUnviewedFile is a no-op.
|
|
1259
|
+
if (willView) gotoNextUnviewedFile(vp);
|
|
1164
1260
|
return;
|
|
1165
1261
|
}
|
|
1166
1262
|
}
|
|
@@ -1180,10 +1276,14 @@ document.addEventListener('keydown', (event) => {
|
|
|
1180
1276
|
// PageUp/Down scroll the diff/source view. There's no focusable scroller (the diff caret is a JS cursor),
|
|
1181
1277
|
// and d2h-file-side-diff's horizontal scrollport even swallows vertical wheel, so handle paging explicitly.
|
|
1182
1278
|
// Only when the tree isn't focused — the tree pages itself in handleTreeKey below.
|
|
1183
|
-
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) {
|
|
1184
1280
|
var psc = isDiffViewVisible() ? document.getElementById('diff2html-container') : (isSourceViewerVisible() ? document.getElementById('source-body') : null);
|
|
1185
1281
|
if (psc) { event.preventDefault(); psc.scrollTop += (event.key === 'PageDown' ? 0.9 : -0.9) * psc.clientHeight; return; }
|
|
1186
1282
|
}
|
|
1283
|
+
// A non-Shift keystroke between the two Shifts cancels the pending double-Shift quick-open. Without this,
|
|
1284
|
+
// "Shift → type something → Shift" within 300ms still popped the search, so it fired on nearly every other
|
|
1285
|
+
// keystroke. Reset BEFORE the caret handlers below (they swallow arrows) so arrow keys break it too.
|
|
1286
|
+
if (event.key !== 'Shift') { lastShiftAt = 0; lastShiftSide = 0; }
|
|
1187
1287
|
if (treeFocusIndex >= 0 && handleTreeKey(event)) return;
|
|
1188
1288
|
if (treeFocusIndex < 0 && !event.metaKey && !event.ctrlKey && !event.altKey && isSourceViewerVisible() && handleSourceCaretKey(event)) return;
|
|
1189
1289
|
if (treeFocusIndex < 0 && !event.metaKey && !event.ctrlKey && !event.altKey && isDiffViewVisible() && handleDiffCaretKey(event)) return;
|
|
@@ -1207,12 +1307,12 @@ document.addEventListener('keydown', (event) => {
|
|
|
1207
1307
|
lastShiftSide = side;
|
|
1208
1308
|
}
|
|
1209
1309
|
|
|
1210
|
-
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') {
|
|
1211
1311
|
event.preventDefault();
|
|
1212
1312
|
openQuickOpen('content');
|
|
1213
1313
|
return;
|
|
1214
1314
|
}
|
|
1215
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'e') {
|
|
1315
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key.toLowerCase() === 'e') {
|
|
1216
1316
|
event.preventDefault();
|
|
1217
1317
|
openQuickOpen('recent');
|
|
1218
1318
|
return;
|
|
@@ -1227,14 +1327,14 @@ document.addEventListener('keydown', (event) => {
|
|
|
1227
1327
|
}
|
|
1228
1328
|
}
|
|
1229
1329
|
|
|
1230
|
-
if ((event.metaKey || event.ctrlKey) && event.key === 'ArrowDown') {
|
|
1330
|
+
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === 'ArrowDown') {
|
|
1231
1331
|
event.preventDefault();
|
|
1232
1332
|
if (isSourceViewerVisible()) goToSymbolUnderCursor();
|
|
1233
1333
|
else openDiffFileAtCaret();
|
|
1234
1334
|
return;
|
|
1235
1335
|
}
|
|
1236
1336
|
|
|
1237
|
-
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')) {
|
|
1238
1338
|
var aeB = document.activeElement;
|
|
1239
1339
|
if (aeB && (aeB.tagName === 'INPUT' || aeB.tagName === 'TEXTAREA' || aeB.tagName === 'SELECT')) return;
|
|
1240
1340
|
event.preventDefault();
|
|
@@ -1296,7 +1396,7 @@ document.addEventListener('keydown', (event) => {
|
|
|
1296
1396
|
}
|
|
1297
1397
|
}
|
|
1298
1398
|
|
|
1299
|
-
if (event.key === 'F7') {
|
|
1399
|
+
if (event.key === 'F7' && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
1300
1400
|
event.preventDefault();
|
|
1301
1401
|
const delta = event.shiftKey ? -1 : 1;
|
|
1302
1402
|
const sourceViewer = document.getElementById('source-viewer');
|
|
@@ -1304,8 +1404,12 @@ document.addEventListener('keydown', (event) => {
|
|
|
1304
1404
|
// where they were reading. Shift+F7 — and any file with no hunk of its own — falls through to plain
|
|
1305
1405
|
// prev/next-change navigation across the whole diff.
|
|
1306
1406
|
if (delta > 0 && sourceViewer && !sourceViewer.classList.contains('hidden')) {
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1407
|
+
const sp = sourceViewer.dataset.openPath || '';
|
|
1408
|
+
const sourceHunk = firstHunkForPath(sp);
|
|
1409
|
+
// Enter the diff at the open file's own hunk — UNLESS it's already viewed. A viewed file's diff body
|
|
1410
|
+
// is hidden (display:none), so landing on it blanks the content and F7 appears stuck; fall through to
|
|
1411
|
+
// next() instead so we skip to an unviewed change.
|
|
1412
|
+
if (sourceHunk >= 0 && !isFileViewed(sp)) {
|
|
1309
1413
|
setActive(sourceHunk);
|
|
1310
1414
|
return;
|
|
1311
1415
|
}
|
|
@@ -1369,6 +1473,19 @@ document.querySelectorAll('.tab').forEach((button) => {
|
|
|
1369
1473
|
button.addEventListener('click', () => setTab(button.dataset.tab || 'changes'));
|
|
1370
1474
|
});
|
|
1371
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
|
+
|
|
1372
1489
|
document.getElementById('back-to-diff')?.addEventListener('click', () => showDiffView(true));
|
|
1373
1490
|
document.getElementById('source-tabs')?.addEventListener('click', function (event) {
|
|
1374
1491
|
var closeBtn = event.target && event.target.closest && event.target.closest('.source-tab-close');
|
|
@@ -1405,7 +1522,12 @@ if (!restored) {
|
|
|
1405
1522
|
else openDefaultSourceFile();
|
|
1406
1523
|
}
|
|
1407
1524
|
initSourceTreeFolds();
|
|
1408
|
-
|
|
1525
|
+
syncRail(); // reflect the initial view on the activity rail
|
|
1526
|
+
// Electron receives live updates over IPC (monacoriMenu.onDiffUpdate); only serve/browser needs the HTTP
|
|
1527
|
+
// poller. Under file:// its fetch just fails every 1.5s for the app's whole life, so skip it in Electron.
|
|
1528
|
+
if (watchEnabled && !(window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function')) {
|
|
1529
|
+
setInterval(checkForLiveUpdate, 1500);
|
|
1530
|
+
}
|
|
1409
1531
|
window.addEventListener('beforeunload', saveUiState);
|
|
1410
1532
|
|
|
1411
1533
|
// First render has painted — drop the boot overlay (it bridged the blank gap right after loadFile). Two
|
|
@@ -1436,7 +1558,10 @@ window.addEventListener('beforeunload', saveUiState);
|
|
|
1436
1558
|
});
|
|
1437
1559
|
document.addEventListener('mousemove', (event) => {
|
|
1438
1560
|
if (!resizing) return;
|
|
1439
|
-
|
|
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));
|
|
1440
1565
|
document.documentElement.style.setProperty('--sidebar-width', width + 'px');
|
|
1441
1566
|
});
|
|
1442
1567
|
document.addEventListener('mouseup', () => {
|
|
@@ -1622,6 +1747,7 @@ function setDiffCursor(path, side, rowIndex, column, reveal) {
|
|
|
1622
1747
|
var col = Math.max(0, Math.min(column, diffLineText(rows[ri]).length));
|
|
1623
1748
|
diffCursor = { path: path, side: side, rowIndex: ri, column: col };
|
|
1624
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
|
|
1625
1751
|
diffSelectionAnchor = null; // any direct caret placement (click/F7/Cmd-arrow) drops the selection; Shift+Arrow re-sets it
|
|
1626
1752
|
if (reveal) {
|
|
1627
1753
|
// Render the caret AND scroll in the SAME animation frame. A fast key-repeat queues several ArrowDowns
|
|
@@ -1793,11 +1919,51 @@ function moveDiffWord(dir, extend) {
|
|
|
1793
1919
|
setDiffCursor(diffCursor.path, diffCursor.side, diffCursor.rowIndex, ncol, true);
|
|
1794
1920
|
if (anchor) { diffSelectionAnchor = anchor; applyDiffSelection(); }
|
|
1795
1921
|
}
|
|
1922
|
+
// Comment boxes are injected on the right(new) side, right after the line's row (see injectThreadRow /
|
|
1923
|
+
// renderDiffComments). Split-view rows align 1:1 by index, so the caret's row index on the new side finds
|
|
1924
|
+
// the adjacent box regardless of which side the caret sits on. Mirrors commentRowSiblingOf for the source view.
|
|
1925
|
+
function diffCommentBoxSiblingOf(dir) {
|
|
1926
|
+
if (!diffCursor) return null;
|
|
1927
|
+
var wrapper = diffWrapperByPath(diffCursor.path);
|
|
1928
|
+
if (!wrapper) return null;
|
|
1929
|
+
var rows = diffRowsOf(diffSideTable(wrapper, 'new'));
|
|
1930
|
+
var row = rows[diffCursor.rowIndex];
|
|
1931
|
+
if (!row) return null;
|
|
1932
|
+
var sib = dir < 0 ? row.previousElementSibling : row.nextElementSibling;
|
|
1933
|
+
return (sib && sib.classList && sib.classList.contains('mc-comment-row')) ? sib : null;
|
|
1934
|
+
}
|
|
1796
1935
|
function handleDiffCaretKey(event) {
|
|
1797
1936
|
if (!isDiffViewVisible() || !diffCursor) return false;
|
|
1798
1937
|
var ae = document.activeElement;
|
|
1799
1938
|
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.tagName === 'SELECT')) return false;
|
|
1800
1939
|
var extend = event.shiftKey;
|
|
1940
|
+
// A comment box is selected: Backspace/Delete removes it, `e` edits it, an arrow/Escape steps off it.
|
|
1941
|
+
// Same contract as the source view (handleSourceCaretKey), but caret moves go through setDiffCursor.
|
|
1942
|
+
if (selectedCommentRow) {
|
|
1943
|
+
if (event.key === 'Backspace' || event.key === 'Delete') { event.preventDefault(); deleteCommentsInRow(selectedCommentRow); return true; }
|
|
1944
|
+
if (event.key === 'e' || event.key === 'E') { event.preventDefault(); editCommentInRow(selectedCommentRow); return true; }
|
|
1945
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'Escape') {
|
|
1946
|
+
var dir = event.key === 'ArrowUp' ? -1 : (event.key === 'ArrowDown' ? 1 : 0);
|
|
1947
|
+
var sib = dir < 0 ? selectedCommentRow.previousElementSibling : (dir > 0 ? selectedCommentRow.nextElementSibling : null);
|
|
1948
|
+
selectedCommentRow.classList.remove('mc-row-selected');
|
|
1949
|
+
selectedCommentRow = null;
|
|
1950
|
+
event.preventDefault();
|
|
1951
|
+
var wrapper = diffWrapperByPath(diffCursor.path);
|
|
1952
|
+
if (sib && wrapper && isDiffCodeRow(sib)) {
|
|
1953
|
+
var rows = diffRowsOf(diffSideTable(wrapper, 'new'));
|
|
1954
|
+
var idx = rows.indexOf(sib);
|
|
1955
|
+
if (idx >= 0) { setDiffCursor(diffCursor.path, 'new', idx, 0, true); return true; }
|
|
1956
|
+
}
|
|
1957
|
+
setDiffCursor(diffCursor.path, diffCursor.side, diffCursor.rowIndex, diffCursor.column, false); // restore caret where it was
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
// Plain Up/Down: a comment box attached to the caret line is a selectable stop (caret stays visible).
|
|
1963
|
+
if (!extend && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
|
1964
|
+
var box = diffCommentBoxSiblingOf(event.key === 'ArrowUp' ? -1 : 1);
|
|
1965
|
+
if (box) { event.preventDefault(); selectCommentRow(box); return true; }
|
|
1966
|
+
}
|
|
1801
1967
|
if (event.key === 'ArrowDown') { event.preventDefault(); moveDiffCursor(1, 0, extend); return true; }
|
|
1802
1968
|
if (event.key === 'ArrowUp') { event.preventDefault(); moveDiffCursor(-1, 0, extend); return true; }
|
|
1803
1969
|
if (event.key === 'ArrowLeft') { event.preventDefault(); moveDiffCursor(0, -1, extend); return true; }
|
|
@@ -1822,6 +1988,28 @@ function showToast(message) {
|
|
|
1822
1988
|
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
|
|
1823
1989
|
}, 4500);
|
|
1824
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
|
+
}
|
|
1825
2013
|
// Follow each comment to its snapshot line (c.code) in the current content: same line if unchanged, else the
|
|
1826
2014
|
// nearest exact match of that line. A comment is NEVER auto-deleted. If its line can't be found we leave it
|
|
1827
2015
|
// where it is — this happens routinely WITHOUT the file changing: a comment anchored to a deleted/old-side
|
|
@@ -2127,6 +2315,7 @@ function closeComposer() {
|
|
|
2127
2315
|
if (!composerState) return;
|
|
2128
2316
|
composerState = null;
|
|
2129
2317
|
refreshComments();
|
|
2318
|
+
flushPendingDiffUpdate(); // apply any live watch refresh that was held while composing
|
|
2130
2319
|
}
|
|
2131
2320
|
// The composer is injected into BOTH the diff and source views (refreshComments renders comments in
|
|
2132
2321
|
// each), but only one view is on screen at a time — the other lives inside a `.hidden` container with
|
|
@@ -2151,6 +2340,7 @@ function saveComposer(ta) {
|
|
|
2151
2340
|
else addComment(composerState.kind, composerState.path, composerState.line, composerState.code, box.value);
|
|
2152
2341
|
composerState = null;
|
|
2153
2342
|
refreshComments();
|
|
2343
|
+
flushPendingDiffUpdate(); // apply any live watch refresh that was held while composing
|
|
2154
2344
|
}
|
|
2155
2345
|
|
|
2156
2346
|
// Default merge-prompt headings, localized: a Korean user gets Korean defaults. Editable in
|
|
@@ -2292,36 +2482,153 @@ function buildMergedText(kind) {
|
|
|
2292
2482
|
return lines.join(nl);
|
|
2293
2483
|
}
|
|
2294
2484
|
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2485
|
+
// ===== Bottom dock: merged-prompt / memo / terminal share ONE docked slot below the editor =====
|
|
2486
|
+
// Only one is visible at a time — opening one closes the others (the terminal included). Cmd/Ctrl+Shift+'
|
|
2487
|
+
// maximizes the active dock over the editor area (the sidebar stays). A top resizer drags the height.
|
|
2488
|
+
var dockHeightKey = 'monacori-dock-height';
|
|
2489
|
+
var dockMaximized = false;
|
|
2490
|
+
function applyDockHeight(px) {
|
|
2491
|
+
var h = Math.max(140, Math.min(px, window.innerHeight - 120));
|
|
2492
|
+
document.documentElement.style.setProperty('--dock-height', h + 'px');
|
|
2493
|
+
}
|
|
2494
|
+
(function () { var s = parseInt(localStorage.getItem(dockHeightKey) || '', 10); if (s) applyDockHeight(s); })();
|
|
2495
|
+
// The dock panel currently filling the slot: a merged/memo panel, else the terminal when it's open.
|
|
2496
|
+
function activeDockPanel() {
|
|
2497
|
+
var mm = document.getElementById('mc-merged-panel') || document.getElementById('mc-memo-panel');
|
|
2498
|
+
if (mm) return mm;
|
|
2499
|
+
var term = document.getElementById('terminal-panel');
|
|
2500
|
+
return (term && !term.classList.contains('hidden')) ? term : null;
|
|
2501
|
+
}
|
|
2502
|
+
function applyDockMaximized() {
|
|
2503
|
+
if (!activeDockPanel()) dockMaximized = false; // nothing docked -> can't stay maximized
|
|
2504
|
+
document.body.classList.toggle('dock-maximized', dockMaximized);
|
|
2505
|
+
}
|
|
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;
|
|
2513
|
+
if (!activeDockPanel()) return; // nothing docked -> nothing to maximize
|
|
2514
|
+
dockMaximized = !dockMaximized;
|
|
2515
|
+
applyDockMaximized();
|
|
2516
|
+
}
|
|
2517
|
+
function isDockFocused() {
|
|
2518
|
+
var ae = document.activeElement;
|
|
2519
|
+
return !!(ae && ae.closest && ae.closest('.dock-panel'));
|
|
2520
|
+
}
|
|
2521
|
+
// Close the merged/memo docks (the terminal's setOpen also calls this so the slot stays exclusive).
|
|
2522
|
+
function closeMergedMemoDocks() {
|
|
2523
|
+
var m = document.getElementById('mc-merged-panel'); if (m) m.remove();
|
|
2524
|
+
var n = document.getElementById('mc-memo-panel'); if (n) n.remove();
|
|
2525
|
+
document.querySelectorAll('.dock-backdrop').forEach(function (b) { b.remove(); });
|
|
2526
|
+
document.body.classList.toggle('dock-open', !!activeDockPanel());
|
|
2527
|
+
// floating-dock tracks merged/memo only (NOT the terminal) so the maximize CSS hides content for a
|
|
2528
|
+
// terminal dock but never for these floating panels.
|
|
2529
|
+
document.body.classList.toggle('floating-dock', !!(document.getElementById('mc-merged-panel') || document.getElementById('mc-memo-panel')));
|
|
2530
|
+
applyDockMaximized();
|
|
2531
|
+
if (typeof syncRail === 'function') syncRail(); // clear the rail icon for the closed dock(s)
|
|
2532
|
+
}
|
|
2533
|
+
window.__monacoriCloseDocks = closeMergedMemoDocks;
|
|
2534
|
+
// Retry-focus a docked field (Electron async-restores focus to <body>, so a one-shot focus can lose the race).
|
|
2535
|
+
function focusDockField(field, panelSel) {
|
|
2536
|
+
var tries = 0;
|
|
2537
|
+
var tryF = function () {
|
|
2538
|
+
if (!document.querySelector(panelSel)) return true;
|
|
2539
|
+
if (document.activeElement === field) return true;
|
|
2540
|
+
try { field.focus(); } catch (e) {}
|
|
2541
|
+
return document.activeElement === field;
|
|
2542
|
+
};
|
|
2543
|
+
if (!tryF()) { var iv = setInterval(function () { if (tryF() || ++tries > 12) clearInterval(iv); }, 25); }
|
|
2544
|
+
}
|
|
2545
|
+
// Build a docked panel shell (resizer + bar with Maximize/Close + body) and mount it below the editor.
|
|
2546
|
+
// Opening it closes the terminal and any other merged/memo dock (the slot is exclusive). Returns
|
|
2547
|
+
// { panel, body, bar, close }.
|
|
2548
|
+
function mountDock(id, titleText) {
|
|
2549
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.close === 'function') {
|
|
2550
|
+
try { window.__monacoriTerminal.close(); } catch (e) {}
|
|
2551
|
+
}
|
|
2552
|
+
var prior = document.getElementById(id);
|
|
2553
|
+
if (prior) prior.remove();
|
|
2554
|
+
closeMergedMemoDocks();
|
|
2302
2555
|
var panel = document.createElement('div');
|
|
2303
|
-
panel.
|
|
2304
|
-
|
|
2305
|
-
|
|
2556
|
+
panel.id = id;
|
|
2557
|
+
panel.className = 'dock-panel';
|
|
2558
|
+
panel.tabIndex = -1;
|
|
2559
|
+
// The panel floats over the editor; a dim backdrop sits behind it (click to dismiss).
|
|
2560
|
+
var backdrop = document.createElement('div');
|
|
2561
|
+
backdrop.className = 'dock-backdrop';
|
|
2562
|
+
var resizer = document.createElement('div');
|
|
2563
|
+
resizer.className = 'dock-resizer';
|
|
2564
|
+
resizer.setAttribute('aria-hidden', 'true');
|
|
2565
|
+
var bar = document.createElement('div');
|
|
2566
|
+
bar.className = 'dock-bar';
|
|
2306
2567
|
var title = document.createElement('span');
|
|
2307
|
-
title.
|
|
2568
|
+
title.className = 'dock-title';
|
|
2569
|
+
title.textContent = titleText;
|
|
2570
|
+
var maxBtn = document.createElement('button');
|
|
2571
|
+
maxBtn.type = 'button';
|
|
2572
|
+
maxBtn.className = 'dock-btn dock-max';
|
|
2573
|
+
maxBtn.setAttribute('data-i18n-title', 'dock.maximize');
|
|
2574
|
+
maxBtn.title = t('dock.maximize');
|
|
2575
|
+
maxBtn.textContent = '⤢'; // ⤢ maximize glyph
|
|
2308
2576
|
var closeBtn = document.createElement('button');
|
|
2309
2577
|
closeBtn.type = 'button';
|
|
2310
|
-
closeBtn.className = '
|
|
2578
|
+
closeBtn.className = 'dock-btn dock-close';
|
|
2579
|
+
closeBtn.setAttribute('data-i18n', 'merged.close');
|
|
2311
2580
|
closeBtn.textContent = t('merged.close');
|
|
2581
|
+
var body = document.createElement('div');
|
|
2582
|
+
body.className = 'dock-body';
|
|
2583
|
+
bar.appendChild(title);
|
|
2584
|
+
bar.appendChild(maxBtn);
|
|
2585
|
+
bar.appendChild(closeBtn);
|
|
2586
|
+
panel.appendChild(resizer);
|
|
2587
|
+
panel.appendChild(bar);
|
|
2588
|
+
panel.appendChild(body);
|
|
2589
|
+
document.body.appendChild(backdrop);
|
|
2590
|
+
document.body.appendChild(panel);
|
|
2591
|
+
function close() { panel.remove(); backdrop.remove(); closeMergedMemoDocks(); }
|
|
2592
|
+
maxBtn.addEventListener('click', function () { toggleDockMaximized(); });
|
|
2593
|
+
closeBtn.addEventListener('click', close);
|
|
2594
|
+
backdrop.addEventListener('click', close); // click the dim behind the panel to dismiss
|
|
2595
|
+
// Esc closes the dock when focus is inside it; the editor keeps its own handlers otherwise.
|
|
2596
|
+
panel.addEventListener('keydown', function (e) {
|
|
2597
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); }
|
|
2598
|
+
});
|
|
2599
|
+
resizer.addEventListener('mousedown', function (e) {
|
|
2600
|
+
e.preventDefault();
|
|
2601
|
+
resizer.classList.add('resizing');
|
|
2602
|
+
function move(ev) { applyDockHeight(window.innerHeight - ev.clientY); }
|
|
2603
|
+
function up() {
|
|
2604
|
+
resizer.classList.remove('resizing');
|
|
2605
|
+
document.removeEventListener('mousemove', move);
|
|
2606
|
+
document.removeEventListener('mouseup', up);
|
|
2607
|
+
var cur = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--dock-height'), 10);
|
|
2608
|
+
if (cur) { try { localStorage.setItem(dockHeightKey, String(cur)); } catch (x) {} }
|
|
2609
|
+
}
|
|
2610
|
+
document.addEventListener('mousemove', move);
|
|
2611
|
+
document.addEventListener('mouseup', up);
|
|
2612
|
+
});
|
|
2613
|
+
document.body.classList.add('dock-open');
|
|
2614
|
+
document.body.classList.add('floating-dock'); // scopes the maximize CSS so it doesn't hide the diff
|
|
2615
|
+
applyDockMaximized();
|
|
2616
|
+
if (typeof syncRail === 'function') syncRail(); // light up the rail icon for the opened dock
|
|
2617
|
+
return { panel: panel, body: body, bar: bar, close: close };
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
function openMergedView(kind) {
|
|
2621
|
+
var dock = mountDock('mc-merged-panel', kind === 'q' ? t('merged.qTitle') : t('merged.cTitle'));
|
|
2622
|
+
dock.panel.dataset.kind = kind; // remembered so a live locale switch can re-render this same view
|
|
2312
2623
|
var area = document.createElement('textarea');
|
|
2313
2624
|
area.className = 'mc-modal-text';
|
|
2314
2625
|
// NOT readOnly: a readOnly textarea hides the caret in Chromium, yet we need it VISIBLE so the user sees
|
|
2315
|
-
// which comment Opt+Enter / Opt+Arrow will target. Block every edit via beforeinput instead
|
|
2316
|
-
// effect while the caret and selection stay fully interactive.
|
|
2626
|
+
// which comment Opt+Enter / Opt+Arrow will target. Block every edit via beforeinput instead.
|
|
2317
2627
|
area.value = buildMergedText(kind);
|
|
2318
2628
|
area.addEventListener('beforeinput', function (e) { e.preventDefault(); });
|
|
2319
|
-
// Opt/Alt+Enter on the merged text: a custom dropdown for the comment under the caret
|
|
2320
|
-
//
|
|
2321
|
-
// Removing here calls deleteComment(), which re-syncs the on-screen comment boxes via refreshComments.
|
|
2629
|
+
// Opt/Alt+Enter on the merged text: a custom dropdown for the comment under the caret. Opt/Alt+Arrow steps
|
|
2630
|
+
// the caret comment-to-comment so each can be acted on without hand-scrolling.
|
|
2322
2631
|
area.addEventListener('keydown', function (e) {
|
|
2323
|
-
// Opt/Alt + Arrow steps the caret to the next/previous comment block so you can move comment-to-comment
|
|
2324
|
-
// and act on each with Opt+Enter, without hand-scrolling.
|
|
2325
2632
|
if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
|
2326
2633
|
e.preventDefault();
|
|
2327
2634
|
e.stopPropagation();
|
|
@@ -2336,49 +2643,28 @@ function openMergedView(kind) {
|
|
|
2336
2643
|
var cxy = mergedCaretXY(area);
|
|
2337
2644
|
var x = cxy.x, y = cxy.below, flipTop = cxy.top;
|
|
2338
2645
|
var rerender = function () {
|
|
2339
|
-
if (!reviewComments.filter(function (c) { return c.kind === kind; }).length) {
|
|
2646
|
+
if (!reviewComments.filter(function (c) { return c.kind === kind; }).length) { dock.close(); return; }
|
|
2340
2647
|
area.value = buildMergedText(kind);
|
|
2341
2648
|
};
|
|
2342
2649
|
if (area.selectionStart !== area.selectionEnd || seqs.length > 1) {
|
|
2343
2650
|
// Select-all / multi-comment: offer send-to-terminal (the whole merged text) FIRST, then remove-all.
|
|
2344
|
-
// Can't "Go to comment" across many at once, so navigate is omitted here.
|
|
2345
2651
|
var multi = [];
|
|
2346
|
-
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.
|
|
2347
|
-
multi.push({ label: t('merged.sendToTerminal'), onSelect: function () { var text = buildMergedText(kind);
|
|
2652
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.paneCount === 'function' && window.__monacoriTerminal.paneCount() > 0) {
|
|
2653
|
+
multi.push({ label: t('merged.sendToTerminal'), onSelect: function () { var text = buildMergedText(kind); dock.close(); window.__monacoriTerminal.enterSendMode(text); } });
|
|
2348
2654
|
}
|
|
2349
2655
|
multi.push({ label: t('dropdown.remove'), onSelect: function () { seqs.forEach(deleteComment); rerender(); } });
|
|
2350
2656
|
showCustomDropdown(x, y, multi, flipTop);
|
|
2351
2657
|
} else {
|
|
2352
2658
|
var seq = seqs[0];
|
|
2353
2659
|
showCustomDropdown(x, y, [
|
|
2354
|
-
{ label: t('dropdown.navigate'), onSelect: function () {
|
|
2660
|
+
{ label: t('dropdown.navigate'), onSelect: function () { dock.close(); navigateToComment(seq); } },
|
|
2355
2661
|
{ label: t('dropdown.remove'), onSelect: function () { deleteComment(seq); rerender(); } },
|
|
2356
2662
|
], flipTop);
|
|
2357
2663
|
}
|
|
2358
2664
|
});
|
|
2359
|
-
|
|
2360
|
-
//
|
|
2361
|
-
|
|
2362
|
-
head.appendChild(closeBtn);
|
|
2363
|
-
panel.appendChild(head);
|
|
2364
|
-
panel.appendChild(area);
|
|
2365
|
-
modal.appendChild(panel);
|
|
2366
|
-
modal.addEventListener('mousedown', function (e) { if (e.target === modal) modal.remove(); });
|
|
2367
|
-
modal.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); modal.remove(); } });
|
|
2368
|
-
document.body.appendChild(modal);
|
|
2369
|
-
// Focus the read-only text so the caret is visible and Opt+Arrow / Opt+Enter (incl. the send-to-terminal
|
|
2370
|
-
// dropdown item) work. Electron async-restores focus to <body>, so retry briefly (same as the composer).
|
|
2371
|
-
var modalFocusTarget = area;
|
|
2372
|
-
var modalFocusTries = 0;
|
|
2373
|
-
var tryFocusModal = function () {
|
|
2374
|
-
if (!document.getElementById('mc-modal')) return true;
|
|
2375
|
-
if (document.activeElement === modalFocusTarget) return true;
|
|
2376
|
-
try { modalFocusTarget.focus(); modalFocusTarget.selectionStart = modalFocusTarget.selectionEnd = 0; } catch (e) {}
|
|
2377
|
-
return document.activeElement === modalFocusTarget;
|
|
2378
|
-
};
|
|
2379
|
-
if (!tryFocusModal()) {
|
|
2380
|
-
var modalFocusIv = setInterval(function () { if (tryFocusModal() || ++modalFocusTries > 12) clearInterval(modalFocusIv); }, 25);
|
|
2381
|
-
}
|
|
2665
|
+
dock.body.appendChild(area);
|
|
2666
|
+
// Focus the read-only text so the caret is visible and Opt+Arrow / Opt+Enter work; retry (Electron focus race).
|
|
2667
|
+
focusDockField(area, '#mc-merged-panel');
|
|
2382
2668
|
}
|
|
2383
2669
|
|
|
2384
2670
|
// Prompt memo (Cmd/Ctrl+Shift+N): one freeform Markdown scratchpad with a live split preview, persisted
|
|
@@ -2396,27 +2682,10 @@ function renderMemoMd(text) {
|
|
|
2396
2682
|
return renderMarkdownBlocks(text).map(function (b) { return b.html; }).join('');
|
|
2397
2683
|
}
|
|
2398
2684
|
function openMemoView() {
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
var
|
|
2402
|
-
|
|
2403
|
-
modal.className = 'mc-modal';
|
|
2404
|
-
var panel = document.createElement('div');
|
|
2405
|
-
panel.className = 'mc-modal-panel mc-memo-panel';
|
|
2406
|
-
var head = document.createElement('div');
|
|
2407
|
-
head.className = 'mc-modal-head';
|
|
2408
|
-
var title = document.createElement('span');
|
|
2409
|
-
title.setAttribute('data-i18n', 'memo.title');
|
|
2410
|
-
title.textContent = t('memo.title');
|
|
2411
|
-
var closeBtn = document.createElement('button');
|
|
2412
|
-
closeBtn.type = 'button';
|
|
2413
|
-
closeBtn.className = 'mc-btn mc-ghost';
|
|
2414
|
-
closeBtn.setAttribute('data-i18n', 'merged.close');
|
|
2415
|
-
closeBtn.textContent = t('merged.close');
|
|
2416
|
-
closeBtn.addEventListener('click', function () { modal.remove(); });
|
|
2417
|
-
|
|
2418
|
-
var body = document.createElement('div');
|
|
2419
|
-
body.className = 'mc-memo-body';
|
|
2685
|
+
if (document.getElementById('mc-memo-panel')) { closeMergedMemoDocks(); return; } // the shortcut toggles: 2nd press closes
|
|
2686
|
+
var dock = mountDock('mc-memo-panel', t('memo.title'));
|
|
2687
|
+
var memoBody = document.createElement('div');
|
|
2688
|
+
memoBody.className = 'mc-memo-body';
|
|
2420
2689
|
var area = document.createElement('textarea');
|
|
2421
2690
|
area.className = 'mc-modal-text mc-memo-edit';
|
|
2422
2691
|
area.spellcheck = false;
|
|
@@ -2430,45 +2699,25 @@ function openMemoView() {
|
|
|
2430
2699
|
saveMemo(area.value);
|
|
2431
2700
|
preview.innerHTML = renderMemoMd(area.value);
|
|
2432
2701
|
});
|
|
2433
|
-
|
|
2434
|
-
//
|
|
2435
|
-
// only once a terminal pane exists; enterSendMode reopens the panel if it was closed.
|
|
2436
|
-
var sendBtn = null;
|
|
2702
|
+
// Terminal send: hand the current draft to pane-pick mode. Shown only once a terminal pane exists;
|
|
2703
|
+
// enterSendMode reopens the terminal (which closes this memo dock — the slot is exclusive).
|
|
2437
2704
|
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.paneCount === 'function' && window.__monacoriTerminal.paneCount() > 0) {
|
|
2438
|
-
sendBtn = document.createElement('button');
|
|
2705
|
+
var sendBtn = document.createElement('button');
|
|
2439
2706
|
sendBtn.type = 'button';
|
|
2440
|
-
sendBtn.className = '
|
|
2707
|
+
sendBtn.className = 'dock-btn mc-send-term';
|
|
2441
2708
|
sendBtn.setAttribute('data-i18n', 'merged.sendToTerminal');
|
|
2442
2709
|
sendBtn.textContent = t('merged.sendToTerminal');
|
|
2443
2710
|
sendBtn.addEventListener('click', function () {
|
|
2444
2711
|
var text = area.value;
|
|
2445
|
-
|
|
2712
|
+
dock.close();
|
|
2446
2713
|
window.__monacoriTerminal.enterSendMode(text);
|
|
2447
2714
|
});
|
|
2715
|
+
dock.bar.insertBefore(sendBtn, dock.bar.querySelector('.dock-max'));
|
|
2448
2716
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
body.appendChild(area);
|
|
2454
|
-
body.appendChild(preview);
|
|
2455
|
-
panel.appendChild(head);
|
|
2456
|
-
panel.appendChild(body);
|
|
2457
|
-
modal.appendChild(panel);
|
|
2458
|
-
modal.addEventListener('mousedown', function (e) { if (e.target === modal) modal.remove(); });
|
|
2459
|
-
modal.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); modal.remove(); } });
|
|
2460
|
-
document.body.appendChild(modal);
|
|
2461
|
-
// Focus the editor; Electron async-restores focus to <body>, so retry briefly (same as the composer/merged view).
|
|
2462
|
-
var memoFocusTries = 0;
|
|
2463
|
-
var tryFocusMemo = function () {
|
|
2464
|
-
if (!document.getElementById('mc-memo')) return true;
|
|
2465
|
-
if (document.activeElement === area) return true;
|
|
2466
|
-
try { area.focus(); } catch (e) {}
|
|
2467
|
-
return document.activeElement === area;
|
|
2468
|
-
};
|
|
2469
|
-
if (!tryFocusMemo()) {
|
|
2470
|
-
var memoFocusIv = setInterval(function () { if (tryFocusMemo() || ++memoFocusTries > 12) clearInterval(memoFocusIv); }, 25);
|
|
2471
|
-
}
|
|
2717
|
+
memoBody.appendChild(area);
|
|
2718
|
+
memoBody.appendChild(preview);
|
|
2719
|
+
dock.body.appendChild(memoBody);
|
|
2720
|
+
focusDockField(area, '#mc-memo-panel');
|
|
2472
2721
|
}
|
|
2473
2722
|
|
|
2474
2723
|
document.addEventListener('click', function (event) {
|
|
@@ -2539,6 +2788,7 @@ refreshComments();
|
|
|
2539
2788
|
|
|
2540
2789
|
function setActive(p) {
|
|
2541
2790
|
active = p;
|
|
2791
|
+
if (p && p.labelEl) p.labelEl.classList.remove('has-bell'); // viewing the pane clears its bell badge
|
|
2542
2792
|
panes.forEach(function (q) {
|
|
2543
2793
|
q.el.classList.toggle('is-active', q === p);
|
|
2544
2794
|
// 2+ panes: dim every pane but the active one (no border, just a clean focus cue). A lone pane stays full.
|
|
@@ -2552,6 +2802,11 @@ refreshComments();
|
|
|
2552
2802
|
});
|
|
2553
2803
|
}
|
|
2554
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
|
+
}
|
|
2555
2810
|
function makePane() {
|
|
2556
2811
|
if (!ensureXterm()) return null; // xterm unavailable — leave the panel empty rather than throw
|
|
2557
2812
|
var el = document.createElement('div');
|
|
@@ -2587,6 +2842,9 @@ refreshComments();
|
|
|
2587
2842
|
// Match the PHYSICAL key (e.code), not e.key: under a non-Latin layout/IME (e.g. Korean 한글)
|
|
2588
2843
|
// Cmd+V reports e.key as 'ㅍ', so a key-based check misses it — blurring the terminal and
|
|
2589
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; }
|
|
2590
2848
|
if (e.code === 'KeyC' || e.code === 'KeyV' || e.code === 'KeyX' || e.code === 'KeyA') return true;
|
|
2591
2849
|
try { term.blur(); } catch (x) {}
|
|
2592
2850
|
return false;
|
|
@@ -2594,6 +2852,14 @@ refreshComments();
|
|
|
2594
2852
|
return true;
|
|
2595
2853
|
});
|
|
2596
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
|
+
});
|
|
2597
2863
|
el.addEventListener('mousedown', function (e) { if (e.target !== labelEl) setActive(pane); });
|
|
2598
2864
|
labelEl.addEventListener('dblclick', function () { renamePane(pane); });
|
|
2599
2865
|
panes.push(pane);
|
|
@@ -2641,10 +2907,12 @@ refreshComments();
|
|
|
2641
2907
|
}
|
|
2642
2908
|
|
|
2643
2909
|
function removePane(id) {
|
|
2644
|
-
var
|
|
2645
|
-
|
|
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);
|
|
2646
2915
|
if (i < 0) return;
|
|
2647
|
-
var p = panes[i];
|
|
2648
2916
|
try { p.term.dispose(); } catch (e) {}
|
|
2649
2917
|
if (p.el.parentNode) p.el.parentNode.removeChild(p.el);
|
|
2650
2918
|
panes.splice(i, 1);
|
|
@@ -2652,6 +2920,15 @@ refreshComments();
|
|
|
2652
2920
|
if (panes.length === 0) setOpen(false);
|
|
2653
2921
|
else fitAll();
|
|
2654
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
|
+
}
|
|
2655
2932
|
|
|
2656
2933
|
function split() {
|
|
2657
2934
|
if (panes.length >= MAX_PANES) return;
|
|
@@ -2674,10 +2951,13 @@ refreshComments();
|
|
|
2674
2951
|
|
|
2675
2952
|
function isOpen() { return !panel.classList.contains('hidden'); }
|
|
2676
2953
|
function setOpen(open) {
|
|
2954
|
+
// The terminal shares the bottom dock slot with merged/memo — opening it closes those (exclusive slot).
|
|
2955
|
+
if (open && typeof window.__monacoriCloseDocks === 'function') { try { window.__monacoriCloseDocks(); } catch (e) {} }
|
|
2677
2956
|
panel.classList.toggle('hidden', !open);
|
|
2678
2957
|
document.body.classList.toggle('terminal-open', open);
|
|
2679
2958
|
if (toggleBtn) toggleBtn.classList.toggle('is-active', open);
|
|
2680
2959
|
try { sessionStorage.setItem(openKey, open ? '1' : '0'); } catch (e) {}
|
|
2960
|
+
if (typeof applyDockMaximized === 'function') applyDockMaximized(); // keep Cmd+Shift+' maximize in sync
|
|
2681
2961
|
if (open) {
|
|
2682
2962
|
if (panes.length === 0) makePane();
|
|
2683
2963
|
requestAnimationFrame(function () { fitAll(); if (active) try { active.term.focus(); } catch (e) {} });
|
|
@@ -2782,8 +3062,12 @@ refreshComments();
|
|
|
2782
3062
|
}, true);
|
|
2783
3063
|
window.__monacoriTerminal = {
|
|
2784
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)); },
|
|
2785
3068
|
open: function () { setOpen(true); },
|
|
2786
3069
|
paneCount: function () { return panes.length; },
|
|
3070
|
+
closeActivePane: closeActivePane,
|
|
2787
3071
|
enterSendMode: enterSendMode,
|
|
2788
3072
|
send: function (text) { writeToPane(active || panes[0], text); },
|
|
2789
3073
|
sendToPane: function (i, text) { writeToPane(panes[i] || active || panes[0], text); },
|
|
@@ -2811,10 +3095,11 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function
|
|
|
2811
3095
|
window.monacoriMenu.onDiffUpdate(function (html) { try { applyDiffUpdate(html); } catch (e) {} });
|
|
2812
3096
|
}
|
|
2813
3097
|
if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function') {
|
|
2814
|
-
// 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).
|
|
2815
3100
|
window.monacoriMenu.onCloseTab(function () {
|
|
2816
|
-
|
|
2817
|
-
if (
|
|
3101
|
+
var term = window.__monacoriTerminal;
|
|
3102
|
+
if (term && term.isOpen() && term.hasFocus()) { term.closeActivePane(); return; }
|
|
2818
3103
|
if (isSourceViewerVisible()) closeActiveSourceTab();
|
|
2819
3104
|
});
|
|
2820
3105
|
}
|
|
@@ -2925,6 +3210,12 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
|
|
|
2925
3210
|
if (qta) qta.addEventListener('input', function () { saveMergePrompt('q', qta.value); flash(); });
|
|
2926
3211
|
if (cta) cta.addEventListener('input', function () { saveMergePrompt('c', cta.value); flash(); });
|
|
2927
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
|
+
}
|
|
2928
3219
|
// Language: live-switch the whole UI (no reload). Persist, re-apply the static chrome, then re-render
|
|
2929
3220
|
// any currently-shown dynamic text (open composer / merged modal / index status) so it follows too.
|
|
2930
3221
|
langSelectRef = setupCustomSelect('settings-language',
|
|
@@ -2958,6 +3249,29 @@ function setTab(name) {
|
|
|
2958
3249
|
});
|
|
2959
3250
|
document.getElementById('changes-panel')?.classList.toggle('hidden', name !== 'changes');
|
|
2960
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);
|
|
2961
3275
|
}
|
|
2962
3276
|
// Big repos ship the source tree as an inert island (see render.ts); build it the first time the Files
|
|
2963
3277
|
// tab is opened so the (potentially huge) tree never blocks startup. No-op for inline (small) trees.
|
|
@@ -3053,8 +3367,19 @@ function restoreUiState() {
|
|
|
3053
3367
|
// regions (diff container, sidebar trees, status, data) and re-run the bootstrap steps. The window never
|
|
3054
3368
|
// reloads, so the integrated terminal's pty sessions (claude/codex) survive a watch refresh. Electron's
|
|
3055
3369
|
// main pushes the payload over IPC (monacori:diff-update); serve mode's poller fetches /__ai_flow_update.
|
|
3370
|
+
// Live watch refreshes are HELD while a comment composer is open. applyDiffUpdate rebuilds the diff DOM, so
|
|
3371
|
+
// applying it mid-compose would destroy the composer textarea every watch tick — input stalls and characters
|
|
3372
|
+
// arrive in bursts — and flicker the page. Keep only the latest pending payload; flush it on close/save.
|
|
3373
|
+
var pendingDiffUpdate = null;
|
|
3374
|
+
function flushPendingDiffUpdate() {
|
|
3375
|
+
if (!pendingDiffUpdate) return;
|
|
3376
|
+
var u = pendingDiffUpdate;
|
|
3377
|
+
pendingDiffUpdate = null;
|
|
3378
|
+
try { applyDiffUpdate(u); } catch (e) {}
|
|
3379
|
+
}
|
|
3056
3380
|
function applyDiffUpdate(u) {
|
|
3057
3381
|
if (!u || !u.signature || u.signature === currentSignature) return false; // unchanged — nothing to do
|
|
3382
|
+
if (composerState) { pendingDiffUpdate = u; return false; } // composing a comment — hold the refresh until close/save
|
|
3058
3383
|
|
|
3059
3384
|
// Remember what to restore after the swap (comments/viewed persist on their own; these don't).
|
|
3060
3385
|
var sv = document.getElementById('source-viewer');
|
|
@@ -3062,11 +3387,28 @@ function applyDiffUpdate(u) {
|
|
|
3062
3387
|
var wasSource = isSourceViewerVisible();
|
|
3063
3388
|
var container = document.getElementById('diff2html-container');
|
|
3064
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) : '';
|
|
3065
3394
|
// Did the file the user is CURRENTLY viewing actually change in this build? If not, we must not re-render
|
|
3066
3395
|
// the source view — an unrelated file's edit would otherwise flicker the pane they're reading. Capture the
|
|
3067
3396
|
// open file's signature BEFORE fileSignatureByPath is rebuilt below.
|
|
3068
3397
|
var prevOpenSig = openPath ? (fileSignatureByPath.get(openPath) || '') : '';
|
|
3069
3398
|
|
|
3399
|
+
// Snapshot already-materialized file bodies (keyed by path + current signature) BEFORE the swap, so an
|
|
3400
|
+
// UNCHANGED file can be re-filled synchronously afterwards. Without this, the swap turns every wrapper into
|
|
3401
|
+
// an empty lazy shell that blanks until its body re-loads over IPC — the visible "flicker" on a watch tick.
|
|
3402
|
+
var prevBodies = {};
|
|
3403
|
+
if (REVIEW_LAZY && container) {
|
|
3404
|
+
container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
|
|
3405
|
+
var b = w.querySelector('.d2h-files-diff');
|
|
3406
|
+
if (!b || b.hasAttribute('data-lazy')) return; // only bodies that are actually materialized
|
|
3407
|
+
var p = diffWrapperPathKey(w);
|
|
3408
|
+
if (p) prevBodies[p] = { sig: fileSignatureByPath.get(p) || '', html: b.innerHTML };
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3070
3412
|
// 1) Replace the visible regions straight from the payload (no full-HTML parse).
|
|
3071
3413
|
if (container) container.innerHTML = u.diffContainer || '';
|
|
3072
3414
|
var changesPanel = document.getElementById('changes-panel');
|
|
@@ -3079,6 +3421,13 @@ function applyDiffUpdate(u) {
|
|
|
3079
3421
|
if (filesPanel && (!REVIEW_LAZY || filesPanel.innerHTML.trim())) filesPanel.innerHTML = u.filesTree || '';
|
|
3080
3422
|
var statusEl = document.querySelector('.review-status');
|
|
3081
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
|
+
}
|
|
3082
3431
|
if (reviewMeta) { reviewMeta.setAttribute('data-signature', u.signature); if (u.generatedAt) reviewMeta.setAttribute('data-generated-at', u.generatedAt); }
|
|
3083
3432
|
|
|
3084
3433
|
// 2) Re-derive module-level state directly from the payload objects.
|
|
@@ -3095,7 +3444,21 @@ function applyDiffUpdate(u) {
|
|
|
3095
3444
|
links = Array.from(document.querySelectorAll('#changes-panel .file-link'));
|
|
3096
3445
|
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
3097
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
|
+
|
|
3098
3457
|
// 3) Reset lazy-materialize + index state so the new diff bodies / source / symbols rebuild on demand.
|
|
3458
|
+
// bodyCache is keyed by file INDEX, not content — after a watch rebuild the same index maps to the new
|
|
3459
|
+
// body, so it MUST be dropped too. Clearing only bodyPromise left loadBodyHtml() returning the cached
|
|
3460
|
+
// OLD body, so a watch change never showed up in the diff until a full reload.
|
|
3461
|
+
bodyCache = {};
|
|
3099
3462
|
bodyPromise = {};
|
|
3100
3463
|
diffBootDone = false;
|
|
3101
3464
|
sourceLoaded = !REVIEW_LAZY_LOAD; // lazyLoad: re-fetch source content on next use
|
|
@@ -3104,8 +3467,27 @@ function applyDiffUpdate(u) {
|
|
|
3104
3467
|
// sourceBodyPath so the already-painted (unchanged) source view is left exactly as-is — no flicker.
|
|
3105
3468
|
if (openFileChanged) sourceBodyPath = null;
|
|
3106
3469
|
symbolIndex = null;
|
|
3470
|
+
|
|
3471
|
+
// 3b) Re-fill UNCHANGED files' bodies synchronously from the snapshot so they don't blank-then-reload (the
|
|
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.
|
|
3475
|
+
if (REVIEW_LAZY && container) {
|
|
3476
|
+
container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
|
|
3477
|
+
var p = diffWrapperPathKey(w);
|
|
3478
|
+
var prev = p ? prevBodies[p] : null;
|
|
3479
|
+
if (!prev || !prev.sig || prev.sig !== (fileSignatureByPath.get(p) || '')) return; // changed/new -> lazy-load
|
|
3480
|
+
var shell = w.querySelector('.d2h-files-diff[data-lazy]');
|
|
3481
|
+
if (!shell) return;
|
|
3482
|
+
var idx = (w.id || '').replace('file-', '');
|
|
3483
|
+
materializeBody(w, prev.html); // fills the body + markWrapperHunks (uses the new data-first-hunk)
|
|
3484
|
+
bodyCache[idx] = prev.html; // keep the index cache consistent so it never refetches
|
|
3485
|
+
bodyPromise[idx] = Promise.resolve(w);
|
|
3486
|
+
});
|
|
3487
|
+
}
|
|
3488
|
+
refreshHunkIndex(); // rebuild hunks/hunkMeta from the swapped-in DOM so hunkTotal()/hunkPathAt() aren't stale
|
|
3107
3489
|
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
3108
|
-
else {
|
|
3490
|
+
else { diffBootDone = true; }
|
|
3109
3491
|
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0);
|
|
3110
3492
|
|
|
3111
3493
|
// 4) Re-run the DOM-dependent bootstrap steps.
|
|
@@ -3121,7 +3503,10 @@ function applyDiffUpdate(u) {
|
|
|
3121
3503
|
if (openFileChanged) openSourceFile(openPath, false);
|
|
3122
3504
|
} else if (container) {
|
|
3123
3505
|
showDiffView(false);
|
|
3124
|
-
|
|
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;
|
|
3125
3510
|
}
|
|
3126
3511
|
return true;
|
|
3127
3512
|
}
|
|
@@ -3770,6 +4155,39 @@ function showUsages(name, count) {
|
|
|
3770
4155
|
if (title) title.textContent = count + ' usage' + (count === 1 ? '' : 's') + ' of ' + name;
|
|
3771
4156
|
renderUsages();
|
|
3772
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 = '';
|
|
3773
4191
|
}
|
|
3774
4192
|
function renderUsages() {
|
|
3775
4193
|
var results = document.getElementById('usages-results');
|
|
@@ -3807,7 +4225,11 @@ function openUsageItem(item) {
|
|
|
3807
4225
|
openSourceAt(item.path, item.lineIndex, item.column);
|
|
3808
4226
|
}
|
|
3809
4227
|
function closeUsages() {
|
|
3810
|
-
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
|
|
3811
4233
|
}
|
|
3812
4234
|
|
|
3813
4235
|
var symbolIndex = null; // Map<name, [{path,lineIndex,column}]>; built off-thread by a Web Worker, null until ready
|
|
@@ -3985,11 +4407,18 @@ function renderSourceTabs(activePath) {
|
|
|
3985
4407
|
var active = p === activePath;
|
|
3986
4408
|
return '<div class="source-tab' + (active ? ' active' : '') + '" data-tab-path="' + escapeHtml(p) + '" title="' + escapeHtml(p) + '">'
|
|
3987
4409
|
+ '<span class="source-tab-name">' + escapeHtml(sourceTabLabel(p)) + '</span>'
|
|
3988
|
-
+ '<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>'
|
|
3989
4411
|
+ '</div>';
|
|
3990
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.
|
|
3991
4416
|
var act = bar.querySelector('.source-tab.active');
|
|
3992
|
-
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
|
+
}
|
|
3993
4422
|
}
|
|
3994
4423
|
function closeSourceTab(path) {
|
|
3995
4424
|
var idx = sourceTabs.indexOf(path);
|
|
@@ -4462,7 +4891,7 @@ function renderHttpTable(file) {
|
|
|
4462
4891
|
const reqIdx = hasRun ? runAtLine[index] : -1;
|
|
4463
4892
|
const isCursorLine = Boolean(cursor && cursor.lineIndex === index);
|
|
4464
4893
|
const gutter = hasRun
|
|
4465
|
-
? '<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>'
|
|
4466
4895
|
: '';
|
|
4467
4896
|
rows += '<tr class="source-row http-row' + (hasRun ? ' http-request-line' : '') + (isCursorLine ? ' cursor-line' : '') + '" data-line-index="' + index + '">'
|
|
4468
4897
|
+ '<td class="num http-gutter">' + gutter + '<span class="num-text">' + (index + 1) + '</span></td>'
|