@happy-nut/monacori 0.1.22 → 0.1.25

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.
@@ -1100,6 +1100,11 @@ function handleTreeKey(event) {
1100
1100
  if (event.key === 'ArrowUp') { event.preventDefault(); focusTree(treeFocusIndex - 1); return true; }
1101
1101
  if (event.key === 'PageDown') { event.preventDefault(); focusTree(treeFocusIndex + treePageSize()); return true; }
1102
1102
  if (event.key === 'PageUp') { event.preventDefault(); focusTree(treeFocusIndex - treePageSize()); return true; }
1103
+ if (event.key === 'Enter' && event.altKey) {
1104
+ event.preventDefault();
1105
+ if (row && typeof openTreeRowMenu === 'function') openTreeRowMenu(row); // copy path / Finder / terminal
1106
+ return true;
1107
+ }
1103
1108
  if (event.key === 'Enter') {
1104
1109
  event.preventDefault();
1105
1110
  if (row && row.classList.contains('file-link')) { row.click(); clearTreeFocus(); }
@@ -1137,6 +1142,9 @@ function handleTreeKey(event) {
1137
1142
  function isFloatingModalOpen() {
1138
1143
  var sm = document.getElementById('settings-modal');
1139
1144
  if (sm && !sm.classList.contains('hidden')) return true;
1145
+ var hv = document.getElementById('history-view');
1146
+ if (hv && !hv.classList.contains('hidden')) return true; // history overlay owns the keys (Esc/filter/click)
1147
+ if (document.getElementById('goto-line')) return true; // go-to-line prompt owns the keys until Enter/Esc
1140
1148
  // The merged/memo panels are now docked (inline), not overlays — but while one OWNS focus we still stand
1141
1149
  // down the global nav shortcuts so typing / ▲▼ inside it isn't hijacked. Focus elsewhere -> shortcuts run.
1142
1150
  return isDockFocused();
@@ -1170,11 +1178,27 @@ document.addEventListener('keydown', (event) => {
1170
1178
  openMemoView();
1171
1179
  return;
1172
1180
  }
1181
+ // Cmd/Ctrl+9 toggles the git history view (above the focus guard so a 2nd press closes it from inside).
1182
+ if (!settingsUp && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.code === 'Digit9' || event.key === '9') && typeof toggleHistory === 'function') {
1183
+ event.preventDefault();
1184
+ toggleHistory();
1185
+ return;
1186
+ }
1173
1187
 
1174
1188
  // Settings overlay (or a focused merged/memo dock) captures keys: stand down the rest of the global
1175
1189
  // shortcuts (Cmd+1, F7, Cmd+[/], Cmd+B, …). Each has its own Esc + editing handlers.
1176
1190
  if (isFloatingModalOpen()) return;
1177
1191
 
1192
+ // Cmd/Ctrl+A in the diff/source view selects ONLY that view's content (the browser default reached into
1193
+ // the sidebar + terminal). In an editable field, let the default select-within-field stand.
1194
+ if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'a' || event.key === 'A')) {
1195
+ var aae = document.activeElement;
1196
+ if (!(aae && (aae.tagName === 'INPUT' || aae.tagName === 'TEXTAREA' || aae.tagName === 'SELECT')) && selectAllInView()) {
1197
+ event.preventDefault();
1198
+ return;
1199
+ }
1200
+ }
1201
+
1178
1202
  if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '1') {
1179
1203
  event.preventDefault();
1180
1204
  // Coming from the diff: open the file you were viewing as source so Cmd+1 lands ON it (not a stale/blank
@@ -1190,6 +1214,24 @@ document.addEventListener('keydown', (event) => {
1190
1214
  focusOpenFileInTree();
1191
1215
  return;
1192
1216
  }
1217
+ // Cmd/Ctrl+L = go to line (numeric prompt); Cmd/Ctrl+K = copy the caret's file:line. Skip when an
1218
+ // editable field owns focus (a comment composer textarea) so we don't hijack the user's typing.
1219
+ if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'l' || event.key === 'L')) {
1220
+ var lkae = document.activeElement;
1221
+ if (!(lkae && (lkae.tagName === 'INPUT' || lkae.tagName === 'TEXTAREA' || lkae.tagName === 'SELECT'))) {
1222
+ event.preventDefault();
1223
+ openGotoLine();
1224
+ return;
1225
+ }
1226
+ }
1227
+ if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && (event.key === 'k' || event.key === 'K')) {
1228
+ var kkae = document.activeElement;
1229
+ if (!(kkae && (kkae.tagName === 'INPUT' || kkae.tagName === 'TEXTAREA' || kkae.tagName === 'SELECT'))) {
1230
+ event.preventDefault();
1231
+ copyCaretLocation();
1232
+ return;
1233
+ }
1234
+ }
1193
1235
  if ((event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key === '0') {
1194
1236
  event.preventDefault();
1195
1237
  setTab('changes');
@@ -1483,6 +1525,7 @@ document.querySelector('.activity-rail')?.addEventListener('click', (event) => {
1483
1525
  else if (view === 'files') { setTab('files'); }
1484
1526
  else if (view === 'q' || view === 'c') { toggleMergedRail(view); }
1485
1527
  else if (view === 'memo') { openMemoView(); } // openMemoView already toggles
1528
+ else if (view === 'history') { toggleHistory(); }
1486
1529
  syncRail();
1487
1530
  });
1488
1531
 
@@ -2834,6 +2877,10 @@ refreshComments();
2834
2877
  // Exception: keep focus for clipboard/selection combos (Cmd+C/V/X/A) so the terminal's own copy &
2835
2878
  // paste keep working — blurring on Cmd+V drops the textarea focus the paste event needs.
2836
2879
  term.attachCustomKeyEventHandler(function (e) {
2880
+ // F7 / Shift+F7 are diff prev/next-change nav. Don't let the terminal eat them (it would send an
2881
+ // escape sequence to the shell); return false so xterm ignores the key and it bubbles to the document
2882
+ // handler. We DON'T blur — the diff caret is a JS cursor, so nav runs while the terminal keeps focus.
2883
+ if (e.type === 'keydown' && e.key === 'F7' && !e.metaKey && !e.ctrlKey && !e.altKey) return false;
2837
2884
  if (e.type === 'keydown' && e.metaKey) {
2838
2885
  var k = (e.key || '').toLowerCase();
2839
2886
  // The bare modifier press (Cmd goes down BEFORE the letter on macOS) must not blur — blurring
@@ -2846,6 +2893,10 @@ refreshComments();
2846
2893
  // copy misses xterm's own selection, so Cmd+C silently did nothing. No selection -> fall through.
2847
2894
  if (e.code === 'KeyC' && term.hasSelection && term.hasSelection()) { copyToClipboard(term.getSelection()); return false; }
2848
2895
  if (e.code === 'KeyC' || e.code === 'KeyV' || e.code === 'KeyX' || e.code === 'KeyA') return true;
2896
+ // Cmd/Ctrl+W is the close-pane menu accelerator. onCloseTab closes the FOCUSED pane only if the
2897
+ // terminal still has focus — blurring here first made hasFocus() false, so the focused split pane
2898
+ // never closed. Release the key WITHOUT blurring so focus stays and onCloseTab can close it.
2899
+ if (e.code === 'KeyW') return false;
2849
2900
  try { term.blur(); } catch (x) {}
2850
2901
  return false;
2851
2902
  }
@@ -3266,6 +3317,8 @@ function syncRail() {
3266
3317
  setOn('q', !!(merged && merged.dataset.kind === 'q'));
3267
3318
  setOn('c', !!(merged && merged.dataset.kind === 'c'));
3268
3319
  setOn('memo', !!document.getElementById('mc-memo-panel'));
3320
+ var hv = document.getElementById('history-view');
3321
+ setOn('history', !!(hv && !hv.classList.contains('hidden')));
3269
3322
  }
3270
3323
  // Rail click for the merged views toggles: a 2nd click on the open kind closes it (memo already toggles).
3271
3324
  function toggleMergedRail(kind) {
@@ -3348,15 +3401,21 @@ function restoreUiState() {
3348
3401
  }
3349
3402
  return true;
3350
3403
  }
3351
- if (state.sourcePath && sourceByPath.has(state.sourcePath)) {
3352
- openSourceFile(state.sourcePath);
3404
+ // Source view. Open the saved file — or fall back to the first restored tab when that file is gone
3405
+ // (filtered out above) or wasn't recorded. Otherwise we'd render the tab bar but leave the body on its
3406
+ // "select a file" placeholder, which looks broken (a tab is clearly open). No openable tab → drop the
3407
+ // stale tabs and let the init fallback pick a sensible default.
3408
+ var openPath = (state.sourcePath && sourceByPath.has(state.sourcePath)) ? state.sourcePath : (sourceTabs[0] || '');
3409
+ if (openPath) {
3410
+ openSourceFile(openPath);
3353
3411
  // Restore the exact source caret/scroll (openSourceFile alone resets it to the top).
3354
- if (state.viewerCursor && state.viewerCursor.path === state.sourcePath) {
3412
+ if (state.viewerCursor && state.viewerCursor.path === openPath) {
3355
3413
  var vc = state.viewerCursor;
3356
- setTimeout(function () { try { setSourceCursor(state.sourcePath, vc.lineIndex, vc.column, true, -1); } catch (e) {} }, 60);
3414
+ setTimeout(function () { try { setSourceCursor(openPath, vc.lineIndex, vc.column, true, -1); } catch (e) {} }, 60);
3357
3415
  }
3358
3416
  return true;
3359
3417
  }
3418
+ sourceTabs = [];
3360
3419
  } catch {
3361
3420
  sessionStorage.removeItem(uiStateKey);
3362
3421
  }
@@ -3377,6 +3436,45 @@ function flushPendingDiffUpdate() {
3377
3436
  pendingDiffUpdate = null;
3378
3437
  try { applyDiffUpdate(u); } catch (e) {}
3379
3438
  }
3439
+ // Flicker-saver for the live-watch refresh. The default path replaces the WHOLE diff DOM
3440
+ // (container.innerHTML = …), which re-renders the file you're looking at even when only an OFF-SCREEN file
3441
+ // changed. When the file set AND order are identical, reconcile per-file instead: keep every unchanged
3442
+ // wrapper's DOM node untouched (no flicker — including the visible one) and swap only the changed wrappers,
3443
+ // which are off-screen so the swap is invisible. Returns false (caller does the full innerHTML swap) for the
3444
+ // risky cases — files added/removed/reordered — so that proven path still handles index-shift correctly.
3445
+ function reconcileDiffWrappers(container, newDiffHtml, oldSigByPath, newSigByPath) {
3446
+ var oldW = Array.prototype.slice.call(container.querySelectorAll('.d2h-file-wrapper'));
3447
+ if (!oldW.length) return false;
3448
+ var tmp = document.createElement('div');
3449
+ tmp.innerHTML = newDiffHtml;
3450
+ var newW = Array.prototype.slice.call(tmp.querySelectorAll('.d2h-file-wrapper'));
3451
+ if (newW.length !== oldW.length) return false; // add/remove → full swap (global hunk indices shift)
3452
+ for (var i = 0; i < oldW.length; i++) {
3453
+ if (diffWrapperPathKey(oldW[i]) !== diffWrapperPathKey(newW[i])) return false; // reordered → full swap
3454
+ }
3455
+ for (var j = 0; j < oldW.length; j++) {
3456
+ var ow = oldW[j], nw = newW[j], p = diffWrapperPathKey(ow);
3457
+ if ((oldSigByPath.get(p) || '') !== (newSigByPath.get(p) || '')) {
3458
+ // Changed file (off-screen): drop in the fresh shell and clear its cached body so it refetches the
3459
+ // new content when it next scrolls into view. Replacing an off-screen node is invisible.
3460
+ var nidx = (nw.id || '').replace('file-', '');
3461
+ delete bodyCache[nidx];
3462
+ delete bodyPromise[nidx];
3463
+ ow.parentNode.replaceChild(nw, ow);
3464
+ } else {
3465
+ // Unchanged file: keep its DOM node (no flicker). An earlier file's changed hunk count can shift the
3466
+ // global numbering, so sync the index attrs and renumber a materialized body's hunk ids (id/class
3467
+ // changes only — invisible, no flicker).
3468
+ var baseChanged = ow.getAttribute('data-first-hunk') !== nw.getAttribute('data-first-hunk');
3469
+ ow.id = nw.id;
3470
+ if (nw.hasAttribute('data-first-hunk')) ow.setAttribute('data-first-hunk', nw.getAttribute('data-first-hunk'));
3471
+ if (nw.hasAttribute('data-hunk-count')) ow.setAttribute('data-hunk-count', nw.getAttribute('data-hunk-count'));
3472
+ var body = ow.querySelector('.d2h-files-diff');
3473
+ if (baseChanged && body && !body.hasAttribute('data-lazy')) markWrapperHunks(ow);
3474
+ }
3475
+ }
3476
+ return true;
3477
+ }
3380
3478
  function applyDiffUpdate(u) {
3381
3479
  if (!u || !u.signature || u.signature === currentSignature) return false; // unchanged — nothing to do
3382
3480
  if (composerState) { pendingDiffUpdate = u; return false; } // composing a comment — hold the refresh until close/save
@@ -3396,11 +3494,19 @@ function applyDiffUpdate(u) {
3396
3494
  // open file's signature BEFORE fileSignatureByPath is rebuilt below.
3397
3495
  var prevOpenSig = openPath ? (fileSignatureByPath.get(openPath) || '') : '';
3398
3496
 
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.
3497
+ // Fast-path: when the file set + order are unchanged, reconcile per-file so an off-screen change never
3498
+ // flickers the file you're viewing. Falls back to the full swap (below) for add/remove/reorder or eager.
3499
+ var newSigByPath = new Map((u.fileStates || []).map(function (f) { return [f.path, f.signature]; }));
3500
+ var fastPath = false;
3501
+ if (REVIEW_LAZY && container && u.diffContainer) {
3502
+ fastPath = reconcileDiffWrappers(container, u.diffContainer, fileSignatureByPath, newSigByPath);
3503
+ }
3504
+
3505
+ // Full-swap path only: snapshot already-materialized bodies (keyed by path + signature) BEFORE the swap so
3506
+ // UNCHANGED files re-fill synchronously afterwards — otherwise the swap blanks every wrapper into an empty
3507
+ // lazy shell until its body reloads over IPC (the "flicker"). The fast-path keeps those nodes, so skip it.
3402
3508
  var prevBodies = {};
3403
- if (REVIEW_LAZY && container) {
3509
+ if (!fastPath && REVIEW_LAZY && container) {
3404
3510
  container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
3405
3511
  var b = w.querySelector('.d2h-files-diff');
3406
3512
  if (!b || b.hasAttribute('data-lazy')) return; // only bodies that are actually materialized
@@ -3409,8 +3515,9 @@ function applyDiffUpdate(u) {
3409
3515
  });
3410
3516
  }
3411
3517
 
3412
- // 1) Replace the visible regions straight from the payload (no full-HTML parse).
3413
- if (container) container.innerHTML = u.diffContainer || '';
3518
+ // 1) Replace the visible regions straight from the payload (no full-HTML parse) — unless the fast-path
3519
+ // already reconciled the diff DOM in place.
3520
+ if (container && !fastPath) container.innerHTML = u.diffContainer || '';
3414
3521
  var changesPanel = document.getElementById('changes-panel');
3415
3522
  if (changesPanel) changesPanel.innerHTML = u.changesPanel || '';
3416
3523
  // Files tree: keep the inert island (lazy, not yet opened) in sync, and refresh the live panel when it's
@@ -3472,7 +3579,7 @@ function applyDiffUpdate(u) {
3472
3579
  // flicker). Runs BEFORE setupLazyDiff so the IntersectionObserver sees them already materialized and never
3473
3580
  // re-fetches them. The fresh wrapper carries the correct data-first-hunk + file index, so materializeBody
3474
3581
  // numbers hunks exactly as a normal lazy load would. Changed/new files stay shells and lazy-load as usual.
3475
- if (REVIEW_LAZY && container) {
3582
+ if (!fastPath && REVIEW_LAZY && container) {
3476
3583
  container.querySelectorAll('.d2h-file-wrapper').forEach(function (w) {
3477
3584
  var p = diffWrapperPathKey(w);
3478
3585
  var prev = p ? prevBodies[p] : null;
@@ -3859,6 +3966,26 @@ function isSourceViewerVisible() {
3859
3966
  return Boolean(viewer && !viewer.classList.contains('hidden'));
3860
3967
  }
3861
3968
 
3969
+ // Cmd/Ctrl+A scoped to the current view: select the source body, or the active diff file's content —
3970
+ // NOT the whole page (the browser default reached into the sidebar/terminal). Returns false if there's
3971
+ // no view target so the caller can fall back to the default.
3972
+ function selectAllInView() {
3973
+ var target = null;
3974
+ if (isSourceViewerVisible()) target = document.getElementById('source-body');
3975
+ else if (typeof isDiffViewVisible === 'function' && isDiffViewVisible()) {
3976
+ target = document.querySelector('#diff2html-container .d2h-file-wrapper:not(.df-inactive)') || document.getElementById('diff2html-container');
3977
+ }
3978
+ if (!target) return false;
3979
+ try {
3980
+ var sel = window.getSelection();
3981
+ var range = document.createRange();
3982
+ range.selectNodeContents(target);
3983
+ sel.removeAllRanges();
3984
+ sel.addRange(range);
3985
+ } catch (e) { return false; }
3986
+ return true;
3987
+ }
3988
+
3862
3989
  function openDiffFileAtCaret() {
3863
3990
  if (diffCursor && isDiffViewVisible()) {
3864
3991
  const dwrap = diffWrapperByPath(diffCursor.path);
@@ -5175,3 +5302,328 @@ function formatBytes(bytes) {
5175
5302
  if (kib < 1024) return kib.toFixed(1) + ' KiB';
5176
5303
  return (kib / 1024).toFixed(1) + ' MiB';
5177
5304
  }
5305
+ // ===== Git history view (Cmd+9): commit list with graph lanes + per-commit diff. =====
5306
+ // Data comes from the main process (window.monacoriGit.log / .commitDiff); the lane layout is computed
5307
+ // here from each commit's parents. Read-only — the per-commit diff is static diff2html HTML.
5308
+
5309
+ var HISTORY_LANE_W = 14, HISTORY_DOT_R = 3.5, HISTORY_ROW_H = 24;
5310
+ var HISTORY_COLORS = ['#6c9fd4', '#7faf6b', '#d4a857', '#c77dd4', '#d36c6c', '#5bb6b6', '#b0884f', '#8d8df0'];
5311
+ var historyCommits = [];
5312
+ var historyGraph = [];
5313
+ var historyMaxLane = 0;
5314
+ var historyActiveSha = '';
5315
+ var historyLoading = false;
5316
+
5317
+ // Lane layout. Walks commits newest-first, tracking open edges (lanes) by the hash each expects next.
5318
+ // Returns per-row { hash, myLane, color, topEdges, bottomEdges } using LANE INDICES + COLOR INDICES (px-free,
5319
+ // so it's unit-testable). First parent inherits the commit's color so a branch keeps one hue down its line.
5320
+ function computeHistoryGraph(commits) {
5321
+ var lanes = []; // lane index -> hash the lane is waiting to reach (open edge from above)
5322
+ var colorOf = {}; // hash -> color index
5323
+ var next = 0;
5324
+ function colorFor(h) { if (colorOf[h] == null) colorOf[h] = next++; return colorOf[h]; }
5325
+ function freeLane() { for (var i = 0; i < lanes.length; i++) if (lanes[i] == null) return i; lanes.push(null); return lanes.length - 1; }
5326
+ var rows = [];
5327
+ var maxLane = 0;
5328
+ for (var ci = 0; ci < commits.length; ci++) {
5329
+ var c = commits[ci];
5330
+ var incoming = lanes.slice();
5331
+ var myLane = lanes.indexOf(c.hash);
5332
+ if (myLane === -1) myLane = freeLane();
5333
+ var myColor = colorFor(c.hash);
5334
+ lanes[myLane] = c.hash;
5335
+ for (var i = 0; i < lanes.length; i++) if (i !== myLane && lanes[i] === c.hash) lanes[i] = null; // merge other edges in
5336
+ var parents = c.parents || [];
5337
+ var parentLanes = {};
5338
+ if (parents.length === 0) {
5339
+ lanes[myLane] = null; // root commit — the lane ends here
5340
+ } else {
5341
+ lanes[myLane] = parents[0];
5342
+ if (colorOf[parents[0]] == null) colorOf[parents[0]] = myColor; // first parent keeps the hue
5343
+ parentLanes[myLane] = true;
5344
+ for (var p = 1; p < parents.length; p++) {
5345
+ var ex = lanes.indexOf(parents[p]);
5346
+ var l = ex !== -1 ? ex : freeLane();
5347
+ lanes[l] = parents[p];
5348
+ colorFor(parents[p]);
5349
+ parentLanes[l] = true;
5350
+ }
5351
+ }
5352
+ var outgoing = lanes.slice();
5353
+ var topEdges = [];
5354
+ for (var a = 0; a < incoming.length; a++) {
5355
+ if (incoming[a] == null) continue;
5356
+ topEdges.push({ from: a, to: incoming[a] === c.hash ? myLane : a, color: colorOf[incoming[a]] });
5357
+ }
5358
+ var bottomEdges = [];
5359
+ for (var b = 0; b < outgoing.length; b++) {
5360
+ if (outgoing[b] == null) continue;
5361
+ bottomEdges.push({ from: parentLanes[b] ? myLane : b, to: b, color: colorOf[outgoing[b]] });
5362
+ }
5363
+ for (var m = 0; m < Math.max(incoming.length, outgoing.length); m++) {
5364
+ if (incoming[m] != null || outgoing[m] != null) maxLane = Math.max(maxLane, m);
5365
+ }
5366
+ maxLane = Math.max(maxLane, myLane);
5367
+ rows.push({ hash: c.hash, myLane: myLane, color: myColor, topEdges: topEdges, bottomEdges: bottomEdges });
5368
+ }
5369
+ rows.maxLane = maxLane;
5370
+ return rows;
5371
+ }
5372
+ if (typeof window !== 'undefined') window.computeHistoryGraph = computeHistoryGraph; // exposed for tests
5373
+
5374
+ function historyLaneX(l) { return 9 + l * HISTORY_LANE_W; }
5375
+ function historyColor(i) { return HISTORY_COLORS[i % HISTORY_COLORS.length]; }
5376
+ function historyRowSvg(row) {
5377
+ var w = historyLaneX(historyMaxLane) + 9, h = HISTORY_ROW_H, mid = h / 2;
5378
+ var s = '<svg class="hgraph" width="' + w + '" height="' + h + '" viewBox="0 0 ' + w + ' ' + h + '" aria-hidden="true">';
5379
+ var edge = function (e, y1, y2) {
5380
+ var x1 = historyLaneX(e.from), x2 = historyLaneX(e.to);
5381
+ var c1 = (y1 + y2) / 2;
5382
+ return '<path d="M' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + c1 + ', ' + x2 + ' ' + c1 + ', ' + x2 + ' ' + y2 + '" stroke="' + historyColor(e.color) + '" fill="none" stroke-width="1.6"/>';
5383
+ };
5384
+ row.topEdges.forEach(function (e) { s += edge(e, 0, mid); });
5385
+ row.bottomEdges.forEach(function (e) { s += edge(e, mid, h); });
5386
+ s += '<circle cx="' + historyLaneX(row.myLane) + '" cy="' + mid + '" r="' + HISTORY_DOT_R + '" fill="' + historyColor(row.color) + '"/></svg>';
5387
+ return s;
5388
+ }
5389
+
5390
+ // "HEAD -> main, origin/main, tag: v1" -> small badges (HEAD/branch/tag styled distinctly).
5391
+ function historyRefBadges(refs) {
5392
+ if (!refs || !refs.trim()) return '';
5393
+ return refs.split(',').map(function (r) {
5394
+ r = r.trim();
5395
+ if (!r) return '';
5396
+ var cls = 'href-branch', label = r;
5397
+ if (r.indexOf('tag:') === 0) { cls = 'href-tag'; label = r.replace('tag:', '').trim(); }
5398
+ else if (r.indexOf('HEAD') === 0) { cls = 'href-head'; }
5399
+ else if (r.indexOf('origin/') === 0 || r.indexOf('/') !== -1) { cls = 'href-remote'; }
5400
+ return '<span class="href ' + cls + '">' + escapeHtml(label) + '</span>';
5401
+ }).join('');
5402
+ }
5403
+
5404
+ function historyShortDate(iso) {
5405
+ if (!iso) return '';
5406
+ // 2026-06-20T21:03:11+09:00 -> "2026-06-20 21:03"
5407
+ var m = String(iso).match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})/);
5408
+ return m ? m[1] + ' ' + m[2] : String(iso).slice(0, 16);
5409
+ }
5410
+
5411
+ function renderHistoryList() {
5412
+ var list = document.getElementById('history-list');
5413
+ if (!list) return;
5414
+ if (!historyCommits.length) {
5415
+ list.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t(historyLoading ? 'history.loading' : 'history.empty')) + '</div>';
5416
+ return;
5417
+ }
5418
+ list.style.setProperty('--hgraph-w', (historyLaneX(historyMaxLane) + 9) + 'px');
5419
+ list.innerHTML = historyCommits.map(function (c, i) {
5420
+ return '<button type="button" class="hrow' + (c.hash === historyActiveSha ? ' active' : '') + '" data-sha="' + escapeHtml(c.hash) + '">'
5421
+ + '<span class="hgraph-cell">' + historyRowSvg(historyGraph[i]) + '</span>'
5422
+ + '<span class="hmsg">' + historyRefBadges(c.refs) + escapeHtml(c.subject) + '</span>'
5423
+ + '<span class="hauthor">' + escapeHtml(c.author) + '</span>'
5424
+ + '<span class="hdate">' + escapeHtml(historyShortDate(c.date)) + '</span>'
5425
+ + '</button>';
5426
+ }).join('');
5427
+ }
5428
+
5429
+ // Text filter (subject / author). The graph only reads right on the full contiguous history, so filtering
5430
+ // hides the graph column (IntelliJ does the same) and just shows matching rows.
5431
+ function applyHistoryFilter() {
5432
+ var input = document.getElementById('history-search');
5433
+ var list = document.getElementById('history-list');
5434
+ if (!list) return;
5435
+ var q = (input && input.value || '').trim().toLowerCase();
5436
+ list.classList.toggle('filtering', q.length > 0);
5437
+ var rows = list.querySelectorAll('.hrow');
5438
+ for (var i = 0; i < rows.length; i++) {
5439
+ var c = historyCommits[i];
5440
+ var hit = !q || (c.subject + '\n' + c.author + '\n' + c.hash).toLowerCase().indexOf(q) !== -1;
5441
+ rows[i].classList.toggle('hidden', !hit);
5442
+ }
5443
+ }
5444
+
5445
+ function openHistoryCommit(sha) {
5446
+ if (!sha || !window.monacoriGit) return;
5447
+ historyActiveSha = sha;
5448
+ var list = document.getElementById('history-list');
5449
+ if (list) list.querySelectorAll('.hrow').forEach(function (r) { r.classList.toggle('active', r.dataset.sha === sha); });
5450
+ var detail = document.getElementById('history-detail');
5451
+ if (detail) detail.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('history.loading')) + '</div>';
5452
+ Promise.resolve(window.monacoriGit.commitDiff(sha)).then(function (d) {
5453
+ if (!d || historyActiveSha !== sha) return; // selection moved on while loading
5454
+ renderHistoryDetail(d);
5455
+ }, function () {});
5456
+ }
5457
+
5458
+ function renderHistoryDetail(d) {
5459
+ var detail = document.getElementById('history-detail');
5460
+ if (!detail) return;
5461
+ var head = '<div class="history-detail-head">'
5462
+ + '<div class="hd-msg">' + escapeHtml(d.message || '').replace(/\n/g, '<br>') + '</div>'
5463
+ + '<div class="hd-meta"><span class="hd-hash">' + escapeHtml((d.hash || '').slice(0, 10)) + '</span>'
5464
+ + '<span class="hd-author">' + escapeHtml(d.author) + (d.email ? ' &lt;' + escapeHtml(d.email) + '&gt;' : '') + '</span>'
5465
+ + '<span class="hd-date">' + escapeHtml(historyShortDate(d.date)) + '</span>'
5466
+ + historyRefBadges(d.refs) + '</div></div>';
5467
+ var body = (d.diffHtml && d.diffHtml.trim())
5468
+ ? '<div class="history-diff diff2html-container">' + d.diffHtml + '</div>'
5469
+ : '<div class="quick-open-empty">' + escapeHtml(t(d.isMerge ? 'history.merge' : 'history.noDiff')) + '</div>';
5470
+ detail.innerHTML = head + body;
5471
+ }
5472
+
5473
+ function isHistoryOpen() {
5474
+ var v = document.getElementById('history-view');
5475
+ return !!(v && !v.classList.contains('hidden'));
5476
+ }
5477
+ function closeHistory() {
5478
+ var v = document.getElementById('history-view');
5479
+ if (v) v.classList.add('hidden');
5480
+ if (typeof syncRail === 'function') syncRail();
5481
+ }
5482
+ function openHistory() {
5483
+ var v = document.getElementById('history-view');
5484
+ if (!v) return;
5485
+ if (!window.monacoriGit) return; // browser/serve mode: no git bridge
5486
+ v.classList.remove('hidden');
5487
+ if (typeof syncRail === 'function') syncRail();
5488
+ var search = document.getElementById('history-search');
5489
+ if (search) { search.value = ''; }
5490
+ applyHistoryFilter();
5491
+ historyLoading = true;
5492
+ renderHistoryList();
5493
+ Promise.resolve(window.monacoriGit.log({ limit: 300 })).then(function (commits) {
5494
+ historyLoading = false;
5495
+ historyCommits = Array.isArray(commits) ? commits : [];
5496
+ historyGraph = computeHistoryGraph(historyCommits);
5497
+ historyMaxLane = historyGraph.maxLane || 0;
5498
+ renderHistoryList();
5499
+ var detail = document.getElementById('history-detail');
5500
+ if (detail) detail.innerHTML = '<div class="quick-open-empty">' + escapeHtml(t('history.selectCommit')) + '</div>';
5501
+ if (historyCommits[0]) openHistoryCommit(historyCommits[0].hash); // preview the newest commit
5502
+ if (search) setTimeout(function () { try { search.focus(); } catch (e) {} }, 0);
5503
+ }, function () { historyLoading = false; renderHistoryList(); });
5504
+ }
5505
+ function toggleHistory() { if (isHistoryOpen()) closeHistory(); else openHistory(); }
5506
+ if (typeof window !== 'undefined') window.__monacoriHistory = { open: openHistory, close: closeHistory, toggle: toggleHistory, isOpen: isHistoryOpen };
5507
+
5508
+ (function wireHistory() {
5509
+ var list = document.getElementById('history-list');
5510
+ if (list) list.addEventListener('click', function (e) {
5511
+ var row = e.target.closest && e.target.closest('.hrow[data-sha]');
5512
+ if (row) openHistoryCommit(row.dataset.sha);
5513
+ });
5514
+ var search = document.getElementById('history-search');
5515
+ if (search) search.addEventListener('input', applyHistoryFilter);
5516
+ var closeBtn = document.getElementById('history-close');
5517
+ if (closeBtn) closeBtn.addEventListener('click', closeHistory);
5518
+ var view = document.getElementById('history-view');
5519
+ if (view) view.addEventListener('keydown', function (e) {
5520
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeHistory(); }
5521
+ });
5522
+ })();
5523
+ // ===== Go-to-line (Cmd/Ctrl+L), copy caret location (Cmd/Ctrl+K), and the sidebar row action menu. =====
5524
+
5525
+ // Programmatic clipboard write. Electron's bridge is reliable on file://; navigator.clipboard is the fallback.
5526
+ function copyTextToClipboard(text) {
5527
+ try { if (window.monacoriClipboard && typeof window.monacoriClipboard.write === 'function') { window.monacoriClipboard.write(text); return true; } } catch (e) {}
5528
+ try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text); return true; } } catch (e) {}
5529
+ return false;
5530
+ }
5531
+
5532
+ // "path:line" for the current caret — source view (the painted file) or the diff caret. '' if neither.
5533
+ function caretLocation() {
5534
+ if (typeof isSourceViewerVisible === 'function' && isSourceViewerVisible()) {
5535
+ var sv = document.getElementById('source-viewer');
5536
+ var p = (sv && sv.dataset.openPath) || '';
5537
+ if (p && typeof viewerCursor !== 'undefined' && viewerCursor && viewerCursor.path === p) return p + ':' + (viewerCursor.lineIndex + 1);
5538
+ if (p) return p;
5539
+ }
5540
+ if (typeof isDiffViewVisible === 'function' && isDiffViewVisible() && typeof diffCursor !== 'undefined' && diffCursor) {
5541
+ var wrap = diffWrapperByPath(diffCursor.path);
5542
+ var row = wrap ? diffRowAt(wrap, diffCursor.side, diffCursor.rowIndex) : null;
5543
+ var ln = row ? diffLineNumber(row) : null;
5544
+ return diffCursor.path + (ln ? ':' + ln : '');
5545
+ }
5546
+ return '';
5547
+ }
5548
+
5549
+ // Cmd/Ctrl+K — copy the caret's file:line to the clipboard.
5550
+ function copyCaretLocation() {
5551
+ var loc = caretLocation();
5552
+ if (!loc) return;
5553
+ if (copyTextToClipboard(loc) && typeof showToast === 'function') showToast(t('goto.copied') + ' ' + loc);
5554
+ }
5555
+
5556
+ // Diff view: place the caret on the row whose (new, then old) line number matches n, in the active file.
5557
+ function gotoDiffLine(n) {
5558
+ var path = (typeof diffCursor !== 'undefined' && diffCursor && diffCursor.path) || '';
5559
+ if (!path && typeof diffActiveWrapper === 'function') {
5560
+ var w = diffActiveWrapper();
5561
+ var nm = w && w.querySelector('.d2h-file-name');
5562
+ if (nm && nm.textContent) path = nm.textContent.trim();
5563
+ }
5564
+ var wrap = path && diffWrapperByPath(path);
5565
+ if (!wrap) return;
5566
+ var sides = [(diffCursor && diffCursor.side) || 'new', 'new', 'old'];
5567
+ for (var s = 0; s < sides.length; s++) {
5568
+ var rows = diffRowsOf(diffSideTable(wrap, sides[s]));
5569
+ for (var i = 0; i < rows.length; i++) {
5570
+ if (diffLineNumber(rows[i]) === n) { setDiffCursor(path, sides[s], i, 0, true); return; }
5571
+ }
5572
+ }
5573
+ }
5574
+
5575
+ function gotoLineJump(n) {
5576
+ if (!(n >= 1)) return;
5577
+ if (typeof isSourceViewerVisible === 'function' && isSourceViewerVisible()) {
5578
+ var sv = document.getElementById('source-viewer');
5579
+ var p = (sv && sv.dataset.openPath) || '';
5580
+ var f = p && sourceByPath.get(p);
5581
+ if (f && f.embedded && typeof f.content === 'string') {
5582
+ var max = f.content.split(/\r?\n/).length;
5583
+ setSourceCursor(p, Math.max(0, Math.min(max - 1, n - 1)), 0, true, -1);
5584
+ return;
5585
+ }
5586
+ }
5587
+ if (typeof isDiffViewVisible === 'function' && isDiffViewVisible()) gotoDiffLine(n);
5588
+ }
5589
+
5590
+ // Cmd/Ctrl+L — a small numeric prompt; Enter jumps, Esc closes.
5591
+ function openGotoLine() {
5592
+ if (!((typeof isSourceViewerVisible === 'function' && isSourceViewerVisible()) || (typeof isDiffViewVisible === 'function' && isDiffViewVisible()))) return;
5593
+ var prior = document.getElementById('goto-line');
5594
+ if (prior) prior.remove();
5595
+ var box = document.createElement('div');
5596
+ box.id = 'goto-line';
5597
+ box.className = 'goto-line';
5598
+ var input = document.createElement('input');
5599
+ input.type = 'text';
5600
+ input.inputMode = 'numeric';
5601
+ input.className = 'goto-line-input';
5602
+ input.placeholder = t('goto.placeholder');
5603
+ box.appendChild(input);
5604
+ document.body.appendChild(box);
5605
+ function close() { box.remove(); document.removeEventListener('keydown', onKey, true); }
5606
+ function onKey(e) {
5607
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); }
5608
+ else if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); var n = parseInt(input.value, 10); close(); if (n >= 1) gotoLineJump(n); }
5609
+ }
5610
+ // Capture phase so Enter/Esc are handled here before the global keymap (which is on bubble).
5611
+ document.addEventListener('keydown', onKey, true);
5612
+ setTimeout(function () { try { input.focus(); } catch (e) {} }, 0);
5613
+ }
5614
+
5615
+ // Sidebar Opt+Enter: actions for a focused file row (copy path / reveal in Finder / open terminal here).
5616
+ function openTreeRowMenu(row) {
5617
+ if (!row) return;
5618
+ var path = row.dataset.sourceFile || row.dataset.file || '';
5619
+ if (!path) return;
5620
+ var r = row.getBoundingClientRect();
5621
+ var items = [
5622
+ { label: t('menu.copyPath'), onSelect: function () { if (copyTextToClipboard(path) && typeof showToast === 'function') showToast(t('goto.copied') + ' ' + path); } },
5623
+ ];
5624
+ if (window.monacoriApp && typeof window.monacoriApp.revealInFinder === 'function') {
5625
+ items.push({ label: t('menu.revealFinder'), onSelect: function () { try { window.monacoriApp.revealInFinder(path); } catch (e) {} } });
5626
+ items.push({ label: t('menu.openTerminal'), onSelect: function () { try { window.monacoriApp.openTerminalAt(path); } catch (e) {} } });
5627
+ }
5628
+ showCustomDropdown(Math.round(r.left + 14), Math.round(r.bottom + 2), items, Math.round(r.top));
5629
+ }