mbeditor 0.3.9 → 0.4.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +183 -16
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +18 -4
- data/app/assets/javascripts/mbeditor/components/TabBar.js +3 -2
- data/app/assets/stylesheets/mbeditor/editor.css +77 -1
- data/app/controllers/mbeditor/editors_controller.rb +6 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a73400b5400313c994fbd40ebae5000cc776f07eebeb9790380a622a193ba73
|
|
4
|
+
data.tar.gz: bdd3a1d8825e5ff23eef4fab724a33e3f48a0ab8ce3be316652c822f378ceffa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af01460ee4ece9edf319dfd84da74e0a4d6016cdc96c73c8f7bd1a1a421de254b7abcf157ec81c182591e94dff9b44a437b274f48c02129c6099a67f89dc6af2
|
|
7
|
+
data.tar.gz: 84d1af77774b4676e193db1800ba33401df62893491a134de11c84e6e3f17f26d2a7cc8b19c99bbdc05bbb9af018f079017ba1466680b29a00a065173399c572
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-04-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Draft auto-save and restore** — unsaved edits are written to `localStorage` every 500 ms per file. On reconnect after a server outage, a dialog offers to restore any drafts that diverged from the in-memory tab content.
|
|
12
|
+
- **Tab bar layout setting** — new "Tab bar layout" preference (Scroll / Wrap multi-row) stored in editor prefs; `TabBar` receives a `tabDisplayMode` prop and applies the appropriate CSS class; horizontal wheel scroll is disabled in wrap mode.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Drag-to-split** — pane 2 drop zone is now only shown when the cursor actively hovers over the right-half content area, preventing the tab bar from collapsing during a drag. Dropping onto a tab bar element is no longer intercepted by the cross-pane drop handler, preserving same-pane reordering.
|
|
16
|
+
- **Quick Open sorting** — files consistently rank above directories; within the same priority tier results are sorted by match relevance (exact basename > prefix match > substring > other).
|
|
17
|
+
- **Search total count** — the count thread is joined with a 100 ms timeout; `total_count` is omitted from the response when the thread has not finished, so the first page is never blocked by the counting subprocess.
|
|
18
|
+
- **Status bar message colour** — removed `color-mix` transparency that was dimming the accent-text colour.
|
|
19
|
+
|
|
8
20
|
## [0.3.9] - 2026-04-21
|
|
9
21
|
|
|
10
22
|
### Added
|
|
@@ -60,7 +60,8 @@ var DEFAULT_EDITOR_PREFS = {
|
|
|
60
60
|
prettierBracketSpacing: true,
|
|
61
61
|
vimMode: false,
|
|
62
62
|
fileTreeTypeahead: true,
|
|
63
|
-
quickOpenShowFolders: false
|
|
63
|
+
quickOpenShowFolders: false,
|
|
64
|
+
tabDisplayMode: 'scroll'
|
|
64
65
|
};
|
|
65
66
|
|
|
66
67
|
var SidebarActionButton = function SidebarActionButton(_ref) {
|
|
@@ -417,6 +418,36 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
417
418
|
var prevGitBranchRef = useRef(null);
|
|
418
419
|
var isSwitchingBranchRef = useRef(false);
|
|
419
420
|
|
|
421
|
+
// ── Draft backup helpers ─────────────────────────────────────────────────
|
|
422
|
+
var draftWriteTimerRef = useRef({});
|
|
423
|
+
var serverOnlineRef = useRef(true);
|
|
424
|
+
|
|
425
|
+
var _draftKey = function _draftKey(path) {
|
|
426
|
+
var base = typeof window.mbeditorBasePath === 'function' ? window.mbeditorBasePath() : '';
|
|
427
|
+
return 'mbeditor_draft\x00' + base + '\x00' + path;
|
|
428
|
+
};
|
|
429
|
+
var _saveDraftNow = function _saveDraftNow(path, content) {
|
|
430
|
+
try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
|
|
431
|
+
};
|
|
432
|
+
var _clearDraft = function _clearDraft(path) {
|
|
433
|
+
try { localStorage.removeItem(_draftKey(path)); } catch (e) {}
|
|
434
|
+
};
|
|
435
|
+
var _loadDraft = function _loadDraft(path) {
|
|
436
|
+
try { return JSON.parse(localStorage.getItem(_draftKey(path))); } catch (e) { return null; }
|
|
437
|
+
};
|
|
438
|
+
var _scheduleDraftWrite = function _scheduleDraftWrite(path, content) {
|
|
439
|
+
if (draftWriteTimerRef.current[path]) clearTimeout(draftWriteTimerRef.current[path]);
|
|
440
|
+
draftWriteTimerRef.current[path] = setTimeout(function () {
|
|
441
|
+
delete draftWriteTimerRef.current[path];
|
|
442
|
+
_saveDraftNow(path, content);
|
|
443
|
+
}, 500);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
var _useState_dro = useState(null);
|
|
447
|
+
var _useState_dro2 = _slicedToArray(_useState_dro, 2);
|
|
448
|
+
var draftRestoreOffer = _useState_dro2[0];
|
|
449
|
+
var setDraftRestoreOffer = _useState_dro2[1];
|
|
450
|
+
|
|
420
451
|
var clamp = function clamp(value, min, max) {
|
|
421
452
|
return Math.min(max, Math.max(min, value));
|
|
422
453
|
};
|
|
@@ -937,6 +968,28 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
937
968
|
return function () { clearTimeout(timeoutId); };
|
|
938
969
|
}, []);
|
|
939
970
|
|
|
971
|
+
// On reconnect: scan open dirty tabs for newer localStorage drafts and offer restore.
|
|
972
|
+
useEffect(function () {
|
|
973
|
+
if (!serverOnline) {
|
|
974
|
+
serverOnlineRef.current = false;
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (serverOnlineRef.current) return; // was already online — no transition
|
|
978
|
+
serverOnlineRef.current = true;
|
|
979
|
+
var st = EditorStore.getState();
|
|
980
|
+
var offers = [];
|
|
981
|
+
st.panes.forEach(function (pane) {
|
|
982
|
+
pane.tabs.forEach(function (tab) {
|
|
983
|
+
if (!tab.dirty || !tab.path || tab.path.startsWith('mbeditor://')) return;
|
|
984
|
+
var draft = _loadDraft(tab.path);
|
|
985
|
+
if (draft && draft.content !== tab.content) {
|
|
986
|
+
offers.push({ paneId: pane.id, tabId: tab.id, path: tab.path, name: tab.name, draftContent: draft.content });
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
if (offers.length > 0) setDraftRestoreOffer(offers);
|
|
991
|
+
}, [serverOnline]);
|
|
992
|
+
|
|
940
993
|
// Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
|
|
941
994
|
// Uses functional setTreeData to skip the re-render when nothing has changed.
|
|
942
995
|
useEffect(function () {
|
|
@@ -1248,6 +1301,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1248
1301
|
});
|
|
1249
1302
|
EditorStore.setState({ panes: newPanes });
|
|
1250
1303
|
EditorStore.setStatus("Saved", "success");
|
|
1304
|
+
_clearDraft(tab.path);
|
|
1251
1305
|
|
|
1252
1306
|
// Hot reload for Markdown: sync preview tab after save
|
|
1253
1307
|
if (/\.(md|markdown)$/i.test(tab.path)) {
|
|
@@ -1302,16 +1356,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1302
1356
|
var pane2 = EditorStore.getState().panes.find(function (p) {
|
|
1303
1357
|
return p.id === 2;
|
|
1304
1358
|
});
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1359
|
+
var alreadySplit = pane2 && pane2.tabs.length > 0;
|
|
1360
|
+
// Only set the split ref; do NOT pre-split the view width here.
|
|
1361
|
+
// Pane 2 appears as a drop zone only when the cursor actually hovers
|
|
1362
|
+
// over the right-half editor content, keeping the tab bar intact.
|
|
1363
|
+
dragSplitWidthRef.current = alreadySplit ? pane1Width : 50;
|
|
1311
1364
|
setDraggedTab({ sourcePaneId: sourcePaneId, tabId: tabId });
|
|
1312
1365
|
};
|
|
1313
1366
|
|
|
1314
1367
|
var clearDragState = function clearDragState() {
|
|
1368
|
+
// If pane 2 is still empty after the drag, restore pane 1 to full width.
|
|
1369
|
+
var pane2 = EditorStore.getState().panes.find(function (p) { return p.id === 2; });
|
|
1370
|
+
if (!pane2 || pane2.tabs.length === 0) {
|
|
1371
|
+
setPane1Width(100);
|
|
1372
|
+
dragSplitWidthRef.current = 50;
|
|
1373
|
+
}
|
|
1315
1374
|
setDraggedTab(null);
|
|
1316
1375
|
setDragOverPaneId(null);
|
|
1317
1376
|
};
|
|
@@ -2062,6 +2121,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2062
2121
|
tabs: tabs,
|
|
2063
2122
|
activeId: activeId,
|
|
2064
2123
|
paneId: paneId,
|
|
2124
|
+
tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
|
|
2065
2125
|
onSelect: function (id) {
|
|
2066
2126
|
// Sync explorer selection with the newly active tab so there's only one highlight
|
|
2067
2127
|
var tab = tabs.find(function(t) { return t.id === id; });
|
|
@@ -2604,16 +2664,40 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2604
2664
|
if (!draggedTab) return;
|
|
2605
2665
|
e.preventDefault();
|
|
2606
2666
|
|
|
2667
|
+
// If the cursor is over the tab bar, suppress the cross-pane split overlay
|
|
2668
|
+
// so same-pane tab reordering within any tab bar is unaffected.
|
|
2669
|
+
if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
|
|
2670
|
+
if (dragOverPaneId !== null) setDragOverPaneId(null);
|
|
2671
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2607
2675
|
var rect = e.currentTarget.getBoundingClientRect();
|
|
2608
2676
|
var splitAtX = rect.left + rect.width * (dragSplitWidthRef.current / 100);
|
|
2609
2677
|
var hoverPaneId = e.clientX >= splitAtX ? 2 : 1;
|
|
2610
2678
|
var nextDropPane = hoverPaneId === draggedTab.sourcePaneId ? null : hoverPaneId;
|
|
2611
2679
|
|
|
2612
|
-
|
|
2680
|
+
// When cursor first enters the right-half content area and pane 2 is empty,
|
|
2681
|
+
// apply the 50% split width so the drop zone becomes visible.
|
|
2682
|
+
if (nextDropPane === 2) {
|
|
2683
|
+
var pane2Empty = EditorStore.getState().panes.find(function(p) { return p.id === 2; });
|
|
2684
|
+
if (!pane2Empty || pane2Empty.tabs.length === 0) {
|
|
2685
|
+
setPane1Width(50);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2613
2690
|
if (dragOverPaneId !== nextDropPane) setDragOverPaneId(nextDropPane);
|
|
2614
2691
|
},
|
|
2615
2692
|
onDropCapture: function (e) {
|
|
2616
2693
|
if (!draggedTab) return;
|
|
2694
|
+
|
|
2695
|
+
// If dropping onto a tab bar element, let the tab item's own onDrop
|
|
2696
|
+
// bubble-phase handler manage the reorder — don't intercept here.
|
|
2697
|
+
if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2617
2701
|
e.preventDefault();
|
|
2618
2702
|
|
|
2619
2703
|
var rect = e.currentTarget.getBoundingClientRect();
|
|
@@ -2628,10 +2712,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2628
2712
|
}
|
|
2629
2713
|
},
|
|
2630
2714
|
state.panes.map(function (pane, idx) {
|
|
2631
|
-
|
|
2715
|
+
// Show empty pane 2 as a drop zone only when the cursor is actively hovering
|
|
2716
|
+
// over its half of the editor content (dragOverPaneId === 2).
|
|
2717
|
+
if (pane.id === 2 && pane.tabs.length === 0 && dragOverPaneId !== 2) return null;
|
|
2632
2718
|
|
|
2633
2719
|
// Dynamic width distribution
|
|
2634
|
-
var isSplit = state.panes[1].tabs.length > 0 ||
|
|
2720
|
+
var isSplit = state.panes[1].tabs.length > 0 || dragOverPaneId === 2;
|
|
2635
2721
|
var flexBasis = '100%';
|
|
2636
2722
|
if (isSplit) flexBasis = pane.id === 1 ? pane1Width + "%" : 100 - pane1Width + "%";
|
|
2637
2723
|
|
|
@@ -3109,6 +3195,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3109
3195
|
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { fileTreeTypeahead: v }); }); }
|
|
3110
3196
|
})
|
|
3111
3197
|
),
|
|
3198
|
+
React.createElement(
|
|
3199
|
+
'label', { className: 'ide-settings-row ide-settings-row-half' },
|
|
3200
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Tab bar layout'),
|
|
3201
|
+
React.createElement(
|
|
3202
|
+
'select', {
|
|
3203
|
+
value: editorPrefs.tabDisplayMode || 'scroll',
|
|
3204
|
+
onChange: function(e) { var v = e.target.value; setEditorPrefs(function(p) { return Object.assign({}, p, { tabDisplayMode: v }); }); }
|
|
3205
|
+
},
|
|
3206
|
+
React.createElement('option', { value: 'scroll' }, 'Scroll'),
|
|
3207
|
+
React.createElement('option', { value: 'wrap' }, 'Wrap (multi-row)')
|
|
3208
|
+
)
|
|
3209
|
+
),
|
|
3112
3210
|
React.createElement(
|
|
3113
3211
|
'label', { className: 'ide-settings-row ide-settings-row-check' },
|
|
3114
3212
|
React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
|
|
@@ -3206,8 +3304,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3206
3304
|
var valNorm = val.replace(/\r\n/g, '\n');
|
|
3207
3305
|
if (valNorm === cleanNorm) {
|
|
3208
3306
|
TabManager.markClean(pane.id, pActiveTab.id, val);
|
|
3307
|
+
_clearDraft(pActiveTab.path);
|
|
3209
3308
|
} else {
|
|
3210
3309
|
TabManager.markDirty(pane.id, pActiveTab.id, val);
|
|
3310
|
+
_scheduleDraftWrite(pActiveTab.path, val);
|
|
3211
3311
|
}
|
|
3212
3312
|
}
|
|
3213
3313
|
});
|
|
@@ -3409,12 +3509,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3409
3509
|
state.gitInfo.behind
|
|
3410
3510
|
)
|
|
3411
3511
|
),
|
|
3412
|
-
!serverOnline &&
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3512
|
+
!serverOnline && (function () {
|
|
3513
|
+
var dirtyCount = state.panes.reduce(function (acc, p) {
|
|
3514
|
+
return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
|
|
3515
|
+
}, 0);
|
|
3516
|
+
return React.createElement(
|
|
3517
|
+
"div",
|
|
3518
|
+
{
|
|
3519
|
+
className: "statusbar-offline",
|
|
3520
|
+
title: dirtyCount > 0 ? dirtyCount + " unsaved file" + (dirtyCount !== 1 ? "s" : "") + " — changes are backed up locally" : "Server offline"
|
|
3521
|
+
},
|
|
3522
|
+
React.createElement("i", { className: "fas fa-exclamation-triangle" }),
|
|
3523
|
+
dirtyCount > 0
|
|
3524
|
+
? " Offline \u2014 " + dirtyCount + " unsaved"
|
|
3525
|
+
: " Server offline"
|
|
3526
|
+
);
|
|
3527
|
+
})(),
|
|
3418
3528
|
activeFileCommit && React.createElement(
|
|
3419
3529
|
"div",
|
|
3420
3530
|
{ className: "statusbar-file-commit", title: activeFileCommit.title + " — " + activeFileCommit.author },
|
|
@@ -3549,6 +3659,63 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3549
3659
|
onSelectFolder: handleOpenFolderInExplorer,
|
|
3550
3660
|
onClose: function () { return setQuickOpen(false); }
|
|
3551
3661
|
}),
|
|
3662
|
+
draftRestoreOffer && React.createElement(
|
|
3663
|
+
"div",
|
|
3664
|
+
{
|
|
3665
|
+
className: "ide-draft-restore-overlay",
|
|
3666
|
+
role: "dialog",
|
|
3667
|
+
"aria-modal": "true",
|
|
3668
|
+
"aria-label": "Restore unsaved drafts"
|
|
3669
|
+
},
|
|
3670
|
+
React.createElement(
|
|
3671
|
+
"div",
|
|
3672
|
+
{ className: "ide-draft-restore-dialog" },
|
|
3673
|
+
React.createElement("div", { className: "ide-draft-restore-title" },
|
|
3674
|
+
React.createElement("i", { className: "fas fa-save", style: { marginRight: 8 } }),
|
|
3675
|
+
"Unsaved drafts found"
|
|
3676
|
+
),
|
|
3677
|
+
React.createElement("div", { className: "ide-draft-restore-body" },
|
|
3678
|
+
draftRestoreOffer.length + " file" + (draftRestoreOffer.length !== 1 ? "s have" : " has") + " locally backed-up drafts from when the server was offline:"
|
|
3679
|
+
),
|
|
3680
|
+
React.createElement(
|
|
3681
|
+
"ul",
|
|
3682
|
+
{ className: "ide-draft-restore-list" },
|
|
3683
|
+
draftRestoreOffer.map(function (o) {
|
|
3684
|
+
return React.createElement("li", { key: o.path }, o.name || o.path);
|
|
3685
|
+
})
|
|
3686
|
+
),
|
|
3687
|
+
React.createElement(
|
|
3688
|
+
"div",
|
|
3689
|
+
{ className: "ide-draft-restore-actions" },
|
|
3690
|
+
React.createElement(
|
|
3691
|
+
"button",
|
|
3692
|
+
{
|
|
3693
|
+
type: "button",
|
|
3694
|
+
className: "ide-draft-restore-btn ide-draft-restore-btn-primary",
|
|
3695
|
+
onClick: function () {
|
|
3696
|
+
draftRestoreOffer.forEach(function (offer) {
|
|
3697
|
+
TabManager.markDirty(offer.paneId, offer.tabId, offer.draftContent);
|
|
3698
|
+
});
|
|
3699
|
+
setDraftRestoreOffer(null);
|
|
3700
|
+
}
|
|
3701
|
+
},
|
|
3702
|
+
"Restore all"
|
|
3703
|
+
),
|
|
3704
|
+
React.createElement(
|
|
3705
|
+
"button",
|
|
3706
|
+
{
|
|
3707
|
+
type: "button",
|
|
3708
|
+
className: "ide-draft-restore-btn",
|
|
3709
|
+
onClick: function () {
|
|
3710
|
+
draftRestoreOffer.forEach(function (offer) { _clearDraft(offer.path); });
|
|
3711
|
+
setDraftRestoreOffer(null);
|
|
3712
|
+
}
|
|
3713
|
+
},
|
|
3714
|
+
"Discard drafts"
|
|
3715
|
+
)
|
|
3716
|
+
)
|
|
3717
|
+
)
|
|
3718
|
+
),
|
|
3552
3719
|
contextMenu && React.createElement(
|
|
3553
3720
|
React.Fragment,
|
|
3554
3721
|
null,
|
|
@@ -94,6 +94,17 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
94
94
|
return 50;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// Match relevance within a priority tier: exact basename > prefix > substring > other.
|
|
98
|
+
function getMatchRelevance(result, q) {
|
|
99
|
+
if (!q) return 3;
|
|
100
|
+
var name = (result.name || (result.path || '').split('/').pop() || '').toLowerCase();
|
|
101
|
+
var lq = q.toLowerCase();
|
|
102
|
+
if (name === lq) return 0;
|
|
103
|
+
if (name.slice(0, lq.length) === lq) return 1;
|
|
104
|
+
if (name.indexOf(lq) >= 0) return 2;
|
|
105
|
+
return 3;
|
|
106
|
+
}
|
|
107
|
+
|
|
97
108
|
var getQuickOpenIcon = function getQuickOpenIcon(path, name, type) {
|
|
98
109
|
if (type === 'dir') {
|
|
99
110
|
return React.createElement('i', { className: 'fas fa-folder quick-open-result-icon quick-open-folder-icon', 'aria-hidden': 'true' });
|
|
@@ -117,11 +128,14 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
|
|
|
117
128
|
var res = SearchService.searchFiles(query);
|
|
118
129
|
// Filter by type: always include files; include dirs only when showFolders is on
|
|
119
130
|
var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
|
|
120
|
-
//
|
|
121
|
-
// JS sort is stable in modern engines
|
|
122
|
-
//
|
|
131
|
+
// Sort: files always beat dirs; within the same tier exact basename > prefix > substring > other.
|
|
132
|
+
// JS sort is stable in modern engines so MiniSearch relevance score order is the tiebreaker
|
|
133
|
+
// when match relevance is equal.
|
|
123
134
|
filtered.sort(function(a, b) {
|
|
124
|
-
|
|
135
|
+
var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
|
|
136
|
+
var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
|
|
137
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
138
|
+
return getMatchRelevance(a, query) - getMatchRelevance(b, query);
|
|
125
139
|
});
|
|
126
140
|
setResults(filtered.slice(0, 200));
|
|
127
141
|
setSelectedIndex(0);
|
|
@@ -18,6 +18,7 @@ var TabBar = function TabBar(_ref) {
|
|
|
18
18
|
var onHardenTab = _ref.onHardenTab;
|
|
19
19
|
var onShowHistory = _ref.onShowHistory;
|
|
20
20
|
var onRevealInExplorer = _ref.onRevealInExplorer;
|
|
21
|
+
var tabDisplayMode = _ref.tabDisplayMode || 'scroll';
|
|
21
22
|
|
|
22
23
|
var containerRef = useRef(null);
|
|
23
24
|
|
|
@@ -87,8 +88,8 @@ var TabBar = function TabBar(_ref) {
|
|
|
87
88
|
null,
|
|
88
89
|
React.createElement(
|
|
89
90
|
'div',
|
|
90
|
-
{ className: 'tab-bar', ref: containerRef, onWheel: function (e) {
|
|
91
|
-
if (containerRef.current) {
|
|
91
|
+
{ className: 'tab-bar tab-bar-' + tabDisplayMode, ref: containerRef, onWheel: function (e) {
|
|
92
|
+
if (tabDisplayMode !== 'wrap' && containerRef.current) {
|
|
92
93
|
containerRef.current.scrollLeft += e.deltaY;
|
|
93
94
|
}
|
|
94
95
|
} },
|
|
@@ -327,6 +327,18 @@ html, body, #mbeditor-root {
|
|
|
327
327
|
.tab-bar::-webkit-scrollbar { height: 3px; }
|
|
328
328
|
.tab-bar::-webkit-scrollbar-thumb { background: var(--ide-hover-bg); }
|
|
329
329
|
|
|
330
|
+
/* Wrap (multi-row) tab layout */
|
|
331
|
+
.tab-bar.tab-bar-wrap {
|
|
332
|
+
flex-wrap: wrap;
|
|
333
|
+
overflow-x: hidden;
|
|
334
|
+
overflow-y: visible;
|
|
335
|
+
height: auto;
|
|
336
|
+
}
|
|
337
|
+
.tab-bar.tab-bar-wrap .tab-item {
|
|
338
|
+
flex-shrink: 1;
|
|
339
|
+
flex-grow: 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
330
342
|
.tab-item {
|
|
331
343
|
display: flex;
|
|
332
344
|
align-items: center;
|
|
@@ -792,7 +804,7 @@ html, body, #mbeditor-root {
|
|
|
792
804
|
|
|
793
805
|
.statusbar-msg {
|
|
794
806
|
margin-left: auto;
|
|
795
|
-
color:
|
|
807
|
+
color: var(--ide-accent-text);
|
|
796
808
|
font-size: 11px;
|
|
797
809
|
overflow: hidden;
|
|
798
810
|
text-overflow: ellipsis;
|
|
@@ -1683,6 +1695,70 @@ html, body, #mbeditor-root {
|
|
|
1683
1695
|
to { opacity: 1; transform: translateY(0); }
|
|
1684
1696
|
}
|
|
1685
1697
|
|
|
1698
|
+
/* ── Draft restore dialog ───────────────────────────────────── */
|
|
1699
|
+
.ide-draft-restore-overlay {
|
|
1700
|
+
position: fixed;
|
|
1701
|
+
inset: 0;
|
|
1702
|
+
background: rgba(0,0,0,0.55);
|
|
1703
|
+
z-index: 10001;
|
|
1704
|
+
display: flex;
|
|
1705
|
+
align-items: center;
|
|
1706
|
+
justify-content: center;
|
|
1707
|
+
}
|
|
1708
|
+
.ide-draft-restore-dialog {
|
|
1709
|
+
background: var(--ide-panel-bg-alt);
|
|
1710
|
+
border: 1px solid var(--ide-border-input);
|
|
1711
|
+
border-radius: 8px;
|
|
1712
|
+
padding: 20px 24px;
|
|
1713
|
+
min-width: 320px;
|
|
1714
|
+
max-width: 480px;
|
|
1715
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
1716
|
+
color: var(--ide-text);
|
|
1717
|
+
font-size: 13px;
|
|
1718
|
+
}
|
|
1719
|
+
.ide-draft-restore-title {
|
|
1720
|
+
font-size: 14px;
|
|
1721
|
+
font-weight: 600;
|
|
1722
|
+
margin-bottom: 10px;
|
|
1723
|
+
color: var(--ide-warning, #e5c07b);
|
|
1724
|
+
}
|
|
1725
|
+
.ide-draft-restore-body { margin-bottom: 8px; color: var(--ide-text-muted); }
|
|
1726
|
+
.ide-draft-restore-list {
|
|
1727
|
+
list-style: none;
|
|
1728
|
+
padding: 0;
|
|
1729
|
+
margin: 0 0 16px 0;
|
|
1730
|
+
max-height: 160px;
|
|
1731
|
+
overflow-y: auto;
|
|
1732
|
+
}
|
|
1733
|
+
.ide-draft-restore-list li {
|
|
1734
|
+
padding: 3px 0;
|
|
1735
|
+
font-family: monospace;
|
|
1736
|
+
font-size: 12px;
|
|
1737
|
+
color: var(--ide-accent-fg, #9cdcfe);
|
|
1738
|
+
}
|
|
1739
|
+
.ide-draft-restore-actions {
|
|
1740
|
+
display: flex;
|
|
1741
|
+
gap: 8px;
|
|
1742
|
+
justify-content: flex-end;
|
|
1743
|
+
}
|
|
1744
|
+
.ide-draft-restore-btn {
|
|
1745
|
+
padding: 5px 14px;
|
|
1746
|
+
border-radius: 4px;
|
|
1747
|
+
border: 1px solid var(--ide-border-input);
|
|
1748
|
+
background: var(--ide-input-bg);
|
|
1749
|
+
color: var(--ide-text);
|
|
1750
|
+
font-size: 12px;
|
|
1751
|
+
cursor: pointer;
|
|
1752
|
+
transition: background 0.1s;
|
|
1753
|
+
}
|
|
1754
|
+
.ide-draft-restore-btn:hover { background: var(--ide-hover-bg); }
|
|
1755
|
+
.ide-draft-restore-btn-primary {
|
|
1756
|
+
background: var(--ide-accent);
|
|
1757
|
+
color: var(--ide-accent-text);
|
|
1758
|
+
border-color: var(--ide-accent);
|
|
1759
|
+
}
|
|
1760
|
+
.ide-draft-restore-btn-primary:hover { opacity: 0.9; }
|
|
1761
|
+
|
|
1686
1762
|
/* ── Settings panel ─────────────────────────────────────────── */
|
|
1687
1763
|
.ide-settings-panel { display: flex; flex-direction: column; height: 100%; overflow-y: auto; }
|
|
1688
1764
|
.ide-settings-body { padding: 8px 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; align-items: start; }
|
|
@@ -328,7 +328,12 @@ module Mbeditor
|
|
|
328
328
|
results = stream_search_results(query, needed)
|
|
329
329
|
has_more = results.length > offset + limit
|
|
330
330
|
response = { results: results[offset, limit] || [], has_more: has_more }
|
|
331
|
-
|
|
331
|
+
if count_thread
|
|
332
|
+
# Give the count thread up to 100 ms; omit total_count when it hasn't finished yet
|
|
333
|
+
# so the first page is never blocked by the counting subprocess.
|
|
334
|
+
count_thread.join(0.1)
|
|
335
|
+
response[:total_count] = count_thread.value unless count_thread.alive?
|
|
336
|
+
end
|
|
332
337
|
|
|
333
338
|
render json: response
|
|
334
339
|
rescue StandardError => e
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|