@happy-nut/monacori 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/i18n.js +2 -2
- package/dist/viewer.client.js +185 -79
- package/dist/viewer.css +15 -7
- package/package.json +1 -1
package/dist/i18n.js
CHANGED
|
@@ -35,6 +35,7 @@ export const MESSAGES = {
|
|
|
35
35
|
"btn.viewed": "Viewed",
|
|
36
36
|
"btn.viewed.title": "Toggle viewed (<)",
|
|
37
37
|
"diff.noDiff": "No diff to review.",
|
|
38
|
+
"diff.lastHunk": "Last change in this file — press F7 again to go to the next file.",
|
|
38
39
|
// Source toolbar
|
|
39
40
|
"source.title": "Source",
|
|
40
41
|
"source.selectFile": "Select a file from the Files tab.",
|
|
@@ -131,7 +132,6 @@ export const MESSAGES = {
|
|
|
131
132
|
"merged.close": "Close",
|
|
132
133
|
"dropdown.navigate": "Go to comment",
|
|
133
134
|
"dropdown.remove": "Remove",
|
|
134
|
-
"toast.commentsDropped": "Removed {n} comment(s) on {file} — the file changed too much to track them",
|
|
135
135
|
"merged.qHeading": "# Questions",
|
|
136
136
|
"merged.cHeading": "# Change requests",
|
|
137
137
|
// Prompt memo (Cmd/Ctrl+Shift+N) — a single freeform Markdown scratchpad with a live split preview.
|
|
@@ -167,6 +167,7 @@ export const MESSAGES = {
|
|
|
167
167
|
"btn.viewed": "확인함",
|
|
168
168
|
"btn.viewed.title": "확인 표시 토글 (<)",
|
|
169
169
|
"diff.noDiff": "검토할 변경사항이 없습니다.",
|
|
170
|
+
"diff.lastHunk": "이 파일의 마지막 변경입니다 — F7을 한 번 더 누르면 다음 파일로 이동합니다.",
|
|
170
171
|
// Source toolbar
|
|
171
172
|
"source.title": "소스",
|
|
172
173
|
"source.selectFile": "파일 탭에서 파일을 선택하세요.",
|
|
@@ -263,7 +264,6 @@ export const MESSAGES = {
|
|
|
263
264
|
"merged.close": "닫기",
|
|
264
265
|
"dropdown.navigate": "코멘트로 이동",
|
|
265
266
|
"dropdown.remove": "지우기",
|
|
266
|
-
"toast.commentsDropped": "{file}이(가) 변경되어 추적할 수 없는 코멘트 {n}개를 제거했습니다",
|
|
267
267
|
// Structural markers stay English in both locales (the preamble prose below follows the locale).
|
|
268
268
|
"merged.qHeading": "# Questions",
|
|
269
269
|
"merged.cHeading": "# Change requests",
|
package/dist/viewer.client.js
CHANGED
|
@@ -319,7 +319,6 @@ function prepareDiff2HtmlHunks() {
|
|
|
319
319
|
prepareViewedControls();
|
|
320
320
|
|
|
321
321
|
function prepareViewedControls() {
|
|
322
|
-
pruneViewedState();
|
|
323
322
|
document.querySelectorAll('.d2h-file-wrapper').forEach((wrapper) => {
|
|
324
323
|
const fileName = wrapper.querySelector('.d2h-file-name')?.textContent?.trim() || '';
|
|
325
324
|
const toggle = wrapper.querySelector('.d2h-file-collapse');
|
|
@@ -356,34 +355,23 @@ function currentFileSignature(path) {
|
|
|
356
355
|
|
|
357
356
|
function isFileViewed(path) {
|
|
358
357
|
const viewed = loadViewedState();
|
|
359
|
-
|
|
360
|
-
return Boolean(signature && viewed[path] === signature);
|
|
358
|
+
return Boolean(viewed[path]); // boolean now; legacy signature strings are also truthy, so old marks still read as viewed
|
|
361
359
|
}
|
|
362
360
|
|
|
363
361
|
function setFileViewed(path, viewed) {
|
|
364
362
|
const state = loadViewedState();
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
363
|
+
// Persist a plain boolean (not the file signature) so a viewed mark survives a restart/refresh the way
|
|
364
|
+
// comments do. Tying it to the signature meant any re-generation that changed the signature silently
|
|
365
|
+
// cleared every viewed mark — exactly the "viewed didn't persist" the user hit.
|
|
366
|
+
if (viewed) state[path] = true;
|
|
367
|
+
else delete state[path];
|
|
371
368
|
saveViewedState(state);
|
|
372
369
|
applyViewedState();
|
|
373
370
|
}
|
|
374
371
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
Object.keys(state).forEach((path) => {
|
|
379
|
-
if (state[path] !== currentFileSignature(path)) {
|
|
380
|
-
delete state[path];
|
|
381
|
-
changed = true;
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
if (changed) saveViewedState(state);
|
|
385
|
-
}
|
|
386
|
-
|
|
372
|
+
// Viewed marks persist by path (a plain boolean), like comments — we deliberately DON'T prune on signature
|
|
373
|
+
// change or restart. Tying persistence to the file signature is what made viewed marks vanish on every
|
|
374
|
+
// re-generation; the user wants them to survive restarts the way comments do.
|
|
387
375
|
function applyViewedState() {
|
|
388
376
|
document.querySelectorAll('.d2h-file-wrapper').forEach((wrapper) => {
|
|
389
377
|
const fileName = wrapper.querySelector('.d2h-file-name')?.textContent?.trim() || '';
|
|
@@ -518,6 +506,18 @@ function revealAt(el, scroller, fraction) {
|
|
|
518
506
|
var off = el.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
|
|
519
507
|
scroller.scrollTop += off - scroller.clientHeight * fraction;
|
|
520
508
|
}
|
|
509
|
+
// Scrolloff variant: scroll ONLY when `el` would otherwise leave the viewport, keeping it within `marginFrac`
|
|
510
|
+
// 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.
|
|
512
|
+
function scrolloffReveal(el, scroller, marginFrac) {
|
|
513
|
+
if (!el || !scroller || !scroller.clientHeight) return;
|
|
514
|
+
var top = el.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
|
|
515
|
+
var rowH = el.offsetHeight || 18;
|
|
516
|
+
var ch = scroller.clientHeight;
|
|
517
|
+
var margin = Math.round(ch * marginFrac);
|
|
518
|
+
if (top < margin) scroller.scrollTop += top - margin;
|
|
519
|
+
else if (top + rowH > ch - margin) scroller.scrollTop += (top + rowH) - (ch - margin);
|
|
520
|
+
}
|
|
521
521
|
function scheduleScrollIntoView(el) {
|
|
522
522
|
pendingScrollEl = el || null;
|
|
523
523
|
if (scrollElRaf) return;
|
|
@@ -557,7 +557,7 @@ function applySetActive(idx, shouldScroll) {
|
|
|
557
557
|
history.replaceState(null, '', '#hunk-' + idx);
|
|
558
558
|
// Row-dependent work waits for the file body (sync for eager/Phase 1, async for cold lazy-LOAD).
|
|
559
559
|
whenFileReady(diffWrapperByPath(file), function () {
|
|
560
|
-
showOnlyFile(file);
|
|
560
|
+
showOnlyFile(file, true); // materialize + isolate the file, but leave the caret to focusDiffRow (skip ensureDiffCursor)
|
|
561
561
|
const active = document.getElementById('hunk-' + idx);
|
|
562
562
|
if (!active) return;
|
|
563
563
|
if (REVIEW_LAZY) {
|
|
@@ -571,16 +571,24 @@ function applySetActive(idx, shouldScroll) {
|
|
|
571
571
|
// F7/change navigation moves the caret but must NOT pollute the Cmd+[/] cursor history.
|
|
572
572
|
navSuppress = true;
|
|
573
573
|
try { focusDiffRow(targetRow); } finally { navSuppress = false; }
|
|
574
|
-
|
|
574
|
+
// Scroll inline in THIS frame, NOT via scheduleDiffScroll's extra rAF. showOnlyFile just display:none'd
|
|
575
|
+
// the previous file, but the scroll container keeps its old (larger) scrollTop — so for one frame the new
|
|
576
|
+
// file renders at that stale offset (≈ line 146) before a deferred scroll snaps to the change (≈ line 21):
|
|
577
|
+
// the visible 146→21 double jump on F7 across a file boundary. Scrolling synchronously here lands the
|
|
578
|
+
// view on the change before this frame paints, so the new file appears already at its first change.
|
|
579
|
+
if (shouldScroll && targetRow && targetRow.scrollIntoView) targetRow.scrollIntoView({ block: 'center' });
|
|
575
580
|
});
|
|
576
581
|
}
|
|
577
582
|
|
|
578
|
-
function showOnlyFile(fileName) {
|
|
583
|
+
function showOnlyFile(fileName, skipCursor) {
|
|
579
584
|
if (REVIEW_LAZY) ensureFileReady(diffWrapperByPath(fileName));
|
|
580
585
|
document.querySelectorAll('.d2h-file-wrapper').forEach((wrapper) => {
|
|
581
586
|
wrapper.classList.toggle('df-inactive', diffWrapperPathKey(wrapper) !== fileName);
|
|
582
587
|
});
|
|
583
|
-
|
|
588
|
+
// applySetActive passes skipCursor: it sets the caret itself via focusDiffRow(targetRow). Letting
|
|
589
|
+
// ensureDiffCursor run here would first place the caret on the file's FIRST code row, then focusDiffRow
|
|
590
|
+
// overrides it to the change — a visible double jump (the F7 "first line → change" flash).
|
|
591
|
+
if (!skipCursor) ensureDiffCursor();
|
|
584
592
|
}
|
|
585
593
|
|
|
586
594
|
// The hunk the diff caret currently sits in. Arrow keys move the caret without touching the active
|
|
@@ -622,6 +630,10 @@ function changeBlockAnchors(wrapper) {
|
|
|
622
630
|
return anchors;
|
|
623
631
|
}
|
|
624
632
|
|
|
633
|
+
// Forward F7 at a file's last change announces "last change — press F7 again" once before crossing to the
|
|
634
|
+
// next file, giving a beat to mark-viewed. Holds the path we've already announced; any caret move clears it
|
|
635
|
+
// (see setDiffCursor), so leaving and returning to the last change re-arms the announcement.
|
|
636
|
+
var pendingFileBoundary = null;
|
|
625
637
|
function next(delta) {
|
|
626
638
|
if (hunkTotal() === 0) return;
|
|
627
639
|
// Within the caret's (unviewed) file, step change-block by change-block so a context-merged hunk
|
|
@@ -640,7 +652,18 @@ function next(delta) {
|
|
|
640
652
|
}
|
|
641
653
|
}
|
|
642
654
|
}
|
|
643
|
-
// File boundary
|
|
655
|
+
// File boundary: no more change blocks in this file. Forward F7 announces "last change — press F7 again
|
|
656
|
+
// to go to the next file" on the FIRST press (a beat to mark-viewed) and only crosses on the SECOND
|
|
657
|
+
// consecutive press. Already-viewed files (and backward nav) cross immediately — no announcement.
|
|
658
|
+
if (delta > 0 && diffCursor && isDiffViewVisible() && !isFileViewed(diffCursor.path)) {
|
|
659
|
+
if (pendingFileBoundary !== diffCursor.path) {
|
|
660
|
+
pendingFileBoundary = diffCursor.path;
|
|
661
|
+
showToast(t('diff.lastHunk'));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
pendingFileBoundary = null; // second consecutive press on the same file → fall through and cross
|
|
665
|
+
}
|
|
666
|
+
// hunk-level nav to the next/prev unviewed file.
|
|
644
667
|
const caretHunk = hunkIndexAtCaret();
|
|
645
668
|
const base = caretHunk >= 0 ? caretHunk : current;
|
|
646
669
|
let idx = base < 0 ? initialHunkForNavigation(delta) : base + delta;
|
|
@@ -1026,6 +1049,14 @@ function handleTreeKey(event) {
|
|
|
1026
1049
|
if (Math.abs(e.deltaY) >= Math.abs(e.deltaX) && e.deltaY !== 0) { dsc.scrollTop += e.deltaY; e.preventDefault(); }
|
|
1027
1050
|
}, { passive: false });
|
|
1028
1051
|
})();
|
|
1052
|
+
// A floating, focus-grabbing overlay (merged-comments, prompt memo, settings) is open. While one is up it
|
|
1053
|
+
// owns focus AND the only caret, so global shortcuts stand down until Esc/close — we must not navigate a
|
|
1054
|
+
// panel the user can't even see behind the overlay (nor leave a second blinking caret in it).
|
|
1055
|
+
function isFloatingModalOpen() {
|
|
1056
|
+
if (document.getElementById('mc-modal') || document.getElementById('mc-memo')) return true;
|
|
1057
|
+
var sm = document.getElementById('settings-modal');
|
|
1058
|
+
return !!(sm && !sm.classList.contains('hidden'));
|
|
1059
|
+
}
|
|
1029
1060
|
document.addEventListener('keydown', (event) => {
|
|
1030
1061
|
if (!quickOpen?.classList.contains('hidden')) {
|
|
1031
1062
|
if (handleQuickOpenKey(event)) return;
|
|
@@ -1035,8 +1066,22 @@ document.addEventListener('keydown', (event) => {
|
|
|
1035
1066
|
if (handleUsagesKey(event)) return;
|
|
1036
1067
|
}
|
|
1037
1068
|
|
|
1069
|
+
// Floating overlay open (merged / memo / settings): it captures keys until Esc. Don't run ANY global
|
|
1070
|
+
// shortcut (Cmd+1, F7, Cmd+[/], Cmd+B, open-merged/memo, …) underneath — focus and the only caret belong
|
|
1071
|
+
// to the overlay. Each overlay has its own Esc + editing handlers, so we simply stand down here.
|
|
1072
|
+
if (isFloatingModalOpen()) return;
|
|
1073
|
+
|
|
1038
1074
|
if ((event.metaKey || event.ctrlKey) && event.key === '1') {
|
|
1039
1075
|
event.preventDefault();
|
|
1076
|
+
// Coming from the diff: open the file you were viewing as source so Cmd+1 lands ON it (not a stale/blank
|
|
1077
|
+
// source pane), and the tree below points at the same file. Capture the path BEFORE openSourceFile flips
|
|
1078
|
+
// the view (isDiffViewVisible would then be false).
|
|
1079
|
+
if (isDiffViewVisible()) {
|
|
1080
|
+
var dw1 = diffActiveWrapper();
|
|
1081
|
+
var dn1 = dw1 && dw1.querySelector('.d2h-file-name');
|
|
1082
|
+
var dpath1 = (diffCursor && diffCursor.path) || (dn1 ? (dn1.textContent || '').trim() : '');
|
|
1083
|
+
if (dpath1 && sourceByPath.has(dpath1)) openSourceFile(dpath1);
|
|
1084
|
+
}
|
|
1040
1085
|
setTab('files');
|
|
1041
1086
|
focusOpenFileInTree();
|
|
1042
1087
|
return;
|
|
@@ -1542,9 +1587,17 @@ function renderDiffCaret() {
|
|
|
1542
1587
|
row.classList.add('mc-diff-cursor-row');
|
|
1543
1588
|
var ctn = diffCellCtn(row);
|
|
1544
1589
|
if (!ctn) return;
|
|
1545
|
-
// Empty line (ctn is just a <br>):
|
|
1546
|
-
//
|
|
1547
|
-
if ((ctn.textContent || '').length === 0)
|
|
1590
|
+
// Empty line (ctn is just a <br>): an inline caret span would wrap onto a 2nd visual line and break the
|
|
1591
|
+
// row height, so position the caret absolutely — it shows without affecting the layout.
|
|
1592
|
+
if ((ctn.textContent || '').length === 0) {
|
|
1593
|
+
var espan = document.createElement('span');
|
|
1594
|
+
espan.className = 'code-cursor';
|
|
1595
|
+
espan.setAttribute('aria-hidden', 'true');
|
|
1596
|
+
espan.style.position = 'absolute';
|
|
1597
|
+
ctn.appendChild(espan);
|
|
1598
|
+
diffCaretSpan = espan;
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1548
1601
|
var pos = diffCaretDomPosition(ctn, diffCursor.column);
|
|
1549
1602
|
if (!pos) return;
|
|
1550
1603
|
var span = document.createElement('span');
|
|
@@ -1568,6 +1621,7 @@ function setDiffCursor(path, side, rowIndex, column, reveal) {
|
|
|
1568
1621
|
var ri = Math.max(0, Math.min(rowIndex, rows.length - 1));
|
|
1569
1622
|
var col = Math.max(0, Math.min(column, diffLineText(rows[ri]).length));
|
|
1570
1623
|
diffCursor = { path: path, side: side, rowIndex: ri, column: col };
|
|
1624
|
+
pendingFileBoundary = null; // any caret move re-arms the last-change announcement for the next F7 (see next)
|
|
1571
1625
|
diffSelectionAnchor = null; // any direct caret placement (click/F7/Cmd-arrow) drops the selection; Shift+Arrow re-sets it
|
|
1572
1626
|
if (reveal) {
|
|
1573
1627
|
// Render the caret AND scroll in the SAME animation frame. A fast key-repeat queues several ArrowDowns
|
|
@@ -1592,7 +1646,7 @@ function scheduleDiffReveal(wrapper, side, ri) {
|
|
|
1592
1646
|
applyDiffSelection();
|
|
1593
1647
|
if (!t) return;
|
|
1594
1648
|
var row = diffRowAt(t.wrapper, t.side, t.ri);
|
|
1595
|
-
|
|
1649
|
+
scrolloffReveal(row, document.getElementById('diff2html-container'), 0.15);
|
|
1596
1650
|
});
|
|
1597
1651
|
}
|
|
1598
1652
|
function navEntryOf(kind) {
|
|
@@ -1768,35 +1822,30 @@ function showToast(message) {
|
|
|
1768
1822
|
setTimeout(function () { if (el.parentNode) el.parentNode.removeChild(el); }, 300);
|
|
1769
1823
|
}, 4500);
|
|
1770
1824
|
}
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
1774
|
-
//
|
|
1825
|
+
// Follow each comment to its snapshot line (c.code) in the current content: same line if unchanged, else the
|
|
1826
|
+
// nearest exact match of that line. A comment is NEVER auto-deleted. If its line can't be found we leave it
|
|
1827
|
+
// where it is — this happens routinely WITHOUT the file changing: a comment anchored to a deleted/old-side
|
|
1828
|
+
// diff line (comments carry no side, so old-side text never matches the new content) would otherwise vanish.
|
|
1829
|
+
// Silently dropping user-authored comments loses data; the reviewer can remove a stale one with the × button.
|
|
1830
|
+
// Files whose content isn't loaded yet (lazy) are skipped here and reconciled once loadSourceData arrives.
|
|
1775
1831
|
function remapComments() {
|
|
1776
1832
|
if (!reviewComments.length) return;
|
|
1777
|
-
var
|
|
1778
|
-
reviewComments
|
|
1833
|
+
var moved = 0;
|
|
1834
|
+
reviewComments.forEach(function (c) {
|
|
1779
1835
|
var file = sourceByPath.get(c.path);
|
|
1780
|
-
if (!file || !file.embedded || typeof file.content !== 'string' || !file.content) return
|
|
1836
|
+
if (!file || !file.embedded || typeof file.content !== 'string' || !file.content) return;
|
|
1781
1837
|
var code = c.code == null ? '' : String(c.code);
|
|
1782
|
-
if (!code.trim()) return
|
|
1838
|
+
if (!code.trim()) return;
|
|
1783
1839
|
var lines = file.content.split(/\r?\n/);
|
|
1784
|
-
if (lines[c.line - 1] === code) return
|
|
1840
|
+
if (lines[c.line - 1] === code) return;
|
|
1785
1841
|
var best = -1, bestDist = Infinity;
|
|
1786
1842
|
for (var i = 0; i < lines.length; i++) {
|
|
1787
1843
|
if (lines[i] === code) { var d = Math.abs(i - (c.line - 1)); if (d < bestDist) { bestDist = d; best = i; } }
|
|
1788
1844
|
}
|
|
1789
|
-
if (best >= 0
|
|
1790
|
-
dropped.push(c);
|
|
1791
|
-
return false;
|
|
1845
|
+
if (best >= 0 && c.line !== best + 1) { c.line = best + 1; moved++; } // moved to follow the line; not found -> keep as-is
|
|
1792
1846
|
});
|
|
1793
|
-
if (!
|
|
1847
|
+
if (!moved) return; // nothing moved — skip the save/re-render
|
|
1794
1848
|
saveComments();
|
|
1795
|
-
var byPath = {};
|
|
1796
|
-
dropped.forEach(function (c) { byPath[c.path] = (byPath[c.path] || 0) + 1; });
|
|
1797
|
-
Object.keys(byPath).forEach(function (p) {
|
|
1798
|
-
showToast(t('toast.commentsDropped').replace('{n}', byPath[p]).replace('{file}', String(p).split('/').pop()));
|
|
1799
|
-
});
|
|
1800
1849
|
refreshComments();
|
|
1801
1850
|
}
|
|
1802
1851
|
function saveComments() {
|
|
@@ -1821,6 +1870,14 @@ function addComment(kind, path, line, code, text) {
|
|
|
1821
1870
|
reviewComments.push({ seq: commentSeq, kind: kind, path: path, line: line, code: String(code || ''), text: trimmed });
|
|
1822
1871
|
saveComments();
|
|
1823
1872
|
}
|
|
1873
|
+
// Edit an existing comment in place (e on a selected box -> composer prefilled -> save). Empty text deletes it.
|
|
1874
|
+
function updateComment(seq, text) {
|
|
1875
|
+
var c = reviewComments.find(function (x) { return x.seq === seq; });
|
|
1876
|
+
if (!c) return;
|
|
1877
|
+
var trimmed = String(text || '').trim();
|
|
1878
|
+
if (trimmed) { c.text = trimmed; saveComments(); }
|
|
1879
|
+
else { deleteComment(seq); }
|
|
1880
|
+
}
|
|
1824
1881
|
function deleteComment(seq) {
|
|
1825
1882
|
reviewComments = reviewComments.filter(function (c) { return c.seq !== seq; });
|
|
1826
1883
|
saveComments();
|
|
@@ -1901,6 +1958,7 @@ function composerTargetLabel(s) {
|
|
|
1901
1958
|
function threadHtml(path, line) {
|
|
1902
1959
|
var html = '';
|
|
1903
1960
|
commentsAt(path, line).forEach(function (c) {
|
|
1961
|
+
if (composerState && composerState.editSeq === c.seq) return; // being edited -> rendered as the composer below
|
|
1904
1962
|
html += '<div class="mc-card mc-' + c.kind + '">'
|
|
1905
1963
|
+ '<div class="mc-card-head"><span class="mc-kind">' + commentKindLabel(c.kind) + '</span>'
|
|
1906
1964
|
+ '<button type="button" class="mc-del" data-seq="' + c.seq + '" title="' + escapeHtml(t('composer.delete')) + '">×</button></div>'
|
|
@@ -1910,7 +1968,7 @@ function threadHtml(path, line) {
|
|
|
1910
1968
|
var ph = composerState.kind === 'q' ? t('composer.question') : t('composer.changeRequest');
|
|
1911
1969
|
html += '<div class="mc-card mc-' + composerState.kind + ' mc-composer">'
|
|
1912
1970
|
+ '<div class="mc-card-head"><span class="mc-kind">' + commentKindLabel(composerState.kind) + '</span><span class="mc-target" title="' + escapeHtml(composerState.path || '') + '">' + escapeHtml(composerTargetLabel(composerState)) + '</span></div>'
|
|
1913
|
-
+ '<textarea class="mc-input" rows="3" placeholder="' + escapeHtml(ph) + '"
|
|
1971
|
+
+ '<textarea class="mc-input" rows="3" placeholder="' + escapeHtml(ph) + '">' + escapeHtml(composerState.editText || '') + '</textarea>'
|
|
1914
1972
|
+ '<div class="mc-actions"><button type="button" class="mc-btn mc-save">' + escapeHtml(t('composer.save')) + '</button>'
|
|
1915
1973
|
+ '<button type="button" class="mc-btn mc-ghost mc-cancel">' + escapeHtml(t('composer.cancel')) + '</button>'
|
|
1916
1974
|
+ '<span class="mc-hint">' + escapeHtml(t('composer.hint')) + '</span></div></div>';
|
|
@@ -2089,7 +2147,8 @@ function saveComposer(ta) {
|
|
|
2089
2147
|
if (!composerState) return;
|
|
2090
2148
|
var box = ta || activeComposerInput();
|
|
2091
2149
|
if (!box) return;
|
|
2092
|
-
|
|
2150
|
+
if (composerState.editSeq != null) updateComment(composerState.editSeq, box.value);
|
|
2151
|
+
else addComment(composerState.kind, composerState.path, composerState.line, composerState.code, box.value);
|
|
2093
2152
|
composerState = null;
|
|
2094
2153
|
refreshComments();
|
|
2095
2154
|
}
|
|
@@ -2281,7 +2340,14 @@ function openMergedView(kind) {
|
|
|
2281
2340
|
area.value = buildMergedText(kind);
|
|
2282
2341
|
};
|
|
2283
2342
|
if (area.selectionStart !== area.selectionEnd || seqs.length > 1) {
|
|
2284
|
-
|
|
2343
|
+
// 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
|
+
var multi = [];
|
|
2346
|
+
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.isOpen === 'function' && window.__monacoriTerminal.isOpen()) {
|
|
2347
|
+
multi.push({ label: t('merged.sendToTerminal'), onSelect: function () { var text = buildMergedText(kind); modal.remove(); window.__monacoriTerminal.enterSendMode(text); } });
|
|
2348
|
+
}
|
|
2349
|
+
multi.push({ label: t('dropdown.remove'), onSelect: function () { seqs.forEach(deleteComment); rerender(); } });
|
|
2350
|
+
showCustomDropdown(x, y, multi, flipTop);
|
|
2285
2351
|
} else {
|
|
2286
2352
|
var seq = seqs[0];
|
|
2287
2353
|
showCustomDropdown(x, y, [
|
|
@@ -2291,23 +2357,8 @@ function openMergedView(kind) {
|
|
|
2291
2357
|
}
|
|
2292
2358
|
});
|
|
2293
2359
|
closeBtn.addEventListener('click', function () { modal.remove(); });
|
|
2294
|
-
//
|
|
2295
|
-
// terminal — the chosen pane is highlighted, the rest dimmed, arrows change the choice, Enter sends.
|
|
2296
|
-
// One button here; the actual pick happens visually over the live claude/codex sessions.
|
|
2297
|
-
var sendBtn = null;
|
|
2298
|
-
if (window.__monacoriTerminal && typeof window.__monacoriTerminal.isOpen === 'function' && window.__monacoriTerminal.isOpen()) {
|
|
2299
|
-
sendBtn = document.createElement('button');
|
|
2300
|
-
sendBtn.type = 'button';
|
|
2301
|
-
sendBtn.className = 'mc-btn mc-send-term';
|
|
2302
|
-
sendBtn.textContent = t('merged.sendToTerminal');
|
|
2303
|
-
sendBtn.addEventListener('click', function () {
|
|
2304
|
-
var text = buildMergedText(kind);
|
|
2305
|
-
modal.remove();
|
|
2306
|
-
window.__monacoriTerminal.enterSendMode(text);
|
|
2307
|
-
});
|
|
2308
|
-
}
|
|
2360
|
+
// Send-to-terminal now lives in the Opt+Enter dropdown (select-all -> first item), not as a header button.
|
|
2309
2361
|
head.appendChild(title);
|
|
2310
|
-
if (sendBtn) head.appendChild(sendBtn);
|
|
2311
2362
|
head.appendChild(closeBtn);
|
|
2312
2363
|
panel.appendChild(head);
|
|
2313
2364
|
panel.appendChild(area);
|
|
@@ -2315,9 +2366,9 @@ function openMergedView(kind) {
|
|
|
2315
2366
|
modal.addEventListener('mousedown', function (e) { if (e.target === modal) modal.remove(); });
|
|
2316
2367
|
modal.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); modal.remove(); } });
|
|
2317
2368
|
document.body.appendChild(modal);
|
|
2318
|
-
// Focus the
|
|
2319
|
-
// async-restores focus to <body>, so retry briefly (same as the composer).
|
|
2320
|
-
var modalFocusTarget = area;
|
|
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;
|
|
2321
2372
|
var modalFocusTries = 0;
|
|
2322
2373
|
var tryFocusModal = function () {
|
|
2323
2374
|
if (!document.getElementById('mc-modal')) return true;
|
|
@@ -2850,8 +2901,10 @@ if (window.monacoriMenu && typeof window.monacoriMenu.onCloseTab === 'function')
|
|
|
2850
2901
|
// Capture so closing settings wins over other Escape handlers (lightbox / composer).
|
|
2851
2902
|
document.addEventListener('keydown', function (e) {
|
|
2852
2903
|
if (e.key === 'Escape' && !modal.classList.contains('hidden')) { e.stopPropagation(); e.preventDefault(); close(); return; }
|
|
2853
|
-
// Cmd/Ctrl+, (the standard "Preferences" accelerator) toggles the settings panel from anywhere
|
|
2904
|
+
// Cmd/Ctrl+, (the standard "Preferences" accelerator) toggles the settings panel from anywhere — but not
|
|
2905
|
+
// while another floating overlay (merged / memo) owns focus; that one must be Esc'd first.
|
|
2854
2906
|
if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && (e.key === ',' || e.code === 'Comma')) {
|
|
2907
|
+
if (modal.classList.contains('hidden') && (document.getElementById('mc-modal') || document.getElementById('mc-memo'))) return;
|
|
2855
2908
|
e.preventDefault(); e.stopPropagation();
|
|
2856
2909
|
if (modal.classList.contains('hidden')) open('general'); else close();
|
|
2857
2910
|
}
|
|
@@ -3009,6 +3062,10 @@ function applyDiffUpdate(u) {
|
|
|
3009
3062
|
var wasSource = isSourceViewerVisible();
|
|
3010
3063
|
var container = document.getElementById('diff2html-container');
|
|
3011
3064
|
var diffScrollTop = container ? container.scrollTop : 0;
|
|
3065
|
+
// Did the file the user is CURRENTLY viewing actually change in this build? If not, we must not re-render
|
|
3066
|
+
// the source view — an unrelated file's edit would otherwise flicker the pane they're reading. Capture the
|
|
3067
|
+
// open file's signature BEFORE fileSignatureByPath is rebuilt below.
|
|
3068
|
+
var prevOpenSig = openPath ? (fileSignatureByPath.get(openPath) || '') : '';
|
|
3012
3069
|
|
|
3013
3070
|
// 1) Replace the visible regions straight from the payload (no full-HTML parse).
|
|
3014
3071
|
if (container) container.innerHTML = u.diffContainer || '';
|
|
@@ -3027,6 +3084,9 @@ function applyDiffUpdate(u) {
|
|
|
3027
3084
|
// 2) Re-derive module-level state directly from the payload objects.
|
|
3028
3085
|
fileStates = u.fileStates || [];
|
|
3029
3086
|
fileSignatureByPath = new Map(fileStates.map(function (f) { return [f.path, f.signature]; }));
|
|
3087
|
+
// The open file changed iff its signature moved (or it vanished from the new build). Drives whether we
|
|
3088
|
+
// re-render the source view below.
|
|
3089
|
+
var openFileChanged = !openPath || prevOpenSig !== (fileSignatureByPath.get(openPath) || '');
|
|
3030
3090
|
sourceFiles = u.sourceFilesMeta || [];
|
|
3031
3091
|
sourceByPath = new Map(sourceFiles.map(function (f) { return [f.path, f]; }));
|
|
3032
3092
|
httpEnvironments = u.httpEnvironments || {};
|
|
@@ -3040,7 +3100,9 @@ function applyDiffUpdate(u) {
|
|
|
3040
3100
|
diffBootDone = false;
|
|
3041
3101
|
sourceLoaded = !REVIEW_LAZY_LOAD; // lazyLoad: re-fetch source content on next use
|
|
3042
3102
|
sourceLoading = false;
|
|
3043
|
-
|
|
3103
|
+
// Force a source body re-render on next open ONLY if the open file actually changed; otherwise keep
|
|
3104
|
+
// sourceBodyPath so the already-painted (unchanged) source view is left exactly as-is — no flicker.
|
|
3105
|
+
if (openFileChanged) sourceBodyPath = null;
|
|
3044
3106
|
symbolIndex = null;
|
|
3045
3107
|
if (REVIEW_LAZY) { setupLazyDiff(); setTimeout(function () { diffBootDone = true; }, 0); }
|
|
3046
3108
|
else { prepareDiff2HtmlHunks(); diffBootDone = true; }
|
|
@@ -3053,9 +3115,10 @@ function applyDiffUpdate(u) {
|
|
|
3053
3115
|
remapComments(); // follow/drop comments whose anchor line moved or vanished in the new build
|
|
3054
3116
|
refreshComments();
|
|
3055
3117
|
|
|
3056
|
-
// 5) Best-effort restore of what the user was looking at.
|
|
3118
|
+
// 5) Best-effort restore of what the user was looking at. Re-render the source view only when the open file
|
|
3119
|
+
// actually changed; an unchanged file stays painted as-is, so an unrelated edit doesn't flicker the pane.
|
|
3057
3120
|
if (wasSource && openPath && sourceByPath.has(openPath)) {
|
|
3058
|
-
openSourceFile(openPath, false);
|
|
3121
|
+
if (openFileChanged) openSourceFile(openPath, false);
|
|
3059
3122
|
} else if (container) {
|
|
3060
3123
|
showDiffView(false);
|
|
3061
3124
|
container.scrollTop = diffScrollTop;
|
|
@@ -3313,6 +3376,17 @@ function setSourceCursor(path, lineIndex, column, shouldReveal = false, targetLi
|
|
|
3313
3376
|
recordNav(navEntryOf('source'));
|
|
3314
3377
|
}
|
|
3315
3378
|
var sourceRevealRaf = 0, sourceRevealPrev = null;
|
|
3379
|
+
// Source rows are a fixed monospace height, so the caret-follow scroll can be computed from
|
|
3380
|
+
// lineIndex*rowHeight instead of reading the caret's getBoundingClientRect — which forces a full reflow on
|
|
3381
|
+
// every move (~15ms on a 400-line file; the main caret-follow stutter). Cached; invalidated on resize.
|
|
3382
|
+
var _srcRowH = 0;
|
|
3383
|
+
function sourceRowHeight() {
|
|
3384
|
+
if (_srcRowH > 0) return _srcRowH;
|
|
3385
|
+
var r = document.querySelector('#source-body .source-row');
|
|
3386
|
+
if (r) { var h = r.offsetHeight; if (h > 0) _srcRowH = h; }
|
|
3387
|
+
return _srcRowH;
|
|
3388
|
+
}
|
|
3389
|
+
if (typeof window !== 'undefined') window.addEventListener('resize', function () { _srcRowH = 0; });
|
|
3316
3390
|
function scheduleSourceReveal(prev) {
|
|
3317
3391
|
// First prev of a coalesced burst wins: a fast ArrowDown updates viewerCursor many times before the frame
|
|
3318
3392
|
// fires; render the caret once (first prev -> final viewerCursor) and scroll in the SAME frame so caret and
|
|
@@ -3326,8 +3400,23 @@ function scheduleSourceReveal(prev) {
|
|
|
3326
3400
|
if (!f || !f.embedded) return;
|
|
3327
3401
|
var lines = f.content.split(/\r?\n/);
|
|
3328
3402
|
updateSourceCaret(p, lines, f.language || 'text');
|
|
3329
|
-
var
|
|
3330
|
-
|
|
3403
|
+
var sb = document.getElementById('source-body');
|
|
3404
|
+
var rowH = sourceRowHeight();
|
|
3405
|
+
if (rowH > 0 && sb && !sb.classList.contains('rendered-body')) {
|
|
3406
|
+
// Scrolloff, not follow: scroll ONLY when the caret would otherwise leave the viewport, keeping it
|
|
3407
|
+
// within a 15% margin of the top/bottom edge. While the caret moves comfortably inside that band the
|
|
3408
|
+
// view stays put — continuous follow was dizzying (the file slid even when everything was visible) and
|
|
3409
|
+
// it forced a scroll/reflow on every move. lineIndex*rowH avoids getBoundingClientRect entirely, and
|
|
3410
|
+
// skipping the scroll when it's unnecessary removes the reflow on most moves too.
|
|
3411
|
+
var caretTop = viewerCursor.lineIndex * rowH;
|
|
3412
|
+
var ch = sb.clientHeight;
|
|
3413
|
+
var margin = Math.round(ch * 0.15);
|
|
3414
|
+
var vTop = sb.scrollTop;
|
|
3415
|
+
if (caretTop < vTop + margin) sb.scrollTop = Math.max(0, caretTop - margin);
|
|
3416
|
+
else if (caretTop + rowH > vTop + ch - margin) sb.scrollTop = caretTop + rowH - ch + margin;
|
|
3417
|
+
} else {
|
|
3418
|
+
revealAt(document.querySelector('.source-row.cursor-line'), sb, 0.85);
|
|
3419
|
+
}
|
|
3331
3420
|
});
|
|
3332
3421
|
}
|
|
3333
3422
|
|
|
@@ -3424,9 +3513,9 @@ function selectCommentRow(row) {
|
|
|
3424
3513
|
selectedCommentRow = row || null;
|
|
3425
3514
|
if (!selectedCommentRow) return;
|
|
3426
3515
|
selectedCommentRow.classList.add('mc-row-selected');
|
|
3427
|
-
//
|
|
3428
|
-
|
|
3429
|
-
|
|
3516
|
+
// Keep the caret visible: the box's active outline (.mc-row-selected) already shows the selection, and the
|
|
3517
|
+
// caret must never be hidden ("어떤 경우에도 커서는 가려지면 안 됨"). Previously this removed cursor-line +
|
|
3518
|
+
// code-cursor, so Go-to-comment → ArrowDown (which selects the comment box on that line) made the caret vanish.
|
|
3430
3519
|
}
|
|
3431
3520
|
function deleteCommentsInRow(row) {
|
|
3432
3521
|
if (!row) return;
|
|
@@ -3438,6 +3527,21 @@ function deleteCommentsInRow(row) {
|
|
|
3438
3527
|
}
|
|
3439
3528
|
refreshComments(); // remaining comment rows re-injected; the caret stays hidden until the next arrow press
|
|
3440
3529
|
}
|
|
3530
|
+
// Open the composer in EDIT mode for the first comment in `row`, pre-filled with its text. threadHtml renders
|
|
3531
|
+
// the composer in place of that card (via composerState.editSeq), and saveComposer routes editSeq through
|
|
3532
|
+
// updateComment instead of addComment. Triggered by `e` while a comment box is selected.
|
|
3533
|
+
function editCommentInRow(row) {
|
|
3534
|
+
if (!row) return;
|
|
3535
|
+
var del = row.querySelector('.mc-del');
|
|
3536
|
+
if (!del) return;
|
|
3537
|
+
var seq = parseInt(del.dataset.seq, 10);
|
|
3538
|
+
var c = reviewComments.find(function (x) { return x.seq === seq; });
|
|
3539
|
+
if (!c) return;
|
|
3540
|
+
row.classList.remove('mc-row-selected');
|
|
3541
|
+
selectedCommentRow = null;
|
|
3542
|
+
composerState = { kind: c.kind, path: c.path, line: c.line, code: c.code, editSeq: seq, editText: c.text };
|
|
3543
|
+
refreshComments();
|
|
3544
|
+
}
|
|
3441
3545
|
function handleSourceCaretKey(event) {
|
|
3442
3546
|
if (!viewerCursor) return false;
|
|
3443
3547
|
var ae = document.activeElement;
|
|
@@ -3446,6 +3550,7 @@ function handleSourceCaretKey(event) {
|
|
|
3446
3550
|
// A comment box is selected (caret hidden): Backspace/Delete removes it; an arrow steps off it.
|
|
3447
3551
|
if (selectedCommentRow) {
|
|
3448
3552
|
if (event.key === 'Backspace' || event.key === 'Delete') { event.preventDefault(); deleteCommentsInRow(selectedCommentRow); return true; }
|
|
3553
|
+
if (event.key === 'e' || event.key === 'E') { event.preventDefault(); editCommentInRow(selectedCommentRow); return true; }
|
|
3449
3554
|
if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'Escape') {
|
|
3450
3555
|
var dir = event.key === 'ArrowUp' ? -1 : (event.key === 'ArrowDown' ? 1 : 0);
|
|
3451
3556
|
var sib = dir < 0 ? selectedCommentRow.previousElementSibling : (dir > 0 ? selectedCommentRow.nextElementSibling : null);
|
|
@@ -4036,6 +4141,7 @@ function toggleRenderMode() {
|
|
|
4036
4141
|
var btn = document.getElementById('render-toggle');
|
|
4037
4142
|
if (btn) btn.addEventListener('click', function () { toggleRenderMode(); });
|
|
4038
4143
|
document.addEventListener('keydown', function (e) {
|
|
4144
|
+
if (isFloatingModalOpen()) return; // a floating overlay owns focus -> no render-toggle shortcut beneath it
|
|
4039
4145
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && !e.altKey && (e.key === 'M' || e.key === 'm' || e.code === 'KeyM')) {
|
|
4040
4146
|
var sv = document.getElementById('source-viewer');
|
|
4041
4147
|
var open = sv && sv.dataset.openPath;
|
package/dist/viewer.css
CHANGED
|
@@ -294,6 +294,10 @@ body {
|
|
|
294
294
|
border: 0;
|
|
295
295
|
border-radius: 0;
|
|
296
296
|
background: var(--panel);
|
|
297
|
+
/* #diff2html-container is a column flexbox; a flex item with overflow:hidden has min-height:0, so a single
|
|
298
|
+
shown file (showOnlyFile) shrinks to the viewport height and overflow:hidden clips the rest — the diff
|
|
299
|
+
can't scroll and the caret leaves the screen near the bottom of a big file. Pin to content height. */
|
|
300
|
+
flex-shrink: 0;
|
|
297
301
|
}
|
|
298
302
|
/* The per-file header is merged into the sticky toolbar (path + status + Viewed) to save vertical space. */
|
|
299
303
|
.d2h-file-header { display: none; }
|
|
@@ -370,6 +374,7 @@ body {
|
|
|
370
374
|
background: transparent;
|
|
371
375
|
border: 0;
|
|
372
376
|
}
|
|
377
|
+
.d2h-code-line-ctn { position: relative; } /* anchors the absolutely-positioned empty-line caret (so blank rows need no inline position) */
|
|
373
378
|
.d2h-code-side-line, .d2h-code-line {
|
|
374
379
|
/* left pad must exceed the 58px absolutely-positioned line-number, else the +/- prefix renders behind it and looks clipped */
|
|
375
380
|
padding: 0 0.6em 0 64px;
|
|
@@ -689,11 +694,6 @@ h1 { margin: 0; font-size: 18px; }
|
|
|
689
694
|
key scrolls the view CONTINUOUSLY instead of leaving it still until the caret reaches the viewport edge
|
|
690
695
|
(the "stutter every ~viewport" the user reported). Applies to the source body, the diff, and the sidebar. */
|
|
691
696
|
.source-body, #diff2html-container, .sidebar-scroll { scroll-padding-block: 35vh; }
|
|
692
|
-
/* revealAt() sets scrollTop directly, and scroll-padding only affects scrollIntoView — so near EOF the
|
|
693
|
-
caret can't reach the 42% line and pins to the viewport bottom, where the footer progress bar overlaps
|
|
694
|
-
and HIDES it ("the caret leaves the screen at the end of a file"). Real trailing space lets the last
|
|
695
|
-
lines scroll up to the middle. The diff caret showed this worst (95% vs source's 81%); both get it. */
|
|
696
|
-
.source-body, #diff2html-container { padding-bottom: 45vh; }
|
|
697
697
|
.source-body {
|
|
698
698
|
border: 1px solid var(--border);
|
|
699
699
|
overflow: auto;
|
|
@@ -750,8 +750,11 @@ h1 { margin: 0; font-size: 18px; }
|
|
|
750
750
|
}
|
|
751
751
|
/* perf: let the browser skip layout/paint for off-screen rows in large files/diffs.
|
|
752
752
|
DOM is unchanged (nav, search, comment anchoring still query every row); degrades
|
|
753
|
-
gracefully where unsupported. contain-intrinsic-size keeps the scrollbar stable.
|
|
754
|
-
.source-row
|
|
753
|
+
gracefully where unsupported. contain-intrinsic-size keeps the scrollbar stable.
|
|
754
|
+
(Removing this from .source-row made the caret stutter WORSE — the full DOM forces a bigger reflow on
|
|
755
|
+
every scroll. The stutter is fixed instead by computing the source scroll from lineIndex*rowHeight,
|
|
756
|
+
which skips getBoundingClientRect's forced reflow entirely; see scheduleSourceReveal.) */
|
|
757
|
+
.source-row { content-visibility: auto; contain-intrinsic-size: auto 21px; }
|
|
755
758
|
.d2h-diff-table tr { content-visibility: auto; contain-intrinsic-size: auto 18px; }
|
|
756
759
|
/* Comment/composer rows are tall and interactive (a textarea lives here). Skip-rendering them
|
|
757
760
|
with a tiny 18px placeholder made the browser re-evaluate their render state on every
|
|
@@ -834,6 +837,11 @@ body.mc-composing .mc-diff-cursor-row .d2h-code-side-line { box-shadow: none; }
|
|
|
834
837
|
body.mc-composing .source-row.cursor-line .md-cell,
|
|
835
838
|
body.mc-composing .source-row.csv-row.cursor-line .csv-cell { background: transparent; }
|
|
836
839
|
body.mc-composing .source-row.cursor-line .num { color: inherit; }
|
|
840
|
+
/* Same single-caret rule for a floating overlay (merged comments / prompt memo / settings): it owns the only
|
|
841
|
+
caret while open, so hide the file's blinking caret behind it — never two carets across visible panels. */
|
|
842
|
+
body:has(#mc-modal) .code-cursor,
|
|
843
|
+
body:has(#mc-memo) .code-cursor,
|
|
844
|
+
body:has(#settings-modal:not(.hidden)) .code-cursor { display: none; }
|
|
837
845
|
.mc-kind {
|
|
838
846
|
font-weight: 700; font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase;
|
|
839
847
|
padding: 2px 8px; border-radius: 999px;
|