mbeditor 0.3.9 → 0.4.3
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 +38 -0
- data/README.md +18 -0
- data/app/assets/javascripts/mbeditor/application.js +1 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +161 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +392 -62
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +20 -4
- data/app/assets/javascripts/mbeditor/components/TabBar.js +3 -2
- data/app/assets/javascripts/mbeditor/editor_plugins.js +21 -0
- data/app/assets/javascripts/mbeditor/file_service.js +6 -0
- data/app/assets/javascripts/mbeditor/search_service.js +7 -3
- data/app/assets/javascripts/mbeditor/websocket_service.js +195 -0
- data/app/assets/stylesheets/mbeditor/application.css +19 -0
- data/app/assets/stylesheets/mbeditor/editor.css +225 -10
- data/app/channels/mbeditor/editor_channel.rb +81 -0
- data/app/controllers/mbeditor/editors_controller.rb +65 -13
- data/app/views/layouts/mbeditor/application.html.erb +4 -0
- data/lib/mbeditor/cable_log_filter.rb +56 -0
- data/lib/mbeditor/engine.rb +10 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +4 -1
- data/lib/mbeditor/version.rb +1 -1
- metadata +5 -2
|
@@ -60,7 +60,9 @@ 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',
|
|
65
|
+
persistFindState: true
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
var SidebarActionButton = function SidebarActionButton(_ref) {
|
|
@@ -195,7 +197,25 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
195
197
|
var searchOffsetRef = useRef(0);
|
|
196
198
|
var searchLoadingMoreRef = useRef(false);
|
|
197
199
|
|
|
200
|
+
var _useStateRx = useState(false);
|
|
201
|
+
var _useStateRx2 = _slicedToArray(_useStateRx, 2);
|
|
202
|
+
var searchUseRegex = _useStateRx2[0];
|
|
203
|
+
var setSearchUseRegex = _useStateRx2[1];
|
|
204
|
+
|
|
205
|
+
var _useStateMC = useState(false);
|
|
206
|
+
var _useStateMC2 = _slicedToArray(_useStateMC, 2);
|
|
207
|
+
var searchMatchCase = _useStateMC2[0];
|
|
208
|
+
var setSearchMatchCase = _useStateMC2[1];
|
|
209
|
+
|
|
210
|
+
var _useStateWW = useState(false);
|
|
211
|
+
var _useStateWW2 = _slicedToArray(_useStateWW, 2);
|
|
212
|
+
var searchWholeWord = _useStateWW2[0];
|
|
213
|
+
var setSearchWholeWord = _useStateWW2[1];
|
|
214
|
+
|
|
198
215
|
var searchQueryRef = useRef('');
|
|
216
|
+
var searchUseRegexRef = useRef(false);
|
|
217
|
+
var searchMatchCaseRef = useRef(false);
|
|
218
|
+
var searchWholeWordRef = useRef(false);
|
|
199
219
|
var searchResultsContainerRef = useRef(null);
|
|
200
220
|
|
|
201
221
|
var _useState8 = useState("explorer");
|
|
@@ -417,6 +437,36 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
417
437
|
var prevGitBranchRef = useRef(null);
|
|
418
438
|
var isSwitchingBranchRef = useRef(false);
|
|
419
439
|
|
|
440
|
+
// ── Draft backup helpers ─────────────────────────────────────────────────
|
|
441
|
+
var draftWriteTimerRef = useRef({});
|
|
442
|
+
var serverOnlineRef = useRef(true);
|
|
443
|
+
|
|
444
|
+
var _draftKey = function _draftKey(path) {
|
|
445
|
+
var base = typeof window.mbeditorBasePath === 'function' ? window.mbeditorBasePath() : '';
|
|
446
|
+
return 'mbeditor_draft\x00' + base + '\x00' + path;
|
|
447
|
+
};
|
|
448
|
+
var _saveDraftNow = function _saveDraftNow(path, content) {
|
|
449
|
+
try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
|
|
450
|
+
};
|
|
451
|
+
var _clearDraft = function _clearDraft(path) {
|
|
452
|
+
try { localStorage.removeItem(_draftKey(path)); } catch (e) {}
|
|
453
|
+
};
|
|
454
|
+
var _loadDraft = function _loadDraft(path) {
|
|
455
|
+
try { return JSON.parse(localStorage.getItem(_draftKey(path))); } catch (e) { return null; }
|
|
456
|
+
};
|
|
457
|
+
var _scheduleDraftWrite = function _scheduleDraftWrite(path, content) {
|
|
458
|
+
if (draftWriteTimerRef.current[path]) clearTimeout(draftWriteTimerRef.current[path]);
|
|
459
|
+
draftWriteTimerRef.current[path] = setTimeout(function () {
|
|
460
|
+
delete draftWriteTimerRef.current[path];
|
|
461
|
+
_saveDraftNow(path, content);
|
|
462
|
+
}, 500);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
var _useState_dro = useState(null);
|
|
466
|
+
var _useState_dro2 = _slicedToArray(_useState_dro, 2);
|
|
467
|
+
var draftRestoreOffer = _useState_dro2[0];
|
|
468
|
+
var setDraftRestoreOffer = _useState_dro2[1];
|
|
469
|
+
|
|
420
470
|
var clamp = function clamp(value, min, max) {
|
|
421
471
|
return Math.min(max, Math.max(min, value));
|
|
422
472
|
};
|
|
@@ -634,6 +684,9 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
634
684
|
if (workspace && typeof workspace.testAvailable === 'boolean') {
|
|
635
685
|
setTestAvailable(workspace.testAvailable);
|
|
636
686
|
}
|
|
687
|
+
if (workspace && typeof workspace.actionCableEnabled === 'boolean') {
|
|
688
|
+
WebSocketService.connect(workspace.actionCableEnabled);
|
|
689
|
+
}
|
|
637
690
|
});
|
|
638
691
|
|
|
639
692
|
// Helper: load tab content for a set of panes and restore them into EditorStore
|
|
@@ -937,11 +990,55 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
937
990
|
return function () { clearTimeout(timeoutId); };
|
|
938
991
|
}, []);
|
|
939
992
|
|
|
993
|
+
// On reconnect: scan open dirty tabs for newer localStorage drafts and offer restore.
|
|
994
|
+
useEffect(function () {
|
|
995
|
+
if (!serverOnline) {
|
|
996
|
+
serverOnlineRef.current = false;
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (serverOnlineRef.current) return; // was already online — no transition
|
|
1000
|
+
serverOnlineRef.current = true;
|
|
1001
|
+
var st = EditorStore.getState();
|
|
1002
|
+
var offers = [];
|
|
1003
|
+
st.panes.forEach(function (pane) {
|
|
1004
|
+
pane.tabs.forEach(function (tab) {
|
|
1005
|
+
if (!tab.dirty || !tab.path || tab.path.startsWith('mbeditor://')) return;
|
|
1006
|
+
var draft = _loadDraft(tab.path);
|
|
1007
|
+
if (draft && draft.content !== tab.content) {
|
|
1008
|
+
offers.push({ paneId: pane.id, tabId: tab.id, path: tab.path, name: tab.name, draftContent: draft.content });
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
});
|
|
1012
|
+
if (offers.length > 0) setDraftRestoreOffer(offers);
|
|
1013
|
+
}, [serverOnline]);
|
|
1014
|
+
|
|
1015
|
+
// WebSocket push — when the server broadcasts files_changed, refresh the tree
|
|
1016
|
+
// and git status immediately (same work as the 10s poll below does).
|
|
1017
|
+
useEffect(function () {
|
|
1018
|
+
function handleFilesChanged() {
|
|
1019
|
+
if (document.hidden) return;
|
|
1020
|
+
GitService.fetchStatus()["catch"](function () {});
|
|
1021
|
+
FileService.getTree().then(function (data) {
|
|
1022
|
+
var newData = data || [];
|
|
1023
|
+
setTreeData(function (prevData) {
|
|
1024
|
+
if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
|
|
1025
|
+
SearchService.buildIndex(newData);
|
|
1026
|
+
return newData;
|
|
1027
|
+
});
|
|
1028
|
+
})["catch"](function () {});
|
|
1029
|
+
}
|
|
1030
|
+
WebSocketService.onFilesChanged(handleFilesChanged);
|
|
1031
|
+
return function () { WebSocketService.offFilesChanged(handleFilesChanged); };
|
|
1032
|
+
}, []);
|
|
1033
|
+
|
|
940
1034
|
// Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
|
|
1035
|
+
// When an ActionCable WebSocket is connected this acts only as a safety-net fallback —
|
|
1036
|
+
// the WebSocket push above handles immediate invalidation after mbeditor mutations.
|
|
941
1037
|
// Uses functional setTreeData to skip the re-render when nothing has changed.
|
|
942
1038
|
useEffect(function () {
|
|
943
1039
|
var intervalId = setInterval(function () {
|
|
944
1040
|
if (document.hidden) return;
|
|
1041
|
+
if (WebSocketService.isConnected()) return; // WebSocket is handling refreshes
|
|
945
1042
|
// Refresh tree and check for git branch changes (to trigger per-branch tab state swap)
|
|
946
1043
|
GitService.fetchStatus()["catch"](function () {});
|
|
947
1044
|
FileService.getTree().then(function (data) {
|
|
@@ -1145,6 +1242,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1145
1242
|
var activeFileCommit = _useState32[0];
|
|
1146
1243
|
var setActiveFileCommit = _useState32[1];
|
|
1147
1244
|
|
|
1245
|
+
// EOL indicator — tracks current line-ending style of the active file
|
|
1246
|
+
var _useState31e = useState(null);
|
|
1247
|
+
var _useState31e2 = _slicedToArray(_useState31e, 2);
|
|
1248
|
+
var activeEOL = _useState31e2[0];
|
|
1249
|
+
var setActiveEOL = _useState31e2[1];
|
|
1250
|
+
|
|
1148
1251
|
useEffect(function () {
|
|
1149
1252
|
if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('diff://') === 0 || activeTab.path.indexOf('combined-diff://') === 0) {
|
|
1150
1253
|
setActiveFileCommit(null);
|
|
@@ -1163,6 +1266,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1163
1266
|
});
|
|
1164
1267
|
}, [activeTab ? activeTab.id : null, gitAvailable]);
|
|
1165
1268
|
|
|
1269
|
+
// Update EOL indicator whenever active tab or its content changes
|
|
1270
|
+
useEffect(function () {
|
|
1271
|
+
if (!activeTab || typeof activeTab.content !== 'string' ||
|
|
1272
|
+
activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || activeTab.isPreview) {
|
|
1273
|
+
setActiveEOL(null);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
if (activeTab.content.indexOf('\r\n') !== -1) {
|
|
1277
|
+
setActiveEOL('CRLF');
|
|
1278
|
+
} else if (activeTab.content.indexOf('\r') !== -1) {
|
|
1279
|
+
setActiveEOL('CR');
|
|
1280
|
+
} else {
|
|
1281
|
+
setActiveEOL('LF');
|
|
1282
|
+
}
|
|
1283
|
+
}, [activeTab ? activeTab.id : null, activeTab ? activeTab.content : null]);
|
|
1284
|
+
|
|
1166
1285
|
useEffect(function () {
|
|
1167
1286
|
if (!activeTab || typeof activeTab.content !== 'string') return;
|
|
1168
1287
|
if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph) return;
|
|
@@ -1248,6 +1367,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1248
1367
|
});
|
|
1249
1368
|
EditorStore.setState({ panes: newPanes });
|
|
1250
1369
|
EditorStore.setStatus("Saved", "success");
|
|
1370
|
+
_clearDraft(tab.path);
|
|
1251
1371
|
|
|
1252
1372
|
// Hot reload for Markdown: sync preview tab after save
|
|
1253
1373
|
if (/\.(md|markdown)$/i.test(tab.path)) {
|
|
@@ -1302,16 +1422,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1302
1422
|
var pane2 = EditorStore.getState().panes.find(function (p) {
|
|
1303
1423
|
return p.id === 2;
|
|
1304
1424
|
});
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1425
|
+
var alreadySplit = pane2 && pane2.tabs.length > 0;
|
|
1426
|
+
// Only set the split ref; do NOT pre-split the view width here.
|
|
1427
|
+
// Pane 2 appears as a drop zone only when the cursor actually hovers
|
|
1428
|
+
// over the right-half editor content, keeping the tab bar intact.
|
|
1429
|
+
dragSplitWidthRef.current = alreadySplit ? pane1Width : 50;
|
|
1311
1430
|
setDraggedTab({ sourcePaneId: sourcePaneId, tabId: tabId });
|
|
1312
1431
|
};
|
|
1313
1432
|
|
|
1314
1433
|
var clearDragState = function clearDragState() {
|
|
1434
|
+
// If pane 2 is still empty after the drag, restore pane 1 to full width.
|
|
1435
|
+
var pane2 = EditorStore.getState().panes.find(function (p) { return p.id === 2; });
|
|
1436
|
+
if (!pane2 || pane2.tabs.length === 0) {
|
|
1437
|
+
setPane1Width(100);
|
|
1438
|
+
dragSplitWidthRef.current = 50;
|
|
1439
|
+
}
|
|
1315
1440
|
setDraggedTab(null);
|
|
1316
1441
|
setDragOverPaneId(null);
|
|
1317
1442
|
};
|
|
@@ -1322,6 +1447,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1322
1447
|
clearDragState();
|
|
1323
1448
|
};
|
|
1324
1449
|
|
|
1450
|
+
var handleChangeEOL = function handleChangeEOL(newEOL) {
|
|
1451
|
+
var ed = window.__mbeditorActiveEditor;
|
|
1452
|
+
if (!ed || !window.monaco) return;
|
|
1453
|
+
var model = ed.getModel();
|
|
1454
|
+
if (!model || !activeTab || !focusedPane) return;
|
|
1455
|
+
var seq = newEOL === 'CRLF'
|
|
1456
|
+
? window.monaco.editor.EndOfLineSequence.CRLF
|
|
1457
|
+
: window.monaco.editor.EndOfLineSequence.LF;
|
|
1458
|
+
model.setEOL(seq);
|
|
1459
|
+
var newContent = model.getValue();
|
|
1460
|
+
TabManager.markDirty(focusedPane.id, activeTab.path, newContent);
|
|
1461
|
+
setActiveEOL(newEOL);
|
|
1462
|
+
EditorStore.setStatus('Line endings changed to ' + newEOL, 'info');
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1325
1465
|
var handleFormat = function handleFormat() {
|
|
1326
1466
|
if (!activeTab) return;
|
|
1327
1467
|
|
|
@@ -1519,7 +1659,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1519
1659
|
searchQueryRef.current = q;
|
|
1520
1660
|
EditorStore.setState({ searchResults: [], searchHasMore: false });
|
|
1521
1661
|
EditorStore.setStatus("Searching project...", "info");
|
|
1522
|
-
SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE).then(function (res) {
|
|
1662
|
+
SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE, { regex: searchUseRegexRef.current, matchCase: searchMatchCaseRef.current, wholeWord: searchWholeWordRef.current }).then(function (res) {
|
|
1523
1663
|
if (searchRequestIdRef.current !== requestId) return;
|
|
1524
1664
|
var hasMore = !!(res && res.hasMore);
|
|
1525
1665
|
setSearchHasMore(hasMore); searchHasMoreRef.current = hasMore;
|
|
@@ -1537,7 +1677,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1537
1677
|
if (!q || searchLoadingMoreRef.current || !searchHasMoreRef.current) return;
|
|
1538
1678
|
searchLoadingMoreRef.current = true;
|
|
1539
1679
|
var offset = searchOffsetRef.current;
|
|
1540
|
-
SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE).then(function(res) {
|
|
1680
|
+
SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE, { regex: searchUseRegexRef.current, matchCase: searchMatchCaseRef.current, wholeWord: searchWholeWordRef.current }).then(function(res) {
|
|
1541
1681
|
if (searchQueryRef.current !== q) { searchLoadingMoreRef.current = false; return; }
|
|
1542
1682
|
var hasMore = !!(res && res.hasMore);
|
|
1543
1683
|
searchHasMoreRef.current = hasMore;
|
|
@@ -1556,6 +1696,33 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1556
1696
|
_debouncedSearch(val);
|
|
1557
1697
|
};
|
|
1558
1698
|
|
|
1699
|
+
var handleSearchRegexToggle = function handleSearchRegexToggle() {
|
|
1700
|
+
var next = !searchUseRegexRef.current;
|
|
1701
|
+
searchUseRegexRef.current = next;
|
|
1702
|
+
setSearchUseRegex(next);
|
|
1703
|
+
if (searchQueryRef.current) {
|
|
1704
|
+
_debouncedSearch(searchQueryRef.current);
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
var handleSearchMatchCaseToggle = function handleSearchMatchCaseToggle() {
|
|
1709
|
+
var next = !searchMatchCaseRef.current;
|
|
1710
|
+
searchMatchCaseRef.current = next;
|
|
1711
|
+
setSearchMatchCase(next);
|
|
1712
|
+
if (searchQueryRef.current) {
|
|
1713
|
+
_debouncedSearch(searchQueryRef.current);
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
var handleSearchWholeWordToggle = function handleSearchWholeWordToggle() {
|
|
1718
|
+
var next = !searchWholeWordRef.current;
|
|
1719
|
+
searchWholeWordRef.current = next;
|
|
1720
|
+
setSearchWholeWord(next);
|
|
1721
|
+
if (searchQueryRef.current) {
|
|
1722
|
+
_debouncedSearch(searchQueryRef.current);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1559
1726
|
var clearSearch = function clearSearch() {
|
|
1560
1727
|
searchRequestIdRef.current += 1;
|
|
1561
1728
|
if (_debouncedSearch.cancel) _debouncedSearch.cancel();
|
|
@@ -2062,6 +2229,7 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2062
2229
|
tabs: tabs,
|
|
2063
2230
|
activeId: activeId,
|
|
2064
2231
|
paneId: paneId,
|
|
2232
|
+
tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
|
|
2065
2233
|
onSelect: function (id) {
|
|
2066
2234
|
// Sync explorer selection with the newly active tab so there's only one highlight
|
|
2067
2235
|
var tab = tabs.find(function(t) { return t.id === id; });
|
|
@@ -2504,32 +2672,57 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2504
2672
|
{ className: "search-panel" },
|
|
2505
2673
|
React.createElement(
|
|
2506
2674
|
"div",
|
|
2507
|
-
{ className: "search-input-
|
|
2675
|
+
{ className: "search-input-shell" },
|
|
2676
|
+
React.createElement("input", {
|
|
2677
|
+
className: "search-input",
|
|
2678
|
+
placeholder: "Find in files…",
|
|
2679
|
+
value: searchQuery,
|
|
2680
|
+
onChange: handleSearchChange
|
|
2681
|
+
}),
|
|
2508
2682
|
React.createElement(
|
|
2509
2683
|
"div",
|
|
2510
|
-
{ className: "search-input-
|
|
2511
|
-
React.createElement(
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2684
|
+
{ className: "search-input-adornments" },
|
|
2685
|
+
React.createElement(
|
|
2686
|
+
"button",
|
|
2687
|
+
{
|
|
2688
|
+
type: "button",
|
|
2689
|
+
className: "search-adornment-btn" + (searchMatchCase ? " active" : ""),
|
|
2690
|
+
onClick: handleSearchMatchCaseToggle,
|
|
2691
|
+
title: "Match Case"
|
|
2692
|
+
},
|
|
2693
|
+
React.createElement("i", { className: "codicon codicon-case-sensitive" })
|
|
2694
|
+
),
|
|
2695
|
+
React.createElement(
|
|
2696
|
+
"button",
|
|
2697
|
+
{
|
|
2698
|
+
type: "button",
|
|
2699
|
+
className: "search-adornment-btn" + (searchWholeWord ? " active" : ""),
|
|
2700
|
+
onClick: handleSearchWholeWordToggle,
|
|
2701
|
+
title: "Match Whole Word"
|
|
2702
|
+
},
|
|
2703
|
+
React.createElement("i", { className: "codicon codicon-whole-word" })
|
|
2704
|
+
),
|
|
2705
|
+
React.createElement(
|
|
2706
|
+
"button",
|
|
2707
|
+
{
|
|
2708
|
+
type: "button",
|
|
2709
|
+
className: "search-adornment-btn" + (searchUseRegex ? " active" : ""),
|
|
2710
|
+
onClick: handleSearchRegexToggle,
|
|
2711
|
+
title: "Use Regular Expression"
|
|
2712
|
+
},
|
|
2713
|
+
React.createElement("i", { className: "codicon codicon-regex" })
|
|
2714
|
+
),
|
|
2517
2715
|
searchQuery && React.createElement(
|
|
2518
2716
|
"button",
|
|
2519
2717
|
{
|
|
2520
2718
|
type: "button",
|
|
2521
|
-
className: "search-
|
|
2719
|
+
className: "search-adornment-btn search-adornment-clear",
|
|
2522
2720
|
onClick: clearSearch,
|
|
2523
2721
|
title: "Clear search",
|
|
2524
2722
|
"aria-label": "Clear search"
|
|
2525
2723
|
},
|
|
2526
2724
|
React.createElement("i", { className: "fas fa-times" })
|
|
2527
2725
|
)
|
|
2528
|
-
),
|
|
2529
|
-
React.createElement(
|
|
2530
|
-
"button",
|
|
2531
|
-
{ type: "submit", className: "search-btn", disabled: searchLoading, title: searchLoading ? "Searching..." : "Search" },
|
|
2532
|
-
React.createElement("i", { className: searchLoading ? "fas fa-spinner fa-spin" : "fas fa-search" })
|
|
2533
2726
|
)
|
|
2534
2727
|
),
|
|
2535
2728
|
(function() {
|
|
@@ -2548,38 +2741,47 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2548
2741
|
? (total + (searchHasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""))
|
|
2549
2742
|
: "No results"
|
|
2550
2743
|
),
|
|
2551
|
-
|
|
2744
|
+
React.createElement(
|
|
2552
2745
|
"div",
|
|
2553
|
-
{
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2746
|
+
{ className: "search-results-area" },
|
|
2747
|
+
hasAny && React.createElement(
|
|
2748
|
+
"div",
|
|
2749
|
+
{
|
|
2750
|
+
className: "search-results" + (searchLoading ? " search-results-blurred" : ""),
|
|
2751
|
+
ref: searchResultsContainerRef,
|
|
2752
|
+
onScroll: handleSearchResultsScroll
|
|
2753
|
+
},
|
|
2754
|
+
allResults.map(function(res, i) {
|
|
2755
|
+
var fileName = res.file.split('/').pop();
|
|
2756
|
+
return React.createElement(
|
|
2757
|
+
"div",
|
|
2758
|
+
{
|
|
2759
|
+
key: i,
|
|
2760
|
+
className: "search-result-item",
|
|
2761
|
+
onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line); }; })(res)
|
|
2762
|
+
},
|
|
2763
|
+
React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
|
|
2570
2764
|
React.createElement(
|
|
2571
|
-
"div", { className: "search-result-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
React.createElement(
|
|
2582
|
-
|
|
2765
|
+
"div", { className: "search-result-body" },
|
|
2766
|
+
React.createElement(
|
|
2767
|
+
"div", { className: "search-result-file" },
|
|
2768
|
+
fileName,
|
|
2769
|
+
React.createElement("span", { className: "search-result-line-num" }, " ", res.file, ":", res.line)
|
|
2770
|
+
),
|
|
2771
|
+
React.createElement("div", { className: "search-result-text" }, res.text)
|
|
2772
|
+
)
|
|
2773
|
+
);
|
|
2774
|
+
}),
|
|
2775
|
+
searchHasMore && React.createElement(
|
|
2776
|
+
"div", { className: "search-loading-more" },
|
|
2777
|
+
React.createElement("i", { className: "fas fa-spinner fa-spin" }),
|
|
2778
|
+
" Loading more\u2026"
|
|
2779
|
+
)
|
|
2780
|
+
),
|
|
2781
|
+
searchLoading && React.createElement(
|
|
2782
|
+
"div",
|
|
2783
|
+
{ className: "search-loading-overlay" },
|
|
2784
|
+
React.createElement("div", { className: "search-loading-spinner" })
|
|
2583
2785
|
)
|
|
2584
2786
|
)
|
|
2585
2787
|
);
|
|
@@ -2604,16 +2806,40 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2604
2806
|
if (!draggedTab) return;
|
|
2605
2807
|
e.preventDefault();
|
|
2606
2808
|
|
|
2809
|
+
// If the cursor is over the tab bar, suppress the cross-pane split overlay
|
|
2810
|
+
// so same-pane tab reordering within any tab bar is unaffected.
|
|
2811
|
+
if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
|
|
2812
|
+
if (dragOverPaneId !== null) setDragOverPaneId(null);
|
|
2813
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2607
2817
|
var rect = e.currentTarget.getBoundingClientRect();
|
|
2608
2818
|
var splitAtX = rect.left + rect.width * (dragSplitWidthRef.current / 100);
|
|
2609
2819
|
var hoverPaneId = e.clientX >= splitAtX ? 2 : 1;
|
|
2610
2820
|
var nextDropPane = hoverPaneId === draggedTab.sourcePaneId ? null : hoverPaneId;
|
|
2611
2821
|
|
|
2612
|
-
|
|
2822
|
+
// When cursor first enters the right-half content area and pane 2 is empty,
|
|
2823
|
+
// apply the 50% split width so the drop zone becomes visible.
|
|
2824
|
+
if (nextDropPane === 2) {
|
|
2825
|
+
var pane2Empty = EditorStore.getState().panes.find(function(p) { return p.id === 2; });
|
|
2826
|
+
if (!pane2Empty || pane2Empty.tabs.length === 0) {
|
|
2827
|
+
setPane1Width(50);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2613
2832
|
if (dragOverPaneId !== nextDropPane) setDragOverPaneId(nextDropPane);
|
|
2614
2833
|
},
|
|
2615
2834
|
onDropCapture: function (e) {
|
|
2616
2835
|
if (!draggedTab) return;
|
|
2836
|
+
|
|
2837
|
+
// If dropping onto a tab bar element, let the tab item's own onDrop
|
|
2838
|
+
// bubble-phase handler manage the reorder — don't intercept here.
|
|
2839
|
+
if (e.target && e.target.closest && e.target.closest('.tab-bar')) {
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2617
2843
|
e.preventDefault();
|
|
2618
2844
|
|
|
2619
2845
|
var rect = e.currentTarget.getBoundingClientRect();
|
|
@@ -2628,10 +2854,12 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
2628
2854
|
}
|
|
2629
2855
|
},
|
|
2630
2856
|
state.panes.map(function (pane, idx) {
|
|
2631
|
-
|
|
2857
|
+
// Show empty pane 2 as a drop zone only when the cursor is actively hovering
|
|
2858
|
+
// over its half of the editor content (dragOverPaneId === 2).
|
|
2859
|
+
if (pane.id === 2 && pane.tabs.length === 0 && dragOverPaneId !== 2) return null;
|
|
2632
2860
|
|
|
2633
2861
|
// Dynamic width distribution
|
|
2634
|
-
var isSplit = state.panes[1].tabs.length > 0 ||
|
|
2862
|
+
var isSplit = state.panes[1].tabs.length > 0 || dragOverPaneId === 2;
|
|
2635
2863
|
var flexBasis = '100%';
|
|
2636
2864
|
if (isSplit) flexBasis = pane.id === 1 ? pane1Width + "%" : 100 - pane1Width + "%";
|
|
2637
2865
|
|
|
@@ -3109,6 +3337,18 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3109
3337
|
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { fileTreeTypeahead: v }); }); }
|
|
3110
3338
|
})
|
|
3111
3339
|
),
|
|
3340
|
+
React.createElement(
|
|
3341
|
+
'label', { className: 'ide-settings-row ide-settings-row-half' },
|
|
3342
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Tab bar layout'),
|
|
3343
|
+
React.createElement(
|
|
3344
|
+
'select', {
|
|
3345
|
+
value: editorPrefs.tabDisplayMode || 'scroll',
|
|
3346
|
+
onChange: function(e) { var v = e.target.value; setEditorPrefs(function(p) { return Object.assign({}, p, { tabDisplayMode: v }); }); }
|
|
3347
|
+
},
|
|
3348
|
+
React.createElement('option', { value: 'scroll' }, 'Scroll'),
|
|
3349
|
+
React.createElement('option', { value: 'wrap' }, 'Wrap (multi-row)')
|
|
3350
|
+
)
|
|
3351
|
+
),
|
|
3112
3352
|
React.createElement(
|
|
3113
3353
|
'label', { className: 'ide-settings-row ide-settings-row-check' },
|
|
3114
3354
|
React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
|
|
@@ -3130,6 +3370,17 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3130
3370
|
})
|
|
3131
3371
|
),
|
|
3132
3372
|
|
|
3373
|
+
React.createElement(
|
|
3374
|
+
'label', { className: 'ide-settings-row ide-settings-row-check' },
|
|
3375
|
+
React.createElement('span', { className: 'ide-settings-label' }, 'Persist find state across files'),
|
|
3376
|
+
React.createElement('input', {
|
|
3377
|
+
type: 'checkbox',
|
|
3378
|
+
className: 'ide-settings-checkbox',
|
|
3379
|
+
checked: editorPrefs.persistFindState !== false,
|
|
3380
|
+
onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { persistFindState: v }); }); }
|
|
3381
|
+
})
|
|
3382
|
+
),
|
|
3383
|
+
|
|
3133
3384
|
/* ── RuboCop ─────────────────────────────────── */
|
|
3134
3385
|
React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
|
|
3135
3386
|
React.createElement(
|
|
@@ -3206,8 +3457,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3206
3457
|
var valNorm = val.replace(/\r\n/g, '\n');
|
|
3207
3458
|
if (valNorm === cleanNorm) {
|
|
3208
3459
|
TabManager.markClean(pane.id, pActiveTab.id, val);
|
|
3460
|
+
_clearDraft(pActiveTab.path);
|
|
3209
3461
|
} else {
|
|
3210
3462
|
TabManager.markDirty(pane.id, pActiveTab.id, val);
|
|
3463
|
+
_scheduleDraftWrite(pActiveTab.path, val);
|
|
3211
3464
|
}
|
|
3212
3465
|
}
|
|
3213
3466
|
});
|
|
@@ -3409,12 +3662,22 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3409
3662
|
state.gitInfo.behind
|
|
3410
3663
|
)
|
|
3411
3664
|
),
|
|
3412
|
-
!serverOnline &&
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3665
|
+
!serverOnline && (function () {
|
|
3666
|
+
var dirtyCount = state.panes.reduce(function (acc, p) {
|
|
3667
|
+
return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
|
|
3668
|
+
}, 0);
|
|
3669
|
+
return React.createElement(
|
|
3670
|
+
"div",
|
|
3671
|
+
{
|
|
3672
|
+
className: "statusbar-offline",
|
|
3673
|
+
title: dirtyCount > 0 ? dirtyCount + " unsaved file" + (dirtyCount !== 1 ? "s" : "") + " — changes are backed up locally" : "Server offline"
|
|
3674
|
+
},
|
|
3675
|
+
React.createElement("i", { className: "fas fa-exclamation-triangle" }),
|
|
3676
|
+
dirtyCount > 0
|
|
3677
|
+
? " Offline \u2014 " + dirtyCount + " unsaved"
|
|
3678
|
+
: " Server offline"
|
|
3679
|
+
);
|
|
3680
|
+
})(),
|
|
3418
3681
|
activeFileCommit && React.createElement(
|
|
3419
3682
|
"div",
|
|
3420
3683
|
{ className: "statusbar-file-commit", title: activeFileCommit.title + " — " + activeFileCommit.author },
|
|
@@ -3428,6 +3691,16 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3428
3691
|
{ className: "statusbar-msg " + state.statusMessage.kind },
|
|
3429
3692
|
state.statusMessage.text
|
|
3430
3693
|
),
|
|
3694
|
+
activeEOL && React.createElement(
|
|
3695
|
+
"button",
|
|
3696
|
+
{
|
|
3697
|
+
type: "button",
|
|
3698
|
+
className: "statusbar-btn statusbar-eol-btn",
|
|
3699
|
+
title: "Line endings: " + activeEOL + " — click to change",
|
|
3700
|
+
onClick: function() { handleChangeEOL(activeEOL === 'CRLF' ? 'LF' : 'CRLF'); }
|
|
3701
|
+
},
|
|
3702
|
+
activeEOL
|
|
3703
|
+
),
|
|
3431
3704
|
React.createElement(
|
|
3432
3705
|
"div",
|
|
3433
3706
|
{ className: "statusbar-version" },
|
|
@@ -3549,6 +3822,63 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
3549
3822
|
onSelectFolder: handleOpenFolderInExplorer,
|
|
3550
3823
|
onClose: function () { return setQuickOpen(false); }
|
|
3551
3824
|
}),
|
|
3825
|
+
draftRestoreOffer && React.createElement(
|
|
3826
|
+
"div",
|
|
3827
|
+
{
|
|
3828
|
+
className: "ide-draft-restore-overlay",
|
|
3829
|
+
role: "dialog",
|
|
3830
|
+
"aria-modal": "true",
|
|
3831
|
+
"aria-label": "Restore unsaved drafts"
|
|
3832
|
+
},
|
|
3833
|
+
React.createElement(
|
|
3834
|
+
"div",
|
|
3835
|
+
{ className: "ide-draft-restore-dialog" },
|
|
3836
|
+
React.createElement("div", { className: "ide-draft-restore-title" },
|
|
3837
|
+
React.createElement("i", { className: "fas fa-save", style: { marginRight: 8 } }),
|
|
3838
|
+
"Unsaved drafts found"
|
|
3839
|
+
),
|
|
3840
|
+
React.createElement("div", { className: "ide-draft-restore-body" },
|
|
3841
|
+
draftRestoreOffer.length + " file" + (draftRestoreOffer.length !== 1 ? "s have" : " has") + " locally backed-up drafts from when the server was offline:"
|
|
3842
|
+
),
|
|
3843
|
+
React.createElement(
|
|
3844
|
+
"ul",
|
|
3845
|
+
{ className: "ide-draft-restore-list" },
|
|
3846
|
+
draftRestoreOffer.map(function (o) {
|
|
3847
|
+
return React.createElement("li", { key: o.path }, o.name || o.path);
|
|
3848
|
+
})
|
|
3849
|
+
),
|
|
3850
|
+
React.createElement(
|
|
3851
|
+
"div",
|
|
3852
|
+
{ className: "ide-draft-restore-actions" },
|
|
3853
|
+
React.createElement(
|
|
3854
|
+
"button",
|
|
3855
|
+
{
|
|
3856
|
+
type: "button",
|
|
3857
|
+
className: "ide-draft-restore-btn ide-draft-restore-btn-primary",
|
|
3858
|
+
onClick: function () {
|
|
3859
|
+
draftRestoreOffer.forEach(function (offer) {
|
|
3860
|
+
TabManager.markDirty(offer.paneId, offer.tabId, offer.draftContent);
|
|
3861
|
+
});
|
|
3862
|
+
setDraftRestoreOffer(null);
|
|
3863
|
+
}
|
|
3864
|
+
},
|
|
3865
|
+
"Restore all"
|
|
3866
|
+
),
|
|
3867
|
+
React.createElement(
|
|
3868
|
+
"button",
|
|
3869
|
+
{
|
|
3870
|
+
type: "button",
|
|
3871
|
+
className: "ide-draft-restore-btn",
|
|
3872
|
+
onClick: function () {
|
|
3873
|
+
draftRestoreOffer.forEach(function (offer) { _clearDraft(offer.path); });
|
|
3874
|
+
setDraftRestoreOffer(null);
|
|
3875
|
+
}
|
|
3876
|
+
},
|
|
3877
|
+
"Discard drafts"
|
|
3878
|
+
)
|
|
3879
|
+
)
|
|
3880
|
+
)
|
|
3881
|
+
),
|
|
3552
3882
|
contextMenu && React.createElement(
|
|
3553
3883
|
React.Fragment,
|
|
3554
3884
|
null,
|