mbeditor 0.3.8 → 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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/app/assets/javascripts/mbeditor/application.js +1 -0
  4. data/app/assets/javascripts/mbeditor/application_iife_head.js +7 -0
  5. data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +1 -1
  6. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +213 -11
  7. data/app/assets/javascripts/mbeditor/components/GitPanel.js +14 -4
  8. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +673 -160
  9. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +41 -1
  10. data/app/assets/javascripts/mbeditor/components/TabBar.js +3 -2
  11. data/app/assets/javascripts/mbeditor/editor_plugins.js +21 -0
  12. data/app/assets/javascripts/mbeditor/editor_store.js +10 -2
  13. data/app/assets/javascripts/mbeditor/file_service.js +29 -23
  14. data/app/assets/javascripts/mbeditor/git_service.js +7 -11
  15. data/app/assets/javascripts/mbeditor/search_service.js +51 -14
  16. data/app/assets/javascripts/mbeditor/tab_manager.js +3 -3
  17. data/app/assets/javascripts/mbeditor/websocket_service.js +126 -0
  18. data/app/assets/stylesheets/mbeditor/editor.css +237 -15
  19. data/app/channels/mbeditor/editor_channel.rb +79 -0
  20. data/app/controllers/mbeditor/editors_controller.rb +177 -136
  21. data/app/controllers/mbeditor/git_controller.rb +5 -40
  22. data/app/services/mbeditor/git_blame_service.rb +6 -0
  23. data/app/services/mbeditor/git_commit_graph_service.rb +2 -0
  24. data/app/services/mbeditor/git_service.rb +97 -28
  25. data/app/services/mbeditor/redmine_service.rb +7 -0
  26. data/app/services/mbeditor/ruby_definition_service.rb +23 -2
  27. data/app/views/layouts/mbeditor/application.html.erb +4 -0
  28. data/lib/mbeditor/cable_log_filter.rb +28 -0
  29. data/lib/mbeditor/configuration.rb +7 -1
  30. data/lib/mbeditor/engine.rb +37 -0
  31. data/lib/mbeditor/rack/silence_ping_request.rb +4 -1
  32. data/lib/mbeditor/version.rb +3 -1
  33. data/lib/mbeditor.rb +2 -0
  34. metadata +5 -2
@@ -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,9 @@ 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',
65
+ persistFindState: true
48
66
  };
49
67
 
50
68
  var SidebarActionButton = function SidebarActionButton(_ref) {
@@ -170,20 +188,35 @@ var MbeditorApp = function MbeditorApp() {
170
188
  var searchHasMore = _useState33h2[0];
171
189
  var setSearchHasMore = _useState33h2[1];
172
190
 
173
- var _useState33i = useState(false);
174
- var _useState33i2 = _slicedToArray(_useState33i, 2);
175
- var searchLoadingMore = _useState33i2[0];
176
- var setSearchLoadingMore = _useState33i2[1];
177
-
178
- var searchOffsetRef = useRef(0); // tracks next offset to load
179
- var searchQueryRef = useRef(''); // tracks query that produced current results
191
+ var _useState33tc = useState(0);
192
+ var _useState33tc2 = _slicedToArray(_useState33tc, 2);
193
+ var searchTotalCount = _useState33tc2[0];
194
+ var setSearchTotalCount = _useState33tc2[1];
195
+
196
+ var searchHasMoreRef = useRef(false);
197
+ var searchOffsetRef = useRef(0);
198
+ var searchLoadingMoreRef = useRef(false);
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
+
215
+ var searchQueryRef = useRef('');
216
+ var searchUseRegexRef = useRef(false);
217
+ var searchMatchCaseRef = useRef(false);
218
+ var searchWholeWordRef = useRef(false);
180
219
  var searchResultsContainerRef = useRef(null);
181
- var searchVirtStartRef = useRef(0); // first visible item index (for virtual list)
182
-
183
- var _useState33j = useState(0);
184
- var _useState33j2 = _slicedToArray(_useState33j, 2);
185
- var searchVirtStart = _useState33j2[0];
186
- var setSearchVirtStart = _useState33j2[1];
187
220
 
188
221
  var _useState8 = useState("explorer");
189
222
 
@@ -404,6 +437,36 @@ var MbeditorApp = function MbeditorApp() {
404
437
  var prevGitBranchRef = useRef(null);
405
438
  var isSwitchingBranchRef = useRef(false);
406
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
+
407
470
  var clamp = function clamp(value, min, max) {
408
471
  return Math.min(max, Math.max(min, value));
409
472
  };
@@ -621,6 +684,9 @@ var MbeditorApp = function MbeditorApp() {
621
684
  if (workspace && typeof workspace.testAvailable === 'boolean') {
622
685
  setTestAvailable(workspace.testAvailable);
623
686
  }
687
+ if (workspace && typeof workspace.actionCableEnabled === 'boolean') {
688
+ WebSocketService.connect(workspace.actionCableEnabled);
689
+ }
624
690
  });
625
691
 
626
692
  // Helper: load tab content for a set of panes and restore them into EditorStore
@@ -924,11 +990,55 @@ var MbeditorApp = function MbeditorApp() {
924
990
  return function () { clearTimeout(timeoutId); };
925
991
  }, []);
926
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
+
927
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.
928
1037
  // Uses functional setTreeData to skip the re-render when nothing has changed.
929
1038
  useEffect(function () {
930
1039
  var intervalId = setInterval(function () {
931
1040
  if (document.hidden) return;
1041
+ if (WebSocketService.isConnected()) return; // WebSocket is handling refreshes
932
1042
  // Refresh tree and check for git branch changes (to trigger per-branch tab state swap)
933
1043
  GitService.fetchStatus()["catch"](function () {});
934
1044
  FileService.getTree().then(function (data) {
@@ -1132,6 +1242,12 @@ var MbeditorApp = function MbeditorApp() {
1132
1242
  var activeFileCommit = _useState32[0];
1133
1243
  var setActiveFileCommit = _useState32[1];
1134
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
+
1135
1251
  useEffect(function () {
1136
1252
  if (!gitAvailable || !activeTab || activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph || !activeTab.path || activeTab.path.indexOf('diff://') === 0 || activeTab.path.indexOf('combined-diff://') === 0) {
1137
1253
  setActiveFileCommit(null);
@@ -1150,6 +1266,22 @@ var MbeditorApp = function MbeditorApp() {
1150
1266
  });
1151
1267
  }, [activeTab ? activeTab.id : null, gitAvailable]);
1152
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
+
1153
1285
  useEffect(function () {
1154
1286
  if (!activeTab || typeof activeTab.content !== 'string') return;
1155
1287
  if (activeTab.isDiff || activeTab.isCombinedDiff || activeTab.isCommitGraph) return;
@@ -1235,6 +1367,7 @@ var MbeditorApp = function MbeditorApp() {
1235
1367
  });
1236
1368
  EditorStore.setState({ panes: newPanes });
1237
1369
  EditorStore.setStatus("Saved", "success");
1370
+ _clearDraft(tab.path);
1238
1371
 
1239
1372
  // Hot reload for Markdown: sync preview tab after save
1240
1373
  if (/\.(md|markdown)$/i.test(tab.path)) {
@@ -1289,16 +1422,21 @@ var MbeditorApp = function MbeditorApp() {
1289
1422
  var pane2 = EditorStore.getState().panes.find(function (p) {
1290
1423
  return p.id === 2;
1291
1424
  });
1292
- if (!pane2 || pane2.tabs.length === 0) {
1293
- dragSplitWidthRef.current = 50;
1294
- setPane1Width(50);
1295
- } else {
1296
- dragSplitWidthRef.current = pane1Width;
1297
- }
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;
1298
1430
  setDraggedTab({ sourcePaneId: sourcePaneId, tabId: tabId });
1299
1431
  };
1300
1432
 
1301
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
+ }
1302
1440
  setDraggedTab(null);
1303
1441
  setDragOverPaneId(null);
1304
1442
  };
@@ -1309,6 +1447,21 @@ var MbeditorApp = function MbeditorApp() {
1309
1447
  clearDragState();
1310
1448
  };
1311
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
+
1312
1465
  var handleFormat = function handleFormat() {
1313
1466
  if (!activeTab) return;
1314
1467
 
@@ -1481,40 +1634,61 @@ var MbeditorApp = function MbeditorApp() {
1481
1634
  var onFormatRef = useRef(handleFormat);
1482
1635
  onFormatRef.current = handleFormat;
1483
1636
 
1637
+ // Eagerly load all remaining pages sequentially in the background.
1638
+ // Self-chains via .then() so only one request is in-flight at a time.
1639
+ // Uses only refs so it's safe to call from async callbacks without
1640
+ // worrying about stale closure state.
1484
1641
  var _debouncedSearch = useRef(window._.debounce(function (q) {
1485
1642
  if (!q.trim()) {
1486
1643
  searchRequestIdRef.current += 1;
1487
1644
  setSearchLoading(false);
1488
- setSearchHasMore(false);
1645
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1646
+ setSearchTotalCount(0);
1489
1647
  searchOffsetRef.current = 0;
1648
+ searchLoadingMoreRef.current = false;
1490
1649
  searchQueryRef.current = '';
1491
1650
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1492
1651
  return;
1493
1652
  }
1494
1653
  var requestId = ++searchRequestIdRef.current;
1495
1654
  setSearchLoading(true);
1496
- setSearchHasMore(false);
1655
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1656
+ setSearchTotalCount(0);
1497
1657
  searchOffsetRef.current = 0;
1658
+ searchLoadingMoreRef.current = false;
1498
1659
  searchQueryRef.current = q;
1499
- setSearchVirtStart(0);
1500
- searchVirtStartRef.current = 0;
1501
1660
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1502
1661
  EditorStore.setStatus("Searching project...", "info");
1503
- 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
- }
1662
+ SearchService.projectSearch(q, 0, SearchService.PAGE_SIZE, { regex: searchUseRegexRef.current, matchCase: searchMatchCaseRef.current, wholeWord: searchWholeWordRef.current }).then(function (res) {
1663
+ if (searchRequestIdRef.current !== requestId) return;
1664
+ var hasMore = !!(res && res.hasMore);
1665
+ setSearchHasMore(hasMore); searchHasMoreRef.current = hasMore;
1666
+ searchOffsetRef.current = SearchService.PAGE_SIZE;
1667
+ if (res && res.totalCount != null) setSearchTotalCount(res.totalCount);
1668
+ var total = (res && res.totalCount != null) ? res.totalCount : (res && res.results ? res.results.length : 0);
1669
+ EditorStore.setStatus("Found " + total + (hasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""), "success");
1511
1670
  }).finally(function () {
1512
- if (searchRequestIdRef.current === requestId) {
1513
- setSearchLoading(false);
1514
- }
1671
+ if (searchRequestIdRef.current === requestId) setSearchLoading(false);
1515
1672
  });
1516
1673
  }, 400)).current;
1517
1674
 
1675
+ var loadMoreSearchResults = function loadMoreSearchResults() {
1676
+ var q = searchQueryRef.current;
1677
+ if (!q || searchLoadingMoreRef.current || !searchHasMoreRef.current) return;
1678
+ searchLoadingMoreRef.current = true;
1679
+ var offset = searchOffsetRef.current;
1680
+ SearchService.projectSearch(q, offset, SearchService.PAGE_SIZE, { regex: searchUseRegexRef.current, matchCase: searchMatchCaseRef.current, wholeWord: searchWholeWordRef.current }).then(function(res) {
1681
+ if (searchQueryRef.current !== q) { searchLoadingMoreRef.current = false; return; }
1682
+ var hasMore = !!(res && res.hasMore);
1683
+ searchHasMoreRef.current = hasMore;
1684
+ setSearchHasMore(hasMore);
1685
+ searchOffsetRef.current = offset + SearchService.PAGE_SIZE;
1686
+ searchLoadingMoreRef.current = false;
1687
+ }).catch(function() {
1688
+ searchLoadingMoreRef.current = false;
1689
+ });
1690
+ };
1691
+
1518
1692
  var handleSearchChange = function handleSearchChange(e) {
1519
1693
  var val = e.target.value;
1520
1694
  if (!val) { clearSearch(); return; }
@@ -1522,17 +1696,43 @@ var MbeditorApp = function MbeditorApp() {
1522
1696
  _debouncedSearch(val);
1523
1697
  };
1524
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
+
1525
1726
  var clearSearch = function clearSearch() {
1526
1727
  searchRequestIdRef.current += 1;
1527
1728
  if (_debouncedSearch.cancel) _debouncedSearch.cancel();
1528
1729
  setSearchQuery("");
1529
1730
  setSearchLoading(false);
1530
- setSearchHasMore(false);
1531
- setSearchLoadingMore(false);
1731
+ setSearchHasMore(false); searchHasMoreRef.current = false;
1732
+ setSearchTotalCount(0);
1532
1733
  searchOffsetRef.current = 0;
1734
+ searchLoadingMoreRef.current = false;
1533
1735
  searchQueryRef.current = '';
1534
- setSearchVirtStart(0);
1535
- searchVirtStartRef.current = 0;
1536
1736
  EditorStore.setState({ searchResults: [], searchHasMore: false });
1537
1737
  };
1538
1738
 
@@ -1541,35 +1741,11 @@ var MbeditorApp = function MbeditorApp() {
1541
1741
  _debouncedSearch(searchQuery);
1542
1742
  };
1543
1743
 
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;
1744
+ // Load more results when the user scrolls near the bottom of the list.
1561
1745
  var handleSearchResultsScroll = function handleSearchResultsScroll(e) {
1562
1746
  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
- }
1747
+ if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
1748
+ loadMoreSearchResults();
1573
1749
  }
1574
1750
  };
1575
1751
 
@@ -2053,6 +2229,7 @@ var MbeditorApp = function MbeditorApp() {
2053
2229
  tabs: tabs,
2054
2230
  activeId: activeId,
2055
2231
  paneId: paneId,
2232
+ tabDisplayMode: editorPrefs.tabDisplayMode || 'scroll',
2056
2233
  onSelect: function (id) {
2057
2234
  // Sync explorer selection with the newly active tab so there's only one highlight
2058
2235
  var tab = tabs.find(function(t) { return t.id === id; });
@@ -2495,116 +2672,120 @@ var MbeditorApp = function MbeditorApp() {
2495
2672
  { className: "search-panel" },
2496
2673
  React.createElement(
2497
2674
  "div",
2498
- { 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
+ }),
2499
2682
  React.createElement(
2500
2683
  "div",
2501
- { className: "search-input-shell" },
2502
- React.createElement("input", {
2503
- className: "search-input",
2504
- placeholder: "Find in files…",
2505
- value: searchQuery,
2506
- onChange: handleSearchChange
2507
- }),
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
+ ),
2508
2715
  searchQuery && React.createElement(
2509
2716
  "button",
2510
2717
  {
2511
2718
  type: "button",
2512
- className: "search-clear-btn",
2719
+ className: "search-adornment-btn search-adornment-clear",
2513
2720
  onClick: clearSearch,
2514
2721
  title: "Clear search",
2515
2722
  "aria-label": "Clear search"
2516
2723
  },
2517
2724
  React.createElement("i", { className: "fas fa-times" })
2518
2725
  )
2519
- ),
2520
- React.createElement(
2521
- "button",
2522
- { type: "submit", className: "search-btn", disabled: searchLoading, title: searchLoading ? "Searching..." : "Search" },
2523
- React.createElement("i", { className: searchLoading ? "fas fa-spinner fa-spin" : "fas fa-search" })
2524
2726
  )
2525
2727
  ),
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;
2536
-
2537
- if (searchQuery && totalCount === 0 && !searchLoading) {
2538
- return React.createElement(
2539
- "div", { className: "search-results-empty" }, "No results"
2540
- );
2541
- }
2728
+ (function() {
2729
+ var allResults = state.searchResults || [];
2730
+ var loadedCount = allResults.length;
2731
+ var total = searchTotalCount > 0 ? searchTotalCount : loadedCount;
2732
+ var hasAny = loadedCount > 0;
2542
2733
 
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(
2734
+ return React.createElement(
2735
+ React.Fragment,
2736
+ null,
2737
+ searchQuery && !searchLoading && React.createElement(
2738
+ "div",
2739
+ { className: "search-results-header" },
2740
+ hasAny
2741
+ ? (total + (searchHasMore ? '+' : '') + " result" + (total !== 1 ? "s" : ""))
2742
+ : "No results"
2743
+ ),
2744
+ React.createElement(
2745
+ "div",
2746
+ { className: "search-results-area" },
2747
+ hasAny && React.createElement(
2558
2748
  "div",
2559
2749
  {
2560
- key: absoluteIdx,
2561
- className: "search-result-item",
2562
- onClick: function() { handleSelectFile(res.file, res.file.split('/').pop(), res.line); }
2750
+ className: "search-results" + (searchLoading ? " search-results-blurred" : ""),
2751
+ ref: searchResultsContainerRef,
2752
+ onScroll: handleSearchResultsScroll
2563
2753
  },
2564
- React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
2565
- React.createElement(
2566
- "div",
2567
- { className: "search-result-body" },
2568
- React.createElement(
2754
+ allResults.map(function(res, i) {
2755
+ var fileName = res.file.split('/').pop();
2756
+ return React.createElement(
2569
2757
  "div",
2570
- { className: "search-result-file" },
2571
- fileName,
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" }),
2572
2764
  React.createElement(
2573
- "span",
2574
- { className: "search-result-line-num" },
2575
- " ", res.file, ":", res.line
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)
2576
2772
  )
2577
- ),
2578
- React.createElement(
2579
- "div",
2580
- { className: "search-result-text" },
2581
- res.text
2582
- )
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"
2583
2779
  )
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
2780
  ),
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(
2781
+ searchLoading && React.createElement(
2601
2782
  "div",
2602
- { className: "search-results-loading-more", 'aria-busy': 'true' },
2603
- 'Loading more…'
2783
+ { className: "search-loading-overlay" },
2784
+ React.createElement("div", { className: "search-loading-spinner" })
2604
2785
  )
2605
- );
2606
- })()
2607
- )
2786
+ )
2787
+ );
2788
+ })()
2608
2789
  )
2609
2790
  )
2610
2791
  ),
@@ -2625,16 +2806,40 @@ var MbeditorApp = function MbeditorApp() {
2625
2806
  if (!draggedTab) return;
2626
2807
  e.preventDefault();
2627
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
+
2628
2817
  var rect = e.currentTarget.getBoundingClientRect();
2629
2818
  var splitAtX = rect.left + rect.width * (dragSplitWidthRef.current / 100);
2630
2819
  var hoverPaneId = e.clientX >= splitAtX ? 2 : 1;
2631
2820
  var nextDropPane = hoverPaneId === draggedTab.sourcePaneId ? null : hoverPaneId;
2632
2821
 
2633
- 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';
2634
2832
  if (dragOverPaneId !== nextDropPane) setDragOverPaneId(nextDropPane);
2635
2833
  },
2636
2834
  onDropCapture: function (e) {
2637
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
+
2638
2843
  e.preventDefault();
2639
2844
 
2640
2845
  var rect = e.currentTarget.getBoundingClientRect();
@@ -2649,10 +2854,12 @@ var MbeditorApp = function MbeditorApp() {
2649
2854
  }
2650
2855
  },
2651
2856
  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
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;
2653
2860
 
2654
2861
  // Dynamic width distribution
2655
- var isSplit = state.panes[1].tabs.length > 0 || !!draggedTab;
2862
+ var isSplit = state.panes[1].tabs.length > 0 || dragOverPaneId === 2;
2656
2863
  var flexBasis = '100%';
2657
2864
  if (isSplit) flexBasis = pane.id === 1 ? pane1Width + "%" : 100 - pane1Width + "%";
2658
2865
 
@@ -2735,6 +2942,32 @@ var MbeditorApp = function MbeditorApp() {
2735
2942
  onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { fontFamily: e.target.value }); }); }
2736
2943
  })
2737
2944
  ),
2945
+ React.createElement(
2946
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Row height in pixels. 0 = auto (roughly font size × 1.5)' },
2947
+ React.createElement('span', { className: 'ide-settings-label' }, 'Line height (0=auto)'),
2948
+ React.createElement('input', {
2949
+ type: 'number', min: '0', max: '100', step: '1',
2950
+ className: 'ide-settings-input',
2951
+ value: editorPrefs.lineHeight != null ? editorPrefs.lineHeight : 0,
2952
+ onChange: function(e) {
2953
+ var v = parseInt(e.target.value, 10);
2954
+ if (!isNaN(v) && v >= 0 && v <= 100) setEditorPrefs(function(p) { return Object.assign({}, p, { lineHeight: v }); });
2955
+ }
2956
+ })
2957
+ ),
2958
+ React.createElement(
2959
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Extra space between characters in pixels. 0 = default' },
2960
+ React.createElement('span', { className: 'ide-settings-label' }, 'Letter spacing (px)'),
2961
+ React.createElement('input', {
2962
+ type: 'number', min: '-5', max: '20', step: '0.5',
2963
+ className: 'ide-settings-input',
2964
+ value: editorPrefs.letterSpacing != null ? editorPrefs.letterSpacing : 0,
2965
+ onChange: function(e) {
2966
+ var v = parseFloat(e.target.value);
2967
+ if (!isNaN(v) && v >= -5 && v <= 20) setEditorPrefs(function(p) { return Object.assign({}, p, { letterSpacing: v }); });
2968
+ }
2969
+ })
2970
+ ),
2738
2971
 
2739
2972
  /* ── Indentation (unified editor + Prettier) ── */
2740
2973
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Indentation'),
@@ -2844,6 +3077,184 @@ var MbeditorApp = function MbeditorApp() {
2844
3077
  onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { vimMode: v }); }); }
2845
3078
  })
2846
3079
  ),
3080
+ React.createElement(
3081
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing bracket automatically' },
3082
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close brackets'),
3083
+ React.createElement(
3084
+ 'select', {
3085
+ value: editorPrefs.autoClosingBrackets || 'always',
3086
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingBrackets: e.target.value }); }); }
3087
+ },
3088
+ React.createElement('option', { value: 'always' }, 'Always'),
3089
+ React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
3090
+ React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
3091
+ React.createElement('option', { value: 'never' }, 'Never')
3092
+ )
3093
+ ),
3094
+ React.createElement(
3095
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'When to insert a matching closing quote automatically' },
3096
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto-close quotes'),
3097
+ React.createElement(
3098
+ 'select', {
3099
+ value: editorPrefs.autoClosingQuotes || 'always',
3100
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoClosingQuotes: e.target.value }); }); }
3101
+ },
3102
+ React.createElement('option', { value: 'always' }, 'Always'),
3103
+ React.createElement('option', { value: 'languageDefined' }, 'Per language rules'),
3104
+ React.createElement('option', { value: 'beforeWhitespace' }, 'Only before whitespace'),
3105
+ React.createElement('option', { value: 'never' }, 'Never')
3106
+ )
3107
+ ),
3108
+ React.createElement(
3109
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'What to highlight on the current editor line' },
3110
+ React.createElement('span', { className: 'ide-settings-label' }, 'Line highlight'),
3111
+ React.createElement(
3112
+ 'select', {
3113
+ value: editorPrefs.renderLineHighlight || 'none',
3114
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { renderLineHighlight: e.target.value }); }); }
3115
+ },
3116
+ React.createElement('option', { value: 'none' }, 'None'),
3117
+ React.createElement('option', { value: 'gutter' }, 'Line number only'),
3118
+ React.createElement('option', { value: 'line' }, 'Current line background'),
3119
+ React.createElement('option', { value: 'all' }, 'Line number + background')
3120
+ )
3121
+ ),
3122
+ React.createElement(
3123
+ 'label', { className: 'ide-settings-row ide-settings-row-half' },
3124
+ React.createElement('span', { className: 'ide-settings-label' }, 'Cursor style'),
3125
+ React.createElement(
3126
+ 'select', {
3127
+ value: editorPrefs.cursorStyle || 'line',
3128
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorStyle: e.target.value }); }); }
3129
+ },
3130
+ React.createElement('option', { value: 'line' }, 'Line (|)'),
3131
+ React.createElement('option', { value: 'block' }, 'Block (filled)'),
3132
+ React.createElement('option', { value: 'underline' }, 'Underline (_)'),
3133
+ React.createElement('option', { value: 'line-thin' }, 'Line thin'),
3134
+ React.createElement('option', { value: 'block-outline' }, 'Block outline'),
3135
+ React.createElement('option', { value: 'underline-thin' }, 'Underline thin')
3136
+ )
3137
+ ),
3138
+ React.createElement(
3139
+ 'label', { className: 'ide-settings-row ide-settings-row-half' },
3140
+ React.createElement('span', { className: 'ide-settings-label' }, 'Cursor blinking'),
3141
+ React.createElement(
3142
+ 'select', {
3143
+ value: editorPrefs.cursorBlinking || 'blink',
3144
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { cursorBlinking: e.target.value }); }); }
3145
+ },
3146
+ React.createElement('option', { value: 'blink' }, 'Blink (on/off)'),
3147
+ React.createElement('option', { value: 'smooth' }, 'Smooth (fade)'),
3148
+ React.createElement('option', { value: 'phase' }, 'Phase (offset fade)'),
3149
+ React.createElement('option', { value: 'expand' }, 'Expand (grow/shrink)'),
3150
+ React.createElement('option', { value: 'solid' }, 'Solid (no blink)')
3151
+ )
3152
+ ),
3153
+ React.createElement(
3154
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show collapse arrows next to foldable regions (functions, classes, blocks)' },
3155
+ React.createElement('span', { className: 'ide-settings-label' }, 'Code folding'),
3156
+ React.createElement('input', {
3157
+ type: 'checkbox',
3158
+ className: 'ide-settings-checkbox',
3159
+ checked: editorPrefs.folding !== false,
3160
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { folding: v }); }); }
3161
+ })
3162
+ ),
3163
+ React.createElement(
3164
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Animate scrolling instead of jumping instantly' },
3165
+ React.createElement('span', { className: 'ide-settings-label' }, 'Smooth scrolling'),
3166
+ React.createElement('input', {
3167
+ type: 'checkbox',
3168
+ className: 'ide-settings-checkbox',
3169
+ checked: !!(editorPrefs.smoothScrolling),
3170
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { smoothScrolling: v }); }); }
3171
+ })
3172
+ ),
3173
+ React.createElement(
3174
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Hold Ctrl (or Cmd) and scroll the mouse wheel to zoom the font size' },
3175
+ React.createElement('span', { className: 'ide-settings-label' }, 'Ctrl+scroll to zoom'),
3176
+ React.createElement('input', {
3177
+ type: 'checkbox',
3178
+ className: 'ide-settings-checkbox',
3179
+ checked: !!(editorPrefs.mouseWheelZoom),
3180
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { mouseWheelZoom: v }); }); }
3181
+ })
3182
+ ),
3183
+
3184
+ /* ── Behaviour ───────────────────────────────── */
3185
+ React.createElement('div', { className: 'ide-settings-section-header' }, 'Behaviour'),
3186
+ React.createElement(
3187
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'How aggressively the editor re-indents lines as you type' },
3188
+ React.createElement('span', { className: 'ide-settings-label' }, 'Auto indent'),
3189
+ React.createElement(
3190
+ 'select', {
3191
+ value: editorPrefs.autoIndent || 'full',
3192
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { autoIndent: e.target.value }); }); }
3193
+ },
3194
+ React.createElement('option', { value: 'none' }, 'None (disabled)'),
3195
+ React.createElement('option', { value: 'keep' }, 'Keep current level'),
3196
+ React.createElement('option', { value: 'brackets' }, 'Indent on { and ['),
3197
+ React.createElement('option', { value: 'advanced' }, 'Language indent rules'),
3198
+ React.createElement('option', { value: 'full' }, 'Full (language grammar)')
3199
+ )
3200
+ ),
3201
+ React.createElement(
3202
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Whether pressing Enter accepts the highlighted autocomplete suggestion' },
3203
+ React.createElement('span', { className: 'ide-settings-label' }, 'Accept suggestion on Enter'),
3204
+ React.createElement(
3205
+ 'select', {
3206
+ value: editorPrefs.acceptSuggestionOnEnter || 'on',
3207
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { acceptSuggestionOnEnter: e.target.value }); }); }
3208
+ },
3209
+ React.createElement('option', { value: 'on' }, 'Always'),
3210
+ React.createElement('option', { value: 'smart' }, 'Only when navigated (↑↓)'),
3211
+ React.createElement('option', { value: 'off' }, 'Never (Tab only)')
3212
+ )
3213
+ ),
3214
+ React.createElement(
3215
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Suggest completions based on words already present in open files' },
3216
+ React.createElement('span', { className: 'ide-settings-label' }, 'Word-based suggestions'),
3217
+ React.createElement(
3218
+ 'select', {
3219
+ value: editorPrefs.wordBasedSuggestions || 'matchingDocuments',
3220
+ onChange: function(e) { setEditorPrefs(function(p) { return Object.assign({}, p, { wordBasedSuggestions: e.target.value }); }); }
3221
+ },
3222
+ React.createElement('option', { value: 'off' }, 'Off'),
3223
+ React.createElement('option', { value: 'currentDocument' }, 'Current file only'),
3224
+ React.createElement('option', { value: 'matchingDocuments' }, 'Same language files'),
3225
+ React.createElement('option', { value: 'allDocuments' }, 'All open files')
3226
+ )
3227
+ ),
3228
+ React.createElement(
3229
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Auto-format pasted code using the language formatter' },
3230
+ React.createElement('span', { className: 'ide-settings-label' }, 'Format on paste'),
3231
+ React.createElement('input', {
3232
+ type: 'checkbox',
3233
+ className: 'ide-settings-checkbox',
3234
+ checked: editorPrefs.formatOnPaste !== false,
3235
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnPaste: v }); }); }
3236
+ })
3237
+ ),
3238
+ React.createElement(
3239
+ '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 {})' },
3240
+ React.createElement('span', { className: 'ide-settings-label' }, 'Format on type'),
3241
+ React.createElement('input', {
3242
+ type: 'checkbox',
3243
+ className: 'ide-settings-checkbox',
3244
+ checked: editorPrefs.formatOnType !== false,
3245
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { formatOnType: v }); }); }
3246
+ })
3247
+ ),
3248
+ React.createElement(
3249
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show autocomplete suggestions while typing (not just on trigger characters like .)' },
3250
+ React.createElement('span', { className: 'ide-settings-label' }, 'Quick suggestions'),
3251
+ React.createElement('input', {
3252
+ type: 'checkbox',
3253
+ className: 'ide-settings-checkbox',
3254
+ checked: editorPrefs.quickSuggestions !== false,
3255
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { quickSuggestions: v }); }); }
3256
+ })
3257
+ ),
2847
3258
 
2848
3259
  /* ── Formatting (Prettier) ───────────────────── */
2849
3260
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Formatting'),
@@ -2926,6 +3337,18 @@ var MbeditorApp = function MbeditorApp() {
2926
3337
  onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { fileTreeTypeahead: v }); }); }
2927
3338
  })
2928
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
+ ),
2929
3352
  React.createElement(
2930
3353
  'label', { className: 'ide-settings-row ide-settings-row-check' },
2931
3354
  React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
@@ -2947,6 +3370,17 @@ var MbeditorApp = function MbeditorApp() {
2947
3370
  })
2948
3371
  ),
2949
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
+
2950
3384
  /* ── RuboCop ─────────────────────────────────── */
2951
3385
  React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
2952
3386
  React.createElement(
@@ -3023,8 +3457,10 @@ var MbeditorApp = function MbeditorApp() {
3023
3457
  var valNorm = val.replace(/\r\n/g, '\n');
3024
3458
  if (valNorm === cleanNorm) {
3025
3459
  TabManager.markClean(pane.id, pActiveTab.id, val);
3460
+ _clearDraft(pActiveTab.path);
3026
3461
  } else {
3027
3462
  TabManager.markDirty(pane.id, pActiveTab.id, val);
3463
+ _scheduleDraftWrite(pActiveTab.path, val);
3028
3464
  }
3029
3465
  }
3030
3466
  });
@@ -3226,12 +3662,22 @@ var MbeditorApp = function MbeditorApp() {
3226
3662
  state.gitInfo.behind
3227
3663
  )
3228
3664
  ),
3229
- !serverOnline && React.createElement(
3230
- "div",
3231
- { className: "statusbar-offline" },
3232
- React.createElement("i", { className: "fas fa-exclamation-triangle" }),
3233
- " Server offline"
3234
- ),
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
+ })(),
3235
3681
  activeFileCommit && React.createElement(
3236
3682
  "div",
3237
3683
  { className: "statusbar-file-commit", title: activeFileCommit.title + " — " + activeFileCommit.author },
@@ -3245,6 +3691,16 @@ var MbeditorApp = function MbeditorApp() {
3245
3691
  { className: "statusbar-msg " + state.statusMessage.kind },
3246
3692
  state.statusMessage.text
3247
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
+ ),
3248
3704
  React.createElement(
3249
3705
  "div",
3250
3706
  { className: "statusbar-version" },
@@ -3366,6 +3822,63 @@ var MbeditorApp = function MbeditorApp() {
3366
3822
  onSelectFolder: handleOpenFolderInExplorer,
3367
3823
  onClose: function () { return setQuickOpen(false); }
3368
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
+ ),
3369
3882
  contextMenu && React.createElement(
3370
3883
  React.Fragment,
3371
3884
  null,