mbeditor 0.3.9 → 0.4.2

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.
@@ -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
- if (!pane2 || pane2.tabs.length === 0) {
1306
- dragSplitWidthRef.current = 50;
1307
- setPane1Width(50);
1308
- } else {
1309
- dragSplitWidthRef.current = pane1Width;
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-wrap" },
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-shell" },
2511
- React.createElement("input", {
2512
- className: "search-input",
2513
- placeholder: "Find in files…",
2514
- value: searchQuery,
2515
- onChange: handleSearchChange
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-clear-btn",
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
- hasAny && React.createElement(
2744
+ React.createElement(
2552
2745
  "div",
2553
- {
2554
- className: "search-results",
2555
- ref: searchResultsContainerRef,
2556
- onScroll: handleSearchResultsScroll
2557
- },
2558
- allResults.map(function(res, i) {
2559
- var fileName = res.file.split('/').pop();
2560
- return React.createElement(
2561
- "div",
2562
- {
2563
- key: i,
2564
- className: "search-result-item",
2565
- onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line); }; })(res)
2566
- },
2567
- React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
2568
- React.createElement(
2569
- "div", { className: "search-result-body" },
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-file" },
2572
- fileName,
2573
- React.createElement("span", { className: "search-result-line-num" }, " ", res.file, ":", res.line)
2574
- ),
2575
- React.createElement("div", { className: "search-result-text" }, res.text)
2576
- )
2577
- );
2578
- }),
2579
- searchHasMore && React.createElement(
2580
- "div", { className: "search-loading-more" },
2581
- React.createElement("i", { className: "fas fa-spinner fa-spin" }),
2582
- " Loading more\u2026"
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
- e.dataTransfer.dropEffect = nextDropPane ? 'move' : 'none';
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
- if (pane.id === 2 && pane.tabs.length === 0 && !draggedTab) return null; // Show pane 2 while dragging to allow drop-to-split
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 || !!draggedTab;
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 && React.createElement(
3413
- "div",
3414
- { className: "statusbar-offline" },
3415
- React.createElement("i", { className: "fas fa-exclamation-triangle" }),
3416
- " Server offline"
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,