@happy-nut/monacori 0.1.20 → 0.1.21
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 +403 -155
- package/dist/build.d.ts +1 -0
- package/dist/build.js +8 -6
- package/dist/diff.d.ts +2 -1
- package/dist/diff.js +3 -3
- package/dist/i18n.js +6 -0
- package/dist/preload.cjs +7 -0
- package/dist/render.d.ts +4 -0
- package/dist/render.js +84 -0
- package/dist/viewer.client.js +317 -126
- package/dist/viewer.css +52 -8
- package/package.json +9 -2
- package/scripts/patch-electron-name.mjs +23 -14
package/dist/viewer.client.js
CHANGED
|
@@ -98,14 +98,19 @@ function whenFileReady(wrapper, cb) {
|
|
|
98
98
|
if (bodyPromise[idx]) { bodyPromise[idx].then(function () { cb(); }); return; }
|
|
99
99
|
cb();
|
|
100
100
|
}
|
|
101
|
+
var lazyIO = null; // remembered so each setupLazyDiff (re-run on every watch refresh) disconnects the prior
|
|
102
|
+
// observer instead of leaving a new one bound to detached wrappers — otherwise observers
|
|
103
|
+
// (and the old DOM they retain) pile up over a long-running session and slowly choke it.
|
|
101
104
|
function setupLazyDiff() {
|
|
102
105
|
var container = document.getElementById('diff2html-container');
|
|
103
106
|
if (!container) return;
|
|
107
|
+
if (lazyIO) { try { lazyIO.disconnect(); } catch (e) {} lazyIO = null; }
|
|
104
108
|
var wrappers = Array.prototype.slice.call(container.querySelectorAll('.d2h-file-wrapper'));
|
|
105
109
|
if (typeof IntersectionObserver !== 'undefined') {
|
|
106
110
|
var io = new IntersectionObserver(function (entries) {
|
|
107
111
|
entries.forEach(function (e) { if (e.isIntersecting) { ensureFileReady(e.target); io.unobserve(e.target); } });
|
|
108
112
|
}, { root: null, rootMargin: '600px 0px' });
|
|
113
|
+
lazyIO = io; // track this observer so the NEXT setupLazyDiff can disconnect it (callback keeps using local io)
|
|
109
114
|
wrappers.forEach(function (w) { io.observe(w); });
|
|
110
115
|
} else {
|
|
111
116
|
wrappers.forEach(function (w) { ensureFileReady(w); }); // no IntersectionObserver -> materialize all
|
|
@@ -508,7 +513,7 @@ function revealAt(el, scroller, fraction) {
|
|
|
508
513
|
}
|
|
509
514
|
// Scrolloff variant: scroll ONLY when `el` would otherwise leave the viewport, keeping it within `marginFrac`
|
|
510
515
|
// 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.
|
|
516
|
+
// centering scrolled the file even when everything was visible (dizzying). Used by the diff caret and the sidebar tree.
|
|
512
517
|
function scrolloffReveal(el, scroller, marginFrac) {
|
|
513
518
|
if (!el || !scroller || !scroller.clientHeight) return;
|
|
514
519
|
var top = el.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
|
|
@@ -675,6 +680,22 @@ function next(delta) {
|
|
|
675
680
|
// Every changed file is marked viewed — nothing left to review, so F7/[/] stay put.
|
|
676
681
|
}
|
|
677
682
|
|
|
683
|
+
// Jump to the first change of the next unviewed file after `path` (wrapping). Used right after marking a
|
|
684
|
+
// file viewed: its diff body is now hidden, so staying would blank the content — we advance to the next
|
|
685
|
+
// change instead. Returns false when every changed file is viewed (nothing to advance to).
|
|
686
|
+
function gotoNextUnviewedFile(path) {
|
|
687
|
+
const total = hunkTotal();
|
|
688
|
+
if (total === 0) return false;
|
|
689
|
+
const start = firstHunkForPath(path);
|
|
690
|
+
let idx = (start >= 0 ? start : (current >= 0 ? current : 0)) + 1;
|
|
691
|
+
for (let step = 0; step < total; step++) {
|
|
692
|
+
const norm = ((idx % total) + total) % total;
|
|
693
|
+
if (!isFileViewed(hunkPathAt(norm) || '')) { setActive(norm); return true; }
|
|
694
|
+
idx += 1;
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
|
|
678
699
|
function initialHunkForNavigation(delta) {
|
|
679
700
|
const openPath = document.getElementById('source-viewer')?.dataset.openPath || '';
|
|
680
701
|
const sourceHunk = firstHunkForPath(openPath);
|
|
@@ -916,8 +937,10 @@ function focusTree(index) {
|
|
|
916
937
|
if (rows.length === 0) return;
|
|
917
938
|
treeFocusIndex = Math.max(0, Math.min(rows.length - 1, index));
|
|
918
939
|
// 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
|
-
//
|
|
940
|
+
// frame; moving the focus class instantly while the coalesced scroll lags makes the panel jump. Coalescing
|
|
941
|
+
// both keeps focus + scroll in lockstep, and scrolloffReveal scrolls ONLY when the focused row nears the
|
|
942
|
+
// top/bottom edge — a row moving inside the visible band must never drag the whole panel (revealAt did,
|
|
943
|
+
// re-centering on every move so even a mid-list row scrolled the sidebar).
|
|
921
944
|
scheduleTreeFocus();
|
|
922
945
|
}
|
|
923
946
|
var treeFocusRaf = 0;
|
|
@@ -929,7 +952,7 @@ function scheduleTreeFocus() {
|
|
|
929
952
|
if (treeFocusIndex < 0 || treeFocusIndex >= rows.length) return;
|
|
930
953
|
const el = rows[treeFocusIndex];
|
|
931
954
|
document.querySelectorAll('.tree-focus').forEach((e) => { if (e !== el) e.classList.remove('tree-focus'); });
|
|
932
|
-
if (el) { el.classList.add('tree-focus');
|
|
955
|
+
if (el) { el.classList.add('tree-focus'); scrolloffReveal(el, document.querySelector('.sidebar-scroll'), 0.15); }
|
|
933
956
|
});
|
|
934
957
|
}
|
|
935
958
|
|
|
@@ -1053,9 +1076,11 @@ function handleTreeKey(event) {
|
|
|
1053
1076
|
// owns focus AND the only caret, so global shortcuts stand down until Esc/close — we must not navigate a
|
|
1054
1077
|
// panel the user can't even see behind the overlay (nor leave a second blinking caret in it).
|
|
1055
1078
|
function isFloatingModalOpen() {
|
|
1056
|
-
if (document.getElementById('mc-modal') || document.getElementById('mc-memo')) return true;
|
|
1057
1079
|
var sm = document.getElementById('settings-modal');
|
|
1058
|
-
|
|
1080
|
+
if (sm && !sm.classList.contains('hidden')) return true;
|
|
1081
|
+
// The merged/memo panels are now docked (inline), not overlays — but while one OWNS focus we still stand
|
|
1082
|
+
// down the global nav shortcuts so typing / ▲▼ inside it isn't hijacked. Focus elsewhere -> shortcuts run.
|
|
1083
|
+
return isDockFocused();
|
|
1059
1084
|
}
|
|
1060
1085
|
document.addEventListener('keydown', (event) => {
|
|
1061
1086
|
if (!quickOpen?.classList.contains('hidden')) {
|
|
@@ -1066,9 +1091,29 @@ document.addEventListener('keydown', (event) => {
|
|
|
1066
1091
|
if (handleUsagesKey(event)) return;
|
|
1067
1092
|
}
|
|
1068
1093
|
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
1094
|
+
// Dock controls fire regardless of focus (terminal / merged / memo) — they sit ABOVE the focus guard so
|
|
1095
|
+
// they still work from inside a dock panel. Cmd/Ctrl+Shift+' maximizes the active dock; Cmd/Ctrl+Shift+/
|
|
1096
|
+
// and +. open the merged views; Cmd/Ctrl+Shift+N toggles the memo. (Match event.code so IME/layout never
|
|
1097
|
+
// swallows the combo.) Settings is a true overlay, so these stand down while it is up.
|
|
1098
|
+
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') {
|
|
1100
|
+
event.preventDefault();
|
|
1101
|
+
toggleDockMaximized();
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && (event.code === 'Slash' || event.code === 'Period' || event.key === '?' || event.key === '>')) {
|
|
1105
|
+
event.preventDefault();
|
|
1106
|
+
openMergedView((event.code === 'Slash' || event.key === '?') ? 'q' : 'c');
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (!settingsUp && (event.metaKey || event.ctrlKey) && event.shiftKey && (event.code === 'KeyN' || event.key === 'n' || event.key === 'N')) {
|
|
1110
|
+
event.preventDefault();
|
|
1111
|
+
openMemoView();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Settings overlay (or a focused merged/memo dock) captures keys: stand down the rest of the global
|
|
1116
|
+
// shortcuts (Cmd+1, F7, Cmd+[/], Cmd+B, …). Each has its own Esc + editing handlers.
|
|
1072
1117
|
if (isFloatingModalOpen()) return;
|
|
1073
1118
|
|
|
1074
1119
|
if ((event.metaKey || event.ctrlKey) && event.key === '1') {
|
|
@@ -1121,21 +1166,8 @@ document.addEventListener('keydown', (event) => {
|
|
|
1121
1166
|
}
|
|
1122
1167
|
}
|
|
1123
1168
|
|
|
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
|
-
}
|
|
1169
|
+
// (Merged views Cmd/Ctrl+Shift+/ +. and the memo Cmd/Ctrl+Shift+N are handled above the focus guard so
|
|
1170
|
+
// they work from inside a dock too.)
|
|
1139
1171
|
// "?" = question, ">" = change-request composer on the current line/selection (no modifier).
|
|
1140
1172
|
if (!event.altKey && !event.metaKey && !event.ctrlKey && (event.key === '?' || event.key === '>')) {
|
|
1141
1173
|
const ce = document.activeElement;
|
|
@@ -1160,7 +1192,12 @@ document.addEventListener('keydown', (event) => {
|
|
|
1160
1192
|
}
|
|
1161
1193
|
if (vp && currentFileSignature(vp)) {
|
|
1162
1194
|
event.preventDefault();
|
|
1163
|
-
|
|
1195
|
+
const willView = !isFileViewed(vp);
|
|
1196
|
+
setFileViewed(vp, willView);
|
|
1197
|
+
// Marking viewed hides this file's diff body — don't strand the caret on the now-blank file.
|
|
1198
|
+
// Auto-advance to the next unviewed change (the user's flow: mark viewed -> jump to next).
|
|
1199
|
+
// Unmarking stays put. If every file is viewed, gotoNextUnviewedFile is a no-op.
|
|
1200
|
+
if (willView) gotoNextUnviewedFile(vp);
|
|
1164
1201
|
return;
|
|
1165
1202
|
}
|
|
1166
1203
|
}
|
|
@@ -1184,6 +1221,10 @@ document.addEventListener('keydown', (event) => {
|
|
|
1184
1221
|
var psc = isDiffViewVisible() ? document.getElementById('diff2html-container') : (isSourceViewerVisible() ? document.getElementById('source-body') : null);
|
|
1185
1222
|
if (psc) { event.preventDefault(); psc.scrollTop += (event.key === 'PageDown' ? 0.9 : -0.9) * psc.clientHeight; return; }
|
|
1186
1223
|
}
|
|
1224
|
+
// A non-Shift keystroke between the two Shifts cancels the pending double-Shift quick-open. Without this,
|
|
1225
|
+
// "Shift → type something → Shift" within 300ms still popped the search, so it fired on nearly every other
|
|
1226
|
+
// keystroke. Reset BEFORE the caret handlers below (they swallow arrows) so arrow keys break it too.
|
|
1227
|
+
if (event.key !== 'Shift') { lastShiftAt = 0; lastShiftSide = 0; }
|
|
1187
1228
|
if (treeFocusIndex >= 0 && handleTreeKey(event)) return;
|
|
1188
1229
|
if (treeFocusIndex < 0 && !event.metaKey && !event.ctrlKey && !event.altKey && isSourceViewerVisible() && handleSourceCaretKey(event)) return;
|
|
1189
1230
|
if (treeFocusIndex < 0 && !event.metaKey && !event.ctrlKey && !event.altKey && isDiffViewVisible() && handleDiffCaretKey(event)) return;
|
|
@@ -1304,8 +1345,12 @@ document.addEventListener('keydown', (event) => {
|
|
|
1304
1345
|
// where they were reading. Shift+F7 — and any file with no hunk of its own — falls through to plain
|
|
1305
1346
|
// prev/next-change navigation across the whole diff.
|
|
1306
1347
|
if (delta > 0 && sourceViewer && !sourceViewer.classList.contains('hidden')) {
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1348
|
+
const sp = sourceViewer.dataset.openPath || '';
|
|
1349
|
+
const sourceHunk = firstHunkForPath(sp);
|
|
1350
|
+
// Enter the diff at the open file's own hunk — UNLESS it's already viewed. A viewed file's diff body
|
|
1351
|
+
// is hidden (display:none), so landing on it blanks the content and F7 appears stuck; fall through to
|
|
1352
|
+
// next() instead so we skip to an unviewed change.
|
|
1353
|
+
if (sourceHunk >= 0 && !isFileViewed(sp)) {
|
|
1309
1354
|
setActive(sourceHunk);
|
|
1310
1355
|
return;
|
|
1311
1356
|
}
|
|
@@ -1405,7 +1450,11 @@ if (!restored) {
|
|
|
1405
1450
|
else openDefaultSourceFile();
|
|
1406
1451
|
}
|
|
1407
1452
|
initSourceTreeFolds();
|
|
1408
|
-
|
|
1453
|
+
// Electron receives live updates over IPC (monacoriMenu.onDiffUpdate); only serve/browser needs the HTTP
|
|
1454
|
+
// poller. Under file:// its fetch just fails every 1.5s for the app's whole life, so skip it in Electron.
|
|
1455
|
+
if (watchEnabled && !(window.monacoriMenu && typeof window.monacoriMenu.onDiffUpdate === 'function')) {
|
|
1456
|
+
setInterval(checkForLiveUpdate, 1500);
|
|
1457
|
+
}
|
|
1409
1458
|
window.addEventListener('beforeunload', saveUiState);
|
|
1410
1459
|
|
|
1411
1460
|
// First render has painted — drop the boot overlay (it bridged the blank gap right after loadFile). Two
|
|
@@ -1793,11 +1842,51 @@ function moveDiffWord(dir, extend) {
|
|
|
1793
1842
|
setDiffCursor(diffCursor.path, diffCursor.side, diffCursor.rowIndex, ncol, true);
|
|
1794
1843
|
if (anchor) { diffSelectionAnchor = anchor; applyDiffSelection(); }
|
|
1795
1844
|
}
|
|
1845
|
+
// Comment boxes are injected on the right(new) side, right after the line's row (see injectThreadRow /
|
|
1846
|
+
// renderDiffComments). Split-view rows align 1:1 by index, so the caret's row index on the new side finds
|
|
1847
|
+
// the adjacent box regardless of which side the caret sits on. Mirrors commentRowSiblingOf for the source view.
|
|
1848
|
+
function diffCommentBoxSiblingOf(dir) {
|
|
1849
|
+
if (!diffCursor) return null;
|
|
1850
|
+
var wrapper = diffWrapperByPath(diffCursor.path);
|
|
1851
|
+
if (!wrapper) return null;
|
|
1852
|
+
var rows = diffRowsOf(diffSideTable(wrapper, 'new'));
|
|
1853
|
+
var row = rows[diffCursor.rowIndex];
|
|
1854
|
+
if (!row) return null;
|
|
1855
|
+
var sib = dir < 0 ? row.previousElementSibling : row.nextElementSibling;
|
|
1856
|
+
return (sib && sib.classList && sib.classList.contains('mc-comment-row')) ? sib : null;
|
|
1857
|
+
}
|
|
1796
1858
|
function handleDiffCaretKey(event) {
|
|
1797
1859
|
if (!isDiffViewVisible() || !diffCursor) return false;
|
|
1798
1860
|
var ae = document.activeElement;
|
|
1799
1861
|
if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.tagName === 'SELECT')) return false;
|
|
1800
1862
|
var extend = event.shiftKey;
|
|
1863
|
+
// A comment box is selected: Backspace/Delete removes it, `e` edits it, an arrow/Escape steps off it.
|
|
1864
|
+
// Same contract as the source view (handleSourceCaretKey), but caret moves go through setDiffCursor.
|
|
1865
|
+
if (selectedCommentRow) {
|
|
1866
|
+
if (event.key === 'Backspace' || event.key === 'Delete') { event.preventDefault(); deleteCommentsInRow(selectedCommentRow); return true; }
|
|
1867
|
+
if (event.key === 'e' || event.key === 'E') { event.preventDefault(); editCommentInRow(selectedCommentRow); return true; }
|
|
1868
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'Escape') {
|
|
1869
|
+
var dir = event.key === 'ArrowUp' ? -1 : (event.key === 'ArrowDown' ? 1 : 0);
|
|
1870
|
+
var sib = dir < 0 ? selectedCommentRow.previousElementSibling : (dir > 0 ? selectedCommentRow.nextElementSibling : null);
|
|
1871
|
+
selectedCommentRow.classList.remove('mc-row-selected');
|
|
1872
|
+
selectedCommentRow = null;
|
|
1873
|
+
event.preventDefault();
|
|
1874
|
+
var wrapper = diffWrapperByPath(diffCursor.path);
|
|
1875
|
+
if (sib && wrapper && isDiffCodeRow(sib)) {
|
|
1876
|
+
var rows = diffRowsOf(diffSideTable(wrapper, 'new'));
|
|
1877
|
+
var idx = rows.indexOf(sib);
|
|
1878
|
+
if (idx >= 0) { setDiffCursor(diffCursor.path, 'new', idx, 0, true); return true; }
|
|
1879
|
+
}
|
|
1880
|
+
setDiffCursor(diffCursor.path, diffCursor.side, diffCursor.rowIndex, diffCursor.column, false); // restore caret where it was
|
|
1881
|
+
return true;
|
|
1882
|
+
}
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
// Plain Up/Down: a comment box attached to the caret line is a selectable stop (caret stays visible).
|
|
1886
|
+
if (!extend && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
|
|
1887
|
+
var box = diffCommentBoxSiblingOf(event.key === 'ArrowUp' ? -1 : 1);
|
|
1888
|
+
if (box) { event.preventDefault(); selectCommentRow(box); return true; }
|
|
1889
|
+
}
|
|
1801
1890
|
if (event.key === 'ArrowDown') { event.preventDefault(); moveDiffCursor(1, 0, extend); return true; }
|
|
1802
1891
|
if (event.key === 'ArrowUp') { event.preventDefault(); moveDiffCursor(-1, 0, extend); return true; }
|
|
1803
1892
|
if (event.key === 'ArrowLeft') { event.preventDefault(); moveDiffCursor(0, -1, extend); return true; }
|
|
@@ -2127,6 +2216,7 @@ function closeComposer() {
|
|
|
2127
2216
|
if (!composerState) return;
|
|
2128
2217
|
composerState = null;
|
|
2129
2218
|
refreshComments();
|
|
2219
|
+
flushPendingDiffUpdate(); // apply any live watch refresh that was held while composing
|
|
2130
2220
|
}
|
|
2131
2221
|
// The composer is injected into BOTH the diff and source views (refreshComments renders comments in
|
|
2132
2222
|
// each), but only one view is on screen at a time — the other lives inside a `.hidden` container with
|
|
@@ -2151,6 +2241,7 @@ function saveComposer(ta) {
|
|
|
2151
2241
|
else addComment(composerState.kind, composerState.path, composerState.line, composerState.code, box.value);
|
|
2152
2242
|
composerState = null;
|
|
2153
2243
|
refreshComments();
|
|
2244
|
+
flushPendingDiffUpdate(); // apply any live watch refresh that was held while composing
|
|
2154
2245
|
}
|
|
2155
2246
|
|
|
2156
2247
|
// Default merge-prompt headings, localized: a Korean user gets Korean defaults. Editable in
|
|
@@ -2292,36 +2383,145 @@ function buildMergedText(kind) {
|
|
|
2292
2383
|
return lines.join(nl);
|
|
2293
2384
|
}
|
|
2294
2385
|
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2386
|
+
// ===== Bottom dock: merged-prompt / memo / terminal share ONE docked slot below the editor =====
|
|
2387
|
+
// Only one is visible at a time — opening one closes the others (the terminal included). Cmd/Ctrl+Shift+'
|
|
2388
|
+
// maximizes the active dock over the editor area (the sidebar stays). A top resizer drags the height.
|
|
2389
|
+
var dockHeightKey = 'monacori-dock-height';
|
|
2390
|
+
var dockMaximized = false;
|
|
2391
|
+
function applyDockHeight(px) {
|
|
2392
|
+
var h = Math.max(140, Math.min(px, window.innerHeight - 120));
|
|
2393
|
+
document.documentElement.style.setProperty('--dock-height', h + 'px');
|
|
2394
|
+
}
|
|
2395
|
+
(function () { var s = parseInt(localStorage.getItem(dockHeightKey) || '', 10); if (s) applyDockHeight(s); })();
|
|
2396
|
+
// The dock panel currently filling the slot: a merged/memo panel, else the terminal when it's open.
|
|
2397
|
+
function activeDockPanel() {
|
|
2398
|
+
var mm = document.getElementById('mc-merged-panel') || document.getElementById('mc-memo-panel');
|
|
2399
|
+
if (mm) return mm;
|
|
2400
|
+
var term = document.getElementById('terminal-panel');
|
|
2401
|
+
return (term && !term.classList.contains('hidden')) ? term : null;
|
|
2402
|
+
}
|
|
2403
|
+
function applyDockMaximized() {
|
|
2404
|
+
if (!activeDockPanel()) dockMaximized = false; // nothing docked -> can't stay maximized
|
|
2405
|
+
document.body.classList.toggle('dock-maximized', dockMaximized);
|
|
2406
|
+
}
|
|
2407
|
+
function toggleDockMaximized() {
|
|
2408
|
+
if (!activeDockPanel()) return; // nothing docked -> nothing to maximize
|
|
2409
|
+
dockMaximized = !dockMaximized;
|
|
2410
|
+
applyDockMaximized();
|
|
2411
|
+
}
|
|
2412
|
+
function isDockFocused() {
|
|
2413
|
+
var ae = document.activeElement;
|
|
2414
|
+
return !!(ae && ae.closest && ae.closest('.dock-panel'));
|
|
2415
|
+
}
|
|
2416
|
+
// Close the merged/memo docks (the terminal's setOpen also calls this so the slot stays exclusive).
|
|
2417
|
+
function closeMergedMemoDocks() {
|
|
2418
|
+
var m = document.getElementById('mc-merged-panel'); if (m) m.remove();
|
|
2419
|
+
var n = document.getElementById('mc-memo-panel'); if (n) n.remove();
|
|
2420
|
+
document.querySelectorAll('.dock-backdrop').forEach(function (b) { b.remove(); });
|
|
2421
|
+
document.body.classList.toggle('dock-open', !!activeDockPanel());
|
|
2422
|
+
// floating-dock tracks merged/memo only (NOT the terminal) so the maximize CSS hides content for a
|
|
2423
|
+
// terminal dock but never for these floating panels.
|
|
2424
|
+
document.body.classList.toggle('floating-dock', !!(document.getElementById('mc-merged-panel') || document.getElementById('mc-memo-panel')));
|
|
2425
|
+
applyDockMaximized();
|
|
2426
|
+
}
|
|
2427
|
+
window.__monacoriCloseDocks = closeMergedMemoDocks;
|
|
2428
|
+
// Retry-focus a docked field (Electron async-restores focus to <body>, so a one-shot focus can lose the race).
|
|
2429
|
+
function focusDockField(field, panelSel) {
|
|
2430
|
+
var tries = 0;
|
|
2431
|
+
var tryF = function () {
|
|
2432
|
+
if (!document.querySelector(panelSel)) return true;
|
|
2433
|
+
if (document.activeElement === field) return true;
|
|
2434
|
+
try { field.focus(); } catch (e) {}
|
|
2435
|
+
return document.activeElement === field;
|
|
2436
|
+
};
|
|
2437
|
+
if (!tryF()) { var iv = setInterval(function () { if (tryF() || ++tries > 12) clearInterval(iv); }, 25); }
|
|
2438
|
+
}
|
|
2439
|
+
// Build a docked panel shell (resizer + bar with Maximize/Close + body) and mount it below the editor.
|
|
2440
|
+
// Opening it closes the terminal and any other merged/memo dock (the slot is exclusive). Returns
|
|
2441
|
+
// { panel, body, bar, close }.
|
|
2442
|
+
function mountDock(id, titleText) {
|
|
2443
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.close === 'function') {
|
|
2444
|
+
try { window.__monacoriTerminal.close(); } catch (e) {}
|
|
2445
|
+
}
|
|
2446
|
+
var prior = document.getElementById(id);
|
|
2447
|
+
if (prior) prior.remove();
|
|
2448
|
+
closeMergedMemoDocks();
|
|
2302
2449
|
var panel = document.createElement('div');
|
|
2303
|
-
panel.
|
|
2304
|
-
|
|
2305
|
-
|
|
2450
|
+
panel.id = id;
|
|
2451
|
+
panel.className = 'dock-panel';
|
|
2452
|
+
panel.tabIndex = -1;
|
|
2453
|
+
// The panel floats over the editor; a dim backdrop sits behind it (click to dismiss).
|
|
2454
|
+
var backdrop = document.createElement('div');
|
|
2455
|
+
backdrop.className = 'dock-backdrop';
|
|
2456
|
+
var resizer = document.createElement('div');
|
|
2457
|
+
resizer.className = 'dock-resizer';
|
|
2458
|
+
resizer.setAttribute('aria-hidden', 'true');
|
|
2459
|
+
var bar = document.createElement('div');
|
|
2460
|
+
bar.className = 'dock-bar';
|
|
2306
2461
|
var title = document.createElement('span');
|
|
2307
|
-
title.
|
|
2462
|
+
title.className = 'dock-title';
|
|
2463
|
+
title.textContent = titleText;
|
|
2464
|
+
var maxBtn = document.createElement('button');
|
|
2465
|
+
maxBtn.type = 'button';
|
|
2466
|
+
maxBtn.className = 'dock-btn dock-max';
|
|
2467
|
+
maxBtn.setAttribute('data-i18n-title', 'dock.maximize');
|
|
2468
|
+
maxBtn.title = t('dock.maximize');
|
|
2469
|
+
maxBtn.textContent = '⤢'; // ⤢ maximize glyph
|
|
2308
2470
|
var closeBtn = document.createElement('button');
|
|
2309
2471
|
closeBtn.type = 'button';
|
|
2310
|
-
closeBtn.className = '
|
|
2472
|
+
closeBtn.className = 'dock-btn dock-close';
|
|
2473
|
+
closeBtn.setAttribute('data-i18n', 'merged.close');
|
|
2311
2474
|
closeBtn.textContent = t('merged.close');
|
|
2475
|
+
var body = document.createElement('div');
|
|
2476
|
+
body.className = 'dock-body';
|
|
2477
|
+
bar.appendChild(title);
|
|
2478
|
+
bar.appendChild(maxBtn);
|
|
2479
|
+
bar.appendChild(closeBtn);
|
|
2480
|
+
panel.appendChild(resizer);
|
|
2481
|
+
panel.appendChild(bar);
|
|
2482
|
+
panel.appendChild(body);
|
|
2483
|
+
document.body.appendChild(backdrop);
|
|
2484
|
+
document.body.appendChild(panel);
|
|
2485
|
+
function close() { panel.remove(); backdrop.remove(); closeMergedMemoDocks(); }
|
|
2486
|
+
maxBtn.addEventListener('click', function () { toggleDockMaximized(); });
|
|
2487
|
+
closeBtn.addEventListener('click', close);
|
|
2488
|
+
backdrop.addEventListener('click', close); // click the dim behind the panel to dismiss
|
|
2489
|
+
// Esc closes the dock when focus is inside it; the editor keeps its own handlers otherwise.
|
|
2490
|
+
panel.addEventListener('keydown', function (e) {
|
|
2491
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); }
|
|
2492
|
+
});
|
|
2493
|
+
resizer.addEventListener('mousedown', function (e) {
|
|
2494
|
+
e.preventDefault();
|
|
2495
|
+
resizer.classList.add('resizing');
|
|
2496
|
+
function move(ev) { applyDockHeight(window.innerHeight - ev.clientY); }
|
|
2497
|
+
function up() {
|
|
2498
|
+
resizer.classList.remove('resizing');
|
|
2499
|
+
document.removeEventListener('mousemove', move);
|
|
2500
|
+
document.removeEventListener('mouseup', up);
|
|
2501
|
+
var cur = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--dock-height'), 10);
|
|
2502
|
+
if (cur) { try { localStorage.setItem(dockHeightKey, String(cur)); } catch (x) {} }
|
|
2503
|
+
}
|
|
2504
|
+
document.addEventListener('mousemove', move);
|
|
2505
|
+
document.addEventListener('mouseup', up);
|
|
2506
|
+
});
|
|
2507
|
+
document.body.classList.add('dock-open');
|
|
2508
|
+
document.body.classList.add('floating-dock'); // scopes the maximize CSS so it doesn't hide the diff
|
|
2509
|
+
applyDockMaximized();
|
|
2510
|
+
return { panel: panel, body: body, bar: bar, close: close };
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function openMergedView(kind) {
|
|
2514
|
+
var dock = mountDock('mc-merged-panel', kind === 'q' ? t('merged.qTitle') : t('merged.cTitle'));
|
|
2515
|
+
dock.panel.dataset.kind = kind; // remembered so a live locale switch can re-render this same view
|
|
2312
2516
|
var area = document.createElement('textarea');
|
|
2313
2517
|
area.className = 'mc-modal-text';
|
|
2314
2518
|
// 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.
|
|
2519
|
+
// which comment Opt+Enter / Opt+Arrow will target. Block every edit via beforeinput instead.
|
|
2317
2520
|
area.value = buildMergedText(kind);
|
|
2318
2521
|
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.
|
|
2522
|
+
// Opt/Alt+Enter on the merged text: a custom dropdown for the comment under the caret. Opt/Alt+Arrow steps
|
|
2523
|
+
// the caret comment-to-comment so each can be acted on without hand-scrolling.
|
|
2322
2524
|
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
2525
|
if (e.altKey && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
|
|
2326
2526
|
e.preventDefault();
|
|
2327
2527
|
e.stopPropagation();
|
|
@@ -2336,49 +2536,28 @@ function openMergedView(kind) {
|
|
|
2336
2536
|
var cxy = mergedCaretXY(area);
|
|
2337
2537
|
var x = cxy.x, y = cxy.below, flipTop = cxy.top;
|
|
2338
2538
|
var rerender = function () {
|
|
2339
|
-
if (!reviewComments.filter(function (c) { return c.kind === kind; }).length) {
|
|
2539
|
+
if (!reviewComments.filter(function (c) { return c.kind === kind; }).length) { dock.close(); return; }
|
|
2340
2540
|
area.value = buildMergedText(kind);
|
|
2341
2541
|
};
|
|
2342
2542
|
if (area.selectionStart !== area.selectionEnd || seqs.length > 1) {
|
|
2343
2543
|
// 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
2544
|
var multi = [];
|
|
2346
|
-
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.
|
|
2347
|
-
multi.push({ label: t('merged.sendToTerminal'), onSelect: function () { var text = buildMergedText(kind);
|
|
2545
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.paneCount === 'function' && window.__monacoriTerminal.paneCount() > 0) {
|
|
2546
|
+
multi.push({ label: t('merged.sendToTerminal'), onSelect: function () { var text = buildMergedText(kind); dock.close(); window.__monacoriTerminal.enterSendMode(text); } });
|
|
2348
2547
|
}
|
|
2349
2548
|
multi.push({ label: t('dropdown.remove'), onSelect: function () { seqs.forEach(deleteComment); rerender(); } });
|
|
2350
2549
|
showCustomDropdown(x, y, multi, flipTop);
|
|
2351
2550
|
} else {
|
|
2352
2551
|
var seq = seqs[0];
|
|
2353
2552
|
showCustomDropdown(x, y, [
|
|
2354
|
-
{ label: t('dropdown.navigate'), onSelect: function () {
|
|
2553
|
+
{ label: t('dropdown.navigate'), onSelect: function () { dock.close(); navigateToComment(seq); } },
|
|
2355
2554
|
{ label: t('dropdown.remove'), onSelect: function () { deleteComment(seq); rerender(); } },
|
|
2356
2555
|
], flipTop);
|
|
2357
2556
|
}
|
|
2358
2557
|
});
|
|
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
|
-
}
|
|
2558
|
+
dock.body.appendChild(area);
|
|
2559
|
+
// Focus the read-only text so the caret is visible and Opt+Arrow / Opt+Enter work; retry (Electron focus race).
|
|
2560
|
+
focusDockField(area, '#mc-merged-panel');
|
|
2382
2561
|
}
|
|
2383
2562
|
|
|
2384
2563
|
// Prompt memo (Cmd/Ctrl+Shift+N): one freeform Markdown scratchpad with a live split preview, persisted
|
|
@@ -2396,27 +2575,10 @@ function renderMemoMd(text) {
|
|
|
2396
2575
|
return renderMarkdownBlocks(text).map(function (b) { return b.html; }).join('');
|
|
2397
2576
|
}
|
|
2398
2577
|
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';
|
|
2578
|
+
if (document.getElementById('mc-memo-panel')) { closeMergedMemoDocks(); return; } // the shortcut toggles: 2nd press closes
|
|
2579
|
+
var dock = mountDock('mc-memo-panel', t('memo.title'));
|
|
2580
|
+
var memoBody = document.createElement('div');
|
|
2581
|
+
memoBody.className = 'mc-memo-body';
|
|
2420
2582
|
var area = document.createElement('textarea');
|
|
2421
2583
|
area.className = 'mc-modal-text mc-memo-edit';
|
|
2422
2584
|
area.spellcheck = false;
|
|
@@ -2430,45 +2592,25 @@ function openMemoView() {
|
|
|
2430
2592
|
saveMemo(area.value);
|
|
2431
2593
|
preview.innerHTML = renderMemoMd(area.value);
|
|
2432
2594
|
});
|
|
2433
|
-
|
|
2434
|
-
//
|
|
2435
|
-
// only once a terminal pane exists; enterSendMode reopens the panel if it was closed.
|
|
2436
|
-
var sendBtn = null;
|
|
2595
|
+
// Terminal send: hand the current draft to pane-pick mode. Shown only once a terminal pane exists;
|
|
2596
|
+
// enterSendMode reopens the terminal (which closes this memo dock — the slot is exclusive).
|
|
2437
2597
|
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.paneCount === 'function' && window.__monacoriTerminal.paneCount() > 0) {
|
|
2438
|
-
sendBtn = document.createElement('button');
|
|
2598
|
+
var sendBtn = document.createElement('button');
|
|
2439
2599
|
sendBtn.type = 'button';
|
|
2440
|
-
sendBtn.className = '
|
|
2600
|
+
sendBtn.className = 'dock-btn mc-send-term';
|
|
2441
2601
|
sendBtn.setAttribute('data-i18n', 'merged.sendToTerminal');
|
|
2442
2602
|
sendBtn.textContent = t('merged.sendToTerminal');
|
|
2443
2603
|
sendBtn.addEventListener('click', function () {
|
|
2444
2604
|
var text = area.value;
|
|
2445
|
-
|
|
2605
|
+
dock.close();
|
|
2446
2606
|
window.__monacoriTerminal.enterSendMode(text);
|
|
2447
2607
|
});
|
|
2608
|
+
dock.bar.insertBefore(sendBtn, dock.bar.querySelector('.dock-max'));
|
|
2448
2609
|
}
|
|
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
|
-
}
|
|
2610
|
+
memoBody.appendChild(area);
|
|
2611
|
+
memoBody.appendChild(preview);
|
|
2612
|
+
dock.body.appendChild(memoBody);
|
|
2613
|
+
focusDockField(area, '#mc-memo-panel');
|
|
2472
2614
|
}
|
|
2473
2615
|
|
|
2474
2616
|
document.addEventListener('click', function (event) {
|
|
@@ -2674,10 +2816,13 @@ refreshComments();
|
|
|
2674
2816
|
|
|
2675
2817
|
function isOpen() { return !panel.classList.contains('hidden'); }
|
|
2676
2818
|
function setOpen(open) {
|
|
2819
|
+
// The terminal shares the bottom dock slot with merged/memo — opening it closes those (exclusive slot).
|
|
2820
|
+
if (open && typeof window.__monacoriCloseDocks === 'function') { try { window.__monacoriCloseDocks(); } catch (e) {} }
|
|
2677
2821
|
panel.classList.toggle('hidden', !open);
|
|
2678
2822
|
document.body.classList.toggle('terminal-open', open);
|
|
2679
2823
|
if (toggleBtn) toggleBtn.classList.toggle('is-active', open);
|
|
2680
2824
|
try { sessionStorage.setItem(openKey, open ? '1' : '0'); } catch (e) {}
|
|
2825
|
+
if (typeof applyDockMaximized === 'function') applyDockMaximized(); // keep Cmd+Shift+' maximize in sync
|
|
2681
2826
|
if (open) {
|
|
2682
2827
|
if (panes.length === 0) makePane();
|
|
2683
2828
|
requestAnimationFrame(function () { fitAll(); if (active) try { active.term.focus(); } catch (e) {} });
|
|
@@ -3053,8 +3198,19 @@ function restoreUiState() {
|
|
|
3053
3198
|
// regions (diff container, sidebar trees, status, data) and re-run the bootstrap steps. The window never
|
|
3054
3199
|
// reloads, so the integrated terminal's pty sessions (claude/codex) survive a watch refresh. Electron's
|
|
3055
3200
|
// main pushes the payload over IPC (monacori:diff-update); serve mode's poller fetches /__ai_flow_update.
|
|
3201
|
+
// Live watch refreshes are HELD while a comment composer is open. applyDiffUpdate rebuilds the diff DOM, so
|
|
3202
|
+
// applying it mid-compose would destroy the composer textarea every watch tick — input stalls and characters
|
|
3203
|
+
// arrive in bursts — and flicker the page. Keep only the latest pending payload; flush it on close/save.
|
|
3204
|
+
var pendingDiffUpdate = null;
|
|
3205
|
+
function flushPendingDiffUpdate() {
|
|
3206
|
+
if (!pendingDiffUpdate) return;
|
|
3207
|
+
var u = pendingDiffUpdate;
|
|
3208
|
+
pendingDiffUpdate = null;
|
|
3209
|
+
try { applyDiffUpdate(u); } catch (e) {}
|
|
3210
|
+
}
|
|
3056
3211
|
function applyDiffUpdate(u) {
|
|
3057
3212
|
if (!u || !u.signature || u.signature === currentSignature) return false; // unchanged — nothing to do
|
|
3213
|
+
if (composerState) { pendingDiffUpdate = u; return false; } // composing a comment — hold the refresh until close/save
|
|
3058
3214
|
|
|
3059
3215
|
// Remember what to restore after the swap (comments/viewed persist on their own; these don't).
|
|
3060
3216
|
var sv = document.getElementById('source-viewer');
|
|
@@ -3067,6 +3223,19 @@ function applyDiffUpdate(u) {
|
|
|
3067
3223
|
// open file's signature BEFORE fileSignatureByPath is rebuilt below.
|
|
3068
3224
|
var prevOpenSig = openPath ? (fileSignatureByPath.get(openPath) || '') : '';
|
|
3069
3225
|
|
|
3226
|
+
// Snapshot already-materialized file bodies (keyed by path + current signature) BEFORE the swap, so an
|
|
3227
|
+
// UNCHANGED file can be re-filled synchronously afterwards. Without this, the swap turns every wrapper into
|
|
3228
|
+
// an empty lazy shell that blanks until its body re-loads over IPC — the visible "flicker" on a watch tick.
|
|
3229
|
+
var prevBodies = {};
|
|
3230
|
+
if (REVIEW_LAZY && container) {
|
|
3231
|
+
container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
|
|
3232
|
+
var b = w.querySelector('.d2h-files-diff');
|
|
3233
|
+
if (!b || b.hasAttribute('data-lazy')) return; // only bodies that are actually materialized
|
|
3234
|
+
var p = diffWrapperPathKey(w);
|
|
3235
|
+
if (p) prevBodies[p] = { sig: fileSignatureByPath.get(p) || '', html: b.innerHTML };
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3070
3239
|
// 1) Replace the visible regions straight from the payload (no full-HTML parse).
|
|
3071
3240
|
if (container) container.innerHTML = u.diffContainer || '';
|
|
3072
3241
|
var changesPanel = document.getElementById('changes-panel');
|
|
@@ -3096,6 +3265,10 @@ function applyDiffUpdate(u) {
|
|
|
3096
3265
|
sourceLinks = Array.from(document.querySelectorAll('.source-link'));
|
|
3097
3266
|
|
|
3098
3267
|
// 3) Reset lazy-materialize + index state so the new diff bodies / source / symbols rebuild on demand.
|
|
3268
|
+
// bodyCache is keyed by file INDEX, not content — after a watch rebuild the same index maps to the new
|
|
3269
|
+
// body, so it MUST be dropped too. Clearing only bodyPromise left loadBodyHtml() returning the cached
|
|
3270
|
+
// OLD body, so a watch change never showed up in the diff until a full reload.
|
|
3271
|
+
bodyCache = {};
|
|
3099
3272
|
bodyPromise = {};
|
|
3100
3273
|
diffBootDone = false;
|
|
3101
3274
|
sourceLoaded = !REVIEW_LAZY_LOAD; // lazyLoad: re-fetch source content on next use
|
|
@@ -3108,6 +3281,24 @@ function applyDiffUpdate(u) {
|
|
|
3108
3281
|
else { prepareDiff2HtmlHunks(); diffBootDone = true; }
|
|
3109
3282
|
if (!REVIEW_LAZY_LOAD) setTimeout(startSymbolIndex, 0);
|
|
3110
3283
|
|
|
3284
|
+
// 3b) Re-fill UNCHANGED files' bodies synchronously from the snapshot so they don't blank-then-reload (the
|
|
3285
|
+
// flicker). The fresh wrapper carries the correct data-first-hunk + file index, so materializeBody numbers
|
|
3286
|
+
// hunks exactly as a normal lazy load would — this only skips the IPC round-trip for files whose content is
|
|
3287
|
+
// identical. Changed/new files stay shells and lazy-load as usual, so a real edit still refreshes the diff.
|
|
3288
|
+
if (REVIEW_LAZY && container) {
|
|
3289
|
+
container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
|
|
3290
|
+
var p = diffWrapperPathKey(w);
|
|
3291
|
+
var prev = p ? prevBodies[p] : null;
|
|
3292
|
+
if (!prev || !prev.sig || prev.sig !== (fileSignatureByPath.get(p) || '')) return; // changed/new -> lazy-load
|
|
3293
|
+
var shell = w.querySelector('.d2h-files-diff[data-lazy]');
|
|
3294
|
+
if (!shell) return;
|
|
3295
|
+
var idx = (w.id || '').replace('file-', '');
|
|
3296
|
+
materializeBody(w, prev.html); // fills the body + markWrapperHunks (uses the new data-first-hunk)
|
|
3297
|
+
bodyCache[idx] = prev.html; // keep the index cache consistent so it never refetches
|
|
3298
|
+
bodyPromise[idx] = Promise.resolve(w);
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3111
3302
|
// 4) Re-run the DOM-dependent bootstrap steps.
|
|
3112
3303
|
applyI18n();
|
|
3113
3304
|
populateHttpEnvSelect();
|