@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.
- package/dist/app-main.js +57 -1
- package/dist/git-log.d.ts +23 -0
- package/dist/git-log.js +60 -0
- package/dist/i18n.js +34 -0
- package/dist/preload.cjs +8 -0
- package/dist/render.js +20 -0
- package/dist/viewer.client.js +463 -11
- package/dist/viewer.client.min.js +1 -1
- package/dist/viewer.css +62 -7
- package/package.json +1 -1
package/dist/viewer.client.js
CHANGED
|
@@ -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
|
-
|
|
3352
|
-
|
|
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 ===
|
|
3412
|
+
if (state.viewerCursor && state.viewerCursor.path === openPath) {
|
|
3355
3413
|
var vc = state.viewerCursor;
|
|
3356
|
-
setTimeout(function () { try { setSourceCursor(
|
|
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
|
-
//
|
|
3400
|
-
//
|
|
3401
|
-
|
|
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
|
-
|
|
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 ? ' <' + escapeHtml(d.email) + '>' : '') + '</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
|
+
}
|