mbeditor 0.3.8 → 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.
@@ -24,6 +24,8 @@ var DEFAULT_EDITOR_PREFS = {
24
24
  theme: 'vs-dark',
25
25
  fontSize: 13,
26
26
  fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
27
+ lineHeight: 0,
28
+ letterSpacing: 0,
27
29
  tabSize: 4,
28
30
  insertSpaces: false,
29
31
  wordWrap: 'off',
@@ -32,6 +34,20 @@ var DEFAULT_EDITOR_PREFS = {
32
34
  scrollBeyondLastLine: false,
33
35
  minimap: false,
34
36
  bracketPairColorization: true,
37
+ renderLineHighlight: 'none',
38
+ cursorStyle: 'line',
39
+ cursorBlinking: 'blink',
40
+ folding: true,
41
+ smoothScrolling: false,
42
+ mouseWheelZoom: false,
43
+ autoClosingBrackets: 'always',
44
+ autoClosingQuotes: 'always',
45
+ autoIndent: 'full',
46
+ formatOnPaste: true,
47
+ formatOnType: true,
48
+ quickSuggestions: true,
49
+ wordBasedSuggestions: 'matchingDocuments',
50
+ acceptSuggestionOnEnter: 'on',
35
51
  autoRevealInExplorer: true,
36
52
  toolbarIconOnly: false,
37
53
  rubocopLintEnabled: true,
@@ -44,7 +60,8 @@ var DEFAULT_EDITOR_PREFS = {
44
60
  prettierBracketSpacing: true,
45
61
  vimMode: false,
46
62
  fileTreeTypeahead: true,
47
- quickOpenShowFolders: false
63
+ quickOpenShowFolders: false,
64
+ tabDisplayMode: 'scroll'
48
65
  };
49
66
 
50
67
  var SidebarActionButton = function SidebarActionButton(_ref) {
@@ -170,20 +187,17 @@ var MbeditorApp = function MbeditorApp() {
170
187
  var searchHasMore = _useState33h2[0];
171
188
  var setSearchHasMore = _useState33h2[1];
172
189
 
173
- var _useState33i = useState(false);
174
- var _useState33i2 = _slicedToArray(_useState33i, 2);
175
- var searchLoadingMore = _useState33i2[0];
176
- var setSearchLoadingMore = _useState33i2[1];
190
+ var _useState33tc = useState(0);
191
+ var _useState33tc2 = _slicedToArray(_useState33tc, 2);
192
+ var searchTotalCount = _useState33tc2[0];
193
+ var setSearchTotalCount = _useState33tc2[1];
177
194
 
178
- var searchOffsetRef = useRef(0); // tracks next offset to load
179
- var searchQueryRef = useRef(''); // tracks query that produced current results
180
- var searchResultsContainerRef = useRef(null);
181
- var searchVirtStartRef = useRef(0); // first visible item index (for virtual list)
195
+ var searchHasMoreRef = useRef(false);
196
+ var searchOffsetRef = useRef(0);
197
+ var searchLoadingMoreRef = useRef(false);
182
198
 
183
- var _useState33j = useState(0);
184
- var _useState33j2 = _slicedToArray(_useState33j, 2);
185
- var searchVirtStart = _useState33j2[0];
186
- var setSearchVirtStart = _useState33j2[1];
199
+ var searchQueryRef = useRef('');
200
+ var searchResultsContainerRef = useRef(null);
187
201
 
188
202
  var _useState8 = useState("explorer");
189
203
 
@@ -404,6 +418,36 @@ var MbeditorApp = function MbeditorApp() {
404
418
  var prevGitBranchRef = useRef(null);
405
419
  var isSwitchingBranchRef = useRef(false);
406
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
+
407
451
  var clamp = function clamp(value, min, max) {
408
452
  return Math.min(max, Math.max(min, value));
409
453
  };
@@ -924,6 +968,28 @@ var MbeditorApp = function MbeditorApp() {
924
968
  return function () { clearTimeout(timeoutId); };
925
969
  }, []);
926
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
+
927
993
  // Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
928
994
  // Uses functional setTreeData to skip the re-render when nothing has changed.
929
995
  useEffect(function () {
@@ -1235,6 +1301,7 @@ var MbeditorApp = function MbeditorApp() {
1235
1301
  });
1236
1302
  EditorStore.setState({ panes: newPanes });
1237
1303
  EditorStore.setStatus("Saved", "success");
1304
+ _clearDraft(tab.path);
1238
1305
 
1239
1306
  // Hot reload for Markdown: sync preview tab after save
1240
1307
  if (/\.(md|markdown)$/i.test(tab.path)) {
@@ -1289,16 +1356,21 @@ var MbeditorApp = function MbeditorApp() {
1289
1356
  var pane2 = EditorStore.getState().panes.find(function (p) {
1290
1357
  return p.id === 2;
1291
1358
  });
1292
- if (!pane2 || pane2.tabs.length === 0) {
1293
- dragSplitWidthRef.current = 50;
1294
- setPane1Width(50);
1295
- } else {
1296
- dragSplitWidthRef.current = pane1Width;
1297
- }
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;
1298
1364
  setDraggedTab({ sourcePaneId: sourcePaneId, tabId: tabId });
1299
1365
  };
1300
1366
 
1301
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
+ }
1302
1374
  setDraggedTab(null);
1303
1375
  setDragOverPaneId(null);
1304
1376
  };
@@ -1481,40 +1553,61 @@ var MbeditorApp = function MbeditorApp() {
1481
1553
  var onFormatRef = useRef(handleFormat);
1482
1554
  onFormatRef.current = handleFormat;
1483
1555
 
1556
+ // Eagerly load all remaining pages sequentially in the background.
1557
+ // Self-chains via .then() so only one request is in-flight at a time.
1558
+ // Uses only refs so it's safe to call from async callbacks without
1559
+ // worrying about stale closure state.
1484
1560
  var _debouncedSearch = useRef(window._.debounce(function (q) {
1485
1561
  if (!q.trim()) {
1486
1562
  searchRequestIdRef.current += 1;
1487
1563
  setSearchLoading(false);
1488
- setSearchHasMore(false);
1564
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1565
+ setSearchTotalCount(0);
1489
1566
  searchOffsetRef.current = 0;
1567
+ searchLoadingMoreRef.current = false;
1490
1568
  searchQueryRef.current = '';
1491
1569
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1492
1570
  return;
1493
1571
  }
1494
1572
  var requestId = ++searchRequestIdRef.current;
1495
1573
  setSearchLoading(true);
1496
- setSearchHasMore(false);
1574
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1575
+ setSearchTotalCount(0);
1497
1576
  searchOffsetRef.current = 0;
1577
+ searchLoadingMoreRef.current = false;
1498
1578
  searchQueryRef.current = q;
1499
- setSearchVirtStart(0);
1500
- searchVirtStartRef.current = 0;
1501
1579
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1502
1580
  EditorStore.setStatus("Searching project...", "info");
1503
1581
  SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE).then(function (res) {
1504
- if (searchRequestIdRef.current === requestId) {
1505
- var hasMore = !!(res && res.hasMore);
1506
- setSearchHasMore(hasMore);
1507
- searchOffsetRef.current = SearchService.PAGE_SIZE;
1508
- var count = res && res.results ? res.results.length : 0;
1509
- EditorStore.setStatus("Found " + count + (hasMore ? '+' : '') + " result" + (count !== 1 ? "s" : ""), "success");
1510
- }
1582
+ if (searchRequestIdRef.current !== requestId) return;
1583
+ var hasMore = !!(res && res.hasMore);
1584
+ setSearchHasMore(hasMore); searchHasMoreRef.current = hasMore;
1585
+ searchOffsetRef.current = SearchService.PAGE_SIZE;
1586
+ if (res && res.totalCount != null) setSearchTotalCount(res.totalCount);
1587
+ var total = (res && res.totalCount != null) ? res.totalCount : (res && res.results ? res.results.length : 0);
1588
+ EditorStore.setStatus("Found " + total + (hasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""), "success");
1511
1589
  }).finally(function () {
1512
- if (searchRequestIdRef.current === requestId) {
1513
- setSearchLoading(false);
1514
- }
1590
+ if (searchRequestIdRef.current === requestId) setSearchLoading(false);
1515
1591
  });
1516
1592
  }, 400)).current;
1517
1593
 
1594
+ var loadMoreSearchResults = function loadMoreSearchResults() {
1595
+ var q = searchQueryRef.current;
1596
+ if (!q || searchLoadingMoreRef.current || !searchHasMoreRef.current) return;
1597
+ searchLoadingMoreRef.current = true;
1598
+ var offset = searchOffsetRef.current;
1599
+ SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE).then(function(res) {
1600
+ if (searchQueryRef.current !== q) { searchLoadingMoreRef.current = false; return; }
1601
+ var hasMore = !!(res && res.hasMore);
1602
+ searchHasMoreRef.current = hasMore;
1603
+ setSearchHasMore(hasMore);
1604
+ searchOffsetRef.current = offset + SearchService.PAGE_SIZE;
1605
+ searchLoadingMoreRef.current = false;
1606
+ }).catch(function() {
1607
+ searchLoadingMoreRef.current = false;
1608
+ });
1609
+ };
1610
+
1518
1611
  var handleSearchChange = function handleSearchChange(e) {
1519
1612
  var val = e.target.value;
1520
1613
  if (!val) { clearSearch(); return; }
@@ -1527,12 +1620,11 @@ var MbeditorApp = function MbeditorApp() {
1527
1620
  if (_debouncedSearch.cancel) _debouncedSearch.cancel();
1528
1621
  setSearchQuery("");
1529
1622
  setSearchLoading(false);
1530
- setSearchHasMore(false);
1531
- setSearchLoadingMore(false);
1623
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1624
+ setSearchTotalCount(0);
1532
1625
  searchOffsetRef.current = 0;
1626
+ searchLoadingMoreRef.current = false;
1533
1627
  searchQueryRef.current = '';
1534
- setSearchVirtStart(0);
1535
- searchVirtStartRef.current = 0;
1536
1628
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1537
1629
  };
1538
1630
 
@@ -1541,35 +1633,11 @@ var MbeditorApp = function MbeditorApp() {
1541
1633
  _debouncedSearch(searchQuery);
1542
1634
  };
1543
1635
 
1544
- var loadMoreSearchResults = function loadMoreSearchResults() {
1545
- var q = searchQueryRef.current;
1546
- if (!q || searchLoadingMore || !searchHasMore) return;
1547
- var offset = searchOffsetRef.current;
1548
- setSearchLoadingMore(true);
1549
- SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE).then(function (res) {
1550
- var hasMore = !!(res && res.hasMore);
1551
- setSearchHasMore(hasMore);
1552
- searchOffsetRef.current = offset + SearchService.PAGE_SIZE;
1553
- }).finally(function () {
1554
- setSearchLoadingMore(false);
1555
- });
1556
- };
1557
-
1558
- // Scroll handler for the virtualized search results list.
1559
- var SEARCH_ITEM_H = 40;
1560
- var SEARCH_OVERSCAN = 8;
1636
+ // Load more results when the user scrolls near the bottom of the list.
1561
1637
  var handleSearchResultsScroll = function handleSearchResultsScroll(e) {
1562
1638
  var el = e.currentTarget;
1563
- var newStart = Math.floor(el.scrollTop / SEARCH_ITEM_H);
1564
- if (Math.abs(newStart - searchVirtStartRef.current) >= 4) {
1565
- searchVirtStartRef.current = newStart;
1566
- setSearchVirtStart(newStart);
1567
- }
1568
- // Trigger load-more when within 200px of the bottom
1569
- if (!searchLoadingMore && searchHasMore) {
1570
- if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
1571
- loadMoreSearchResults();
1572
- }
1639
+ if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
1640
+ loadMoreSearchResults();
1573
1641
  }
1574
1642
  };
1575
1643
 
@@ -2053,6 +2121,7 @@ var MbeditorApp = function MbeditorApp() {
2053
2121
  tabs: tabs,
2054
2122
  activeId: activeId,
2055
2123
  paneId: paneId,
2124
+ tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
2056
2125
  onSelect: function (id) {
2057
2126
  // Sync explorer selection with the newly active tab so there's only one highlight
2058
2127
  var tab = tabs.find(function(t) { return t.id === id; });
@@ -2523,88 +2592,58 @@ var MbeditorApp = function MbeditorApp() {
2523
2592
  React.createElement("i", { className: searchLoading ? "fas fa-spinner fa-spin" : "fas fa-search" })
2524
2593
  )
2525
2594
  ),
2526
- React.createElement(
2527
- "div",
2528
- {
2529
- className: "search-results",
2530
- ref: searchResultsContainerRef,
2531
- onScroll: handleSearchResultsScroll
2532
- },
2533
- (function() {
2534
- var allResults = state.searchResults || [];
2535
- var totalCount = allResults.length;
2595
+ (function() {
2596
+ var allResults = state.searchResults || [];
2597
+ var loadedCount = allResults.length;
2598
+ var total = searchTotalCount > 0 ? searchTotalCount : loadedCount;
2599
+ var hasAny = loadedCount > 0;
2536
2600
 
2537
- if (searchQuery && totalCount === 0 && !searchLoading) {
2538
- return React.createElement(
2539
- "div", { className: "search-results-empty" }, "No results"
2540
- );
2541
- }
2542
-
2543
- if (totalCount === 0) return null;
2544
-
2545
- // Compute virtual window
2546
- var containerEl = searchResultsContainerRef.current;
2547
- var containerH = containerEl ? containerEl.clientHeight : 400;
2548
- var visibleCount = Math.ceil(containerH / SEARCH_ITEM_H) + SEARCH_OVERSCAN * 2;
2549
- var virtStart = Math.max(0, searchVirtStart - SEARCH_OVERSCAN);
2550
- var virtEnd = Math.min(totalCount, virtStart + visibleCount);
2551
- var paddingTop = virtStart * SEARCH_ITEM_H;
2552
- var paddingBottom = Math.max(0, (totalCount - virtEnd) * SEARCH_ITEM_H);
2553
-
2554
- var visibleItems = allResults.slice(virtStart, virtEnd).map(function(res, idx) {
2555
- var absoluteIdx = virtStart + idx;
2556
- var fileName = res.file.split('/').pop();
2557
- return React.createElement(
2558
- "div",
2559
- {
2560
- key: absoluteIdx,
2561
- className: "search-result-item",
2562
- onClick: function() { handleSelectFile(res.file, res.file.split('/').pop(), res.line); }
2563
- },
2564
- React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
2565
- React.createElement(
2601
+ return React.createElement(
2602
+ React.Fragment,
2603
+ null,
2604
+ searchQuery && !searchLoading && React.createElement(
2605
+ "div",
2606
+ { className: "search-results-header" },
2607
+ hasAny
2608
+ ? (total + (searchHasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""))
2609
+ : "No results"
2610
+ ),
2611
+ hasAny && React.createElement(
2612
+ "div",
2613
+ {
2614
+ className: "search-results",
2615
+ ref: searchResultsContainerRef,
2616
+ onScroll: handleSearchResultsScroll
2617
+ },
2618
+ allResults.map(function(res, i) {
2619
+ var fileName = res.file.split('/').pop();
2620
+ return React.createElement(
2566
2621
  "div",
2567
- { className: "search-result-body" },
2622
+ {
2623
+ key: i,
2624
+ className: "search-result-item",
2625
+ onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line); }; })(res)
2626
+ },
2627
+ React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
2568
2628
  React.createElement(
2569
- "div",
2570
- { className: "search-result-file" },
2571
- fileName,
2629
+ "div", { className: "search-result-body" },
2572
2630
  React.createElement(
2573
- "span",
2574
- { className: "search-result-line-num" },
2575
- " ", res.file, ":", res.line
2576
- )
2577
- ),
2578
- React.createElement(
2579
- "div",
2580
- { className: "search-result-text" },
2581
- res.text
2631
+ "div", { className: "search-result-file" },
2632
+ fileName,
2633
+ React.createElement("span", { className: "search-result-line-num" }, " ", res.file, ":", res.line)
2634
+ ),
2635
+ React.createElement("div", { className: "search-result-text" }, res.text)
2582
2636
  )
2583
- )
2584
- );
2585
- });
2586
-
2587
- return React.createElement(
2588
- React.Fragment,
2589
- null,
2590
- React.createElement(
2591
- "div",
2592
- { className: "search-results-meta" },
2593
- totalCount,
2594
- " result" + (totalCount !== 1 ? "s" : ""),
2595
- searchHasMore && React.createElement("span", { className: "search-results-capped" }, " — more available, scroll for next page")
2596
- ),
2597
- React.createElement("div", { style: { height: paddingTop + 'px', flexShrink: 0 } }),
2598
- visibleItems,
2599
- React.createElement("div", { style: { height: paddingBottom + 'px', flexShrink: 0 } }),
2600
- searchLoadingMore && React.createElement(
2601
- "div",
2602
- { className: "search-results-loading-more", 'aria-busy': 'true' },
2603
- 'Loading more…'
2637
+ );
2638
+ }),
2639
+ searchHasMore && React.createElement(
2640
+ "div", { className: "search-loading-more" },
2641
+ React.createElement("i", { className: "fas fa-spinner fa-spin" }),
2642
+ " Loading more\u2026"
2604
2643
  )
2605
- );
2606
- })()
2607
- )
2644
+ )
2645
+ );
2646
+ })()
2608
2647
  )
2609
2648
  )
2610
2649
  ),
@@ -2625,16 +2664,40 @@ var MbeditorApp = function MbeditorApp() {
2625
2664
  if (!draggedTab) return;
2626
2665
  e.preventDefault();
2627
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
+
2628
2675
  var rect = e.currentTarget.getBoundingClientRect();
2629
2676
  var splitAtX = rect.left + rect.width * (dragSplitWidthRef.current / 100);
2630
2677
  var hoverPaneId = e.clientX >= splitAtX ? 2 : 1;
2631
2678
  var nextDropPane = hoverPaneId === draggedTab.sourcePaneId ? null : hoverPaneId;
2632
2679
 
2633
- e.dataTransfer.dropEffect = nextDropPane ? 'move' : 'none';
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';
2634
2690
  if (dragOverPaneId !== nextDropPane) setDragOverPaneId(nextDropPane);
2635
2691
  },
2636
2692
  onDropCapture: function (e) {
2637
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
+
2638
2701
  e.preventDefault();
2639
2702
 
2640
2703
  var rect = e.currentTarget.getBoundingClientRect();
@@ -2649,10 +2712,12 @@ var MbeditorApp = function MbeditorApp() {
2649
2712
  }
2650
2713
  },
2651
2714
  state.panes.map(function (pane, idx) {
2652
- if (pane.id === 2 && pane.tabs.length === 0 && !draggedTab) return null; // Show pane 2 while dragging to allow drop-to-split
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;
2653
2718
 
2654
2719
  // Dynamic width distribution
2655
- var isSplit = state.panes[1].tabs.length > 0 || !!draggedTab;
2720
+ var isSplit = state.panes[1].tabs.length > 0 || dragOverPaneId === 2;
2656
2721
  var flexBasis = '100%';
2657
2722
  if (isSplit) flexBasis = pane.id === 1 ? pane1Width + "%" : 100 - pane1Width + "%";
2658
2723
 
@@ -2735,6 +2800,32 @@ var MbeditorApp = function MbeditorApp() {
2735
2800
  onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { fontFamily: e.target.value }); }); }
2736
2801
  })
2737
2802
  ),
2803
+ React.createElement(
2804
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Row height in pixels. 0 = auto (roughly font size × 1.5)' },
2805
+ React.createElement('span', { className: 'ide-settings-label' }, 'Line height (0=auto)'),
2806
+ React.createElement('input', {
2807
+ type: 'number', min: '0', max: '100', step: '1',
2808
+ className: 'ide-settings-input',
2809
+ value: editorPrefs.lineHeight != null ? editorPrefs.lineHeight : 0,
2810
+ onChange: function(e) {
2811
+ var v = parseInt(e.target.value, 10);
2812
+ if (!isNaN(v) && v >= 0 && v <= 100) setEditorPrefs(function(p) { return Object.assign({}, p, { lineHeight: v }); });
2813
+ }
2814
+ })
2815
+ ),
2816
+ React.createElement(
2817
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Extra space between characters in pixels. 0 = default' },
2818
+ React.createElement('span', { className: 'ide-settings-label' }, 'Letter spacing (px)'),
2819
+ React.createElement('input', {
2820
+ type: 'number', min: '-5', max: '20', step: '0.5',
2821
+ className: 'ide-settings-input',
2822
+ value: editorPrefs.letterSpacing != null ? editorPrefs.letterSpacing : 0,
2823
+ onChange: function(e) {
2824
+ var v = parseFloat(e.target.value);
2825
+ if (!isNaN(v) && v >= -5 && v <= 20) setEditorPrefs(function(p) { return Object.assign({}, p, { letterSpacing: v }); });
2826
+ }
2827
+ })
2828
+ ),
2738
2829
 
2739
2830
  /* ── Indentation (unified editor + Prettier) ── */
2740
2831
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Indentation'),
@@ -2844,6 +2935,184 @@ var MbeditorApp = function MbeditorApp() {
2844
2935
  onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { vimMode: v }); }); }
2845
2936
  })
2846
2937
  ),
2938
+ React.createElement(
2939
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing bracket automatically' },
2940
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close brackets'),
2941
+ React.createElement(
2942
+ 'select', {
2943
+ value: editorPrefs.autoClosingBrackets || 'always',
2944
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingBrackets: e.target.value }); }); }
2945
+ },
2946
+ React.createElement('option', { value: 'always' }, 'Always'),
2947
+ React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
2948
+ React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
2949
+ React.createElement('option', { value: 'never' }, 'Never')
2950
+ )
2951
+ ),
2952
+ React.createElement(
2953
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing quote automatically' },
2954
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close quotes'),
2955
+ React.createElement(
2956
+ 'select', {
2957
+ value: editorPrefs.autoClosingQuotes || 'always',
2958
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingQuotes: e.target.value }); }); }
2959
+ },
2960
+ React.createElement('option', { value: 'always' }, 'Always'),
2961
+ React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
2962
+ React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
2963
+ React.createElement('option', { value: 'never' }, 'Never')
2964
+ )
2965
+ ),
2966
+ React.createElement(
2967
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'What to highlight on the current editor line' },
2968
+ React.createElement('span', { className: 'ide-settings-label' }, 'Line highlight'),
2969
+ React.createElement(
2970
+ 'select', {
2971
+ value: editorPrefs.renderLineHighlight || 'none',
2972
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { renderLineHighlight: e.target.value }); }); }
2973
+ },
2974
+ React.createElement('option', { value: 'none' }, 'None'),
2975
+ React.createElement('option', { value: 'gutter' }, 'Line number only'),
2976
+ React.createElement('option', { value: 'line' }, 'Current line background'),
2977
+ React.createElement('option', { value: 'all' }, 'Line number + background')
2978
+ )
2979
+ ),
2980
+ React.createElement(
2981
+ 'label', { className: 'ide-settings-row ide-settings-row-half' },
2982
+ React.createElement('span', { className: 'ide-settings-label' }, 'Cursor style'),
2983
+ React.createElement(
2984
+ 'select', {
2985
+ value: editorPrefs.cursorStyle || 'line',
2986
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorStyle: e.target.value }); }); }
2987
+ },
2988
+ React.createElement('option', { value: 'line' }, 'Line (|)'),
2989
+ React.createElement('option', { value: 'block' }, 'Block (filled)'),
2990
+ React.createElement('option', { value: 'underline' }, 'Underline (_)'),
2991
+ React.createElement('option', { value: 'line-thin' }, 'Line thin'),
2992
+ React.createElement('option', { value: 'block-outline' }, 'Block outline'),
2993
+ React.createElement('option', { value: 'underline-thin' }, 'Underline thin')
2994
+ )
2995
+ ),
2996
+ React.createElement(
2997
+ 'label', { className: 'ide-settings-row ide-settings-row-half' },
2998
+ React.createElement('span', { className: 'ide-settings-label' }, 'Cursor blinking'),
2999
+ React.createElement(
3000
+ 'select', {
3001
+ value: editorPrefs.cursorBlinking || 'blink',
3002
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorBlinking: e.target.value }); }); }
3003
+ },
3004
+ React.createElement('option', { value: 'blink' }, 'Blink (on/off)'),
3005
+ React.createElement('option', { value: 'smooth' }, 'Smooth (fade)'),
3006
+ React.createElement('option', { value: 'phase' }, 'Phase (offset fade)'),
3007
+ React.createElement('option', { value: 'expand' }, 'Expand (grow/shrink)'),
3008
+ React.createElement('option', { value: 'solid' }, 'Solid (no blink)')
3009
+ )
3010
+ ),
3011
+ React.createElement(
3012
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show collapse arrows next to foldable regions (functions, classes, blocks)' },
3013
+ React.createElement('span', { className: 'ide-settings-label' }, 'Code folding'),
3014
+ React.createElement('input', {
3015
+ type: 'checkbox',
3016
+ className: 'ide-settings-checkbox',
3017
+ checked: editorPrefs.folding !== false,
3018
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { folding: v }); }); }
3019
+ })
3020
+ ),
3021
+ React.createElement(
3022
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Animate scrolling instead of jumping instantly' },
3023
+ React.createElement('span', { className: 'ide-settings-label' }, 'Smooth scrolling'),
3024
+ React.createElement('input', {
3025
+ type: 'checkbox',
3026
+ className: 'ide-settings-checkbox',
3027
+ checked: !!(editorPrefs.smoothScrolling),
3028
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { smoothScrolling: v }); }); }
3029
+ })
3030
+ ),
3031
+ React.createElement(
3032
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Hold Ctrl (or Cmd) and scroll the mouse wheel to zoom the font size' },
3033
+ React.createElement('span', { className: 'ide-settings-label' }, 'Ctrl+scroll to zoom'),
3034
+ React.createElement('input', {
3035
+ type: 'checkbox',
3036
+ className: 'ide-settings-checkbox',
3037
+ checked: !!(editorPrefs.mouseWheelZoom),
3038
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { mouseWheelZoom: v }); }); }
3039
+ })
3040
+ ),
3041
+
3042
+ /* ── Behaviour ───────────────────────────────── */
3043
+ React.createElement('div', { className: 'ide-settings-section-header' }, 'Behaviour'),
3044
+ React.createElement(
3045
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'How aggressively the editor re-indents lines as you type' },
3046
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto indent'),
3047
+ React.createElement(
3048
+ 'select', {
3049
+ value: editorPrefs.autoIndent || 'full',
3050
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoIndent: e.target.value }); }); }
3051
+ },
3052
+ React.createElement('option', { value: 'none' }, 'None (disabled)'),
3053
+ React.createElement('option', { value: 'keep' }, 'Keep current level'),
3054
+ React.createElement('option', { value: 'brackets' }, 'Indent on { and ['),
3055
+ React.createElement('option', { value: 'advanced' }, 'Language indent rules'),
3056
+ React.createElement('option', { value: 'full' }, 'Full (language grammar)')
3057
+ )
3058
+ ),
3059
+ React.createElement(
3060
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Whether pressing Enter accepts the highlighted autocomplete suggestion' },
3061
+ React.createElement('span', { className: 'ide-settings-label' }, 'Accept suggestion on Enter'),
3062
+ React.createElement(
3063
+ 'select', {
3064
+ value: editorPrefs.acceptSuggestionOnEnter || 'on',
3065
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { acceptSuggestionOnEnter: e.target.value }); }); }
3066
+ },
3067
+ React.createElement('option', { value: 'on' }, 'Always'),
3068
+ React.createElement('option', { value: 'smart' }, 'Only when navigated (↑↓)'),
3069
+ React.createElement('option', { value: 'off' }, 'Never (Tab only)')
3070
+ )
3071
+ ),
3072
+ React.createElement(
3073
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Suggest completions based on words already present in open files' },
3074
+ React.createElement('span', { className: 'ide-settings-label' }, 'Word-based suggestions'),
3075
+ React.createElement(
3076
+ 'select', {
3077
+ value: editorPrefs.wordBasedSuggestions || 'matchingDocuments',
3078
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { wordBasedSuggestions: e.target.value }); }); }
3079
+ },
3080
+ React.createElement('option', { value: 'off' }, 'Off'),
3081
+ React.createElement('option', { value: 'currentDocument' }, 'Current file only'),
3082
+ React.createElement('option', { value: 'matchingDocuments' }, 'Same language files'),
3083
+ React.createElement('option', { value: 'allDocuments' }, 'All open files')
3084
+ )
3085
+ ),
3086
+ React.createElement(
3087
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Auto-format pasted code using the language formatter' },
3088
+ React.createElement('span', { className: 'ide-settings-label' }, 'Format on paste'),
3089
+ React.createElement('input', {
3090
+ type: 'checkbox',
3091
+ className: 'ide-settings-checkbox',
3092
+ checked: editorPrefs.formatOnPaste !== false,
3093
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnPaste: v }); }); }
3094
+ })
3095
+ ),
3096
+ React.createElement(
3097
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Re-indent and auto-close blocks as you type (e.g. after pressing Enter inside {})' },
3098
+ React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
3099
+ React.createElement('input', {
3100
+ type: 'checkbox',
3101
+ className: 'ide-settings-checkbox',
3102
+ checked: editorPrefs.formatOnType !== false,
3103
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnType: v }); }); }
3104
+ })
3105
+ ),
3106
+ React.createElement(
3107
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show autocomplete suggestions while typing (not just on trigger characters like .)' },
3108
+ React.createElement('span', { className: 'ide-settings-label' }, 'Quick suggestions'),
3109
+ React.createElement('input', {
3110
+ type: 'checkbox',
3111
+ className: 'ide-settings-checkbox',
3112
+ checked: editorPrefs.quickSuggestions !== false,
3113
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { quickSuggestions: v }); }); }
3114
+ })
3115
+ ),
2847
3116
 
2848
3117
  /* ── Formatting (Prettier) ───────────────────── */
2849
3118
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Formatting'),
@@ -2926,6 +3195,18 @@ var MbeditorApp = function MbeditorApp() {
2926
3195
  onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { fileTreeTypeahead: v }); }); }
2927
3196
  })
2928
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
+ ),
2929
3210
  React.createElement(
2930
3211
  'label', { className: 'ide-settings-row ide-settings-row-check' },
2931
3212
  React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
@@ -3023,8 +3304,10 @@ var MbeditorApp = function MbeditorApp() {
3023
3304
  var valNorm = val.replace(/\r\n/g, '\n');
3024
3305
  if (valNorm === cleanNorm) {
3025
3306
  TabManager.markClean(pane.id, pActiveTab.id, val);
3307
+ _clearDraft(pActiveTab.path);
3026
3308
  } else {
3027
3309
  TabManager.markDirty(pane.id, pActiveTab.id, val);
3310
+ _scheduleDraftWrite(pActiveTab.path, val);
3028
3311
  }
3029
3312
  }
3030
3313
  });
@@ -3226,12 +3509,22 @@ var MbeditorApp = function MbeditorApp() {
3226
3509
  state.gitInfo.behind
3227
3510
  )
3228
3511
  ),
3229
- !serverOnline && React.createElement(
3230
- "div",
3231
- { className: "statusbar-offline" },
3232
- React.createElement("i", { className: "fas fa-exclamation-triangle" }),
3233
- " Server offline"
3234
- ),
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
+ })(),
3235
3528
  activeFileCommit && React.createElement(
3236
3529
  "div",
3237
3530
  { className: "statusbar-file-commit", title: activeFileCommit.title + " — " + activeFileCommit.author },
@@ -3366,6 +3659,63 @@ var MbeditorApp = function MbeditorApp() {
3366
3659
  onSelectFolder: handleOpenFolderInExplorer,
3367
3660
  onClose: function () { return setQuickOpen(false); }
3368
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
+ ),
3369
3719
  contextMenu && React.createElement(
3370
3720
  React.Fragment,
3371
3721
  null,