mbeditor 0.5.3 → 0.7.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +7 -0
  4. data/app/assets/javascripts/mbeditor/application.js +3 -0
  5. data/app/assets/javascripts/mbeditor/components/ChangelogView.js +145 -0
  6. data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
  7. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +359 -31
  8. data/app/assets/javascripts/mbeditor/components/FileTree.js +177 -116
  9. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +952 -143
  10. data/app/assets/javascripts/mbeditor/components/TabBar.js +9 -0
  11. data/app/assets/javascripts/mbeditor/conflict_parser.js +48 -0
  12. data/app/assets/javascripts/mbeditor/editor_plugins.js +420 -67
  13. data/app/assets/javascripts/mbeditor/editor_store.js +1 -0
  14. data/app/assets/javascripts/mbeditor/file_service.js +34 -6
  15. data/app/assets/javascripts/mbeditor/git_service.js +2 -1
  16. data/app/assets/javascripts/mbeditor/history_service.js +177 -0
  17. data/app/assets/javascripts/mbeditor/search_service.js +1 -0
  18. data/app/assets/javascripts/mbeditor/tab_manager.js +8 -5
  19. data/app/assets/stylesheets/mbeditor/application.css +112 -0
  20. data/app/assets/stylesheets/mbeditor/editor.css +443 -78
  21. data/app/channels/mbeditor/editor_channel.rb +5 -41
  22. data/app/controllers/mbeditor/application_controller.rb +8 -1
  23. data/app/controllers/mbeditor/editors_controller.rb +276 -654
  24. data/app/controllers/mbeditor/git_controller.rb +2 -61
  25. data/app/services/mbeditor/availability_probe.rb +83 -0
  26. data/app/services/mbeditor/code_search_service.rb +42 -0
  27. data/app/services/mbeditor/editor_state_service.rb +91 -0
  28. data/app/services/mbeditor/exclusion_matcher.rb +23 -0
  29. data/app/services/mbeditor/file_operation_service.rb +68 -0
  30. data/app/services/mbeditor/file_tree_service.rb +69 -0
  31. data/app/services/mbeditor/git_combined_diff_service.rb +43 -0
  32. data/app/services/mbeditor/git_commit_detail_service.rb +46 -0
  33. data/app/services/mbeditor/git_info_service.rb +151 -0
  34. data/app/services/mbeditor/git_service.rb +36 -26
  35. data/app/services/mbeditor/js_definition_service.rb +59 -0
  36. data/app/services/mbeditor/js_members_service.rb +62 -0
  37. data/app/services/mbeditor/process_runner.rb +48 -0
  38. data/app/services/mbeditor/rails_related_files_service.rb +282 -0
  39. data/app/services/mbeditor/ruby_definition_service.rb +77 -101
  40. data/app/services/mbeditor/schema_service.rb +270 -0
  41. data/app/services/mbeditor/search_replace_service.rb +184 -0
  42. data/app/services/mbeditor/test_runner_service.rb +5 -27
  43. data/app/views/layouts/mbeditor/application.html.erb +2 -2
  44. data/config/routes.rb +8 -1
  45. data/lib/mbeditor/configuration.rb +4 -2
  46. data/lib/mbeditor/version.rb +1 -1
  47. data/public/monaco-editor/vs/language/css/cssMode.js +13 -0
  48. data/public/monaco-editor/vs/language/css/cssWorker.js +77 -0
  49. data/public/monaco-editor/vs/language/html/htmlMode.js +13 -0
  50. data/public/monaco-editor/vs/language/html/htmlWorker.js +454 -0
  51. data/public/monaco-editor/vs/language/json/jsonMode.js +19 -0
  52. data/public/monaco-editor/vs/language/json/jsonWorker.js +42 -0
  53. metadata +26 -3
  54. data/app/services/mbeditor/unused_methods_service.rb +0 -139
@@ -63,9 +63,51 @@ var DEFAULT_EDITOR_PREFS = {
63
63
  quickOpenShowFolders: false,
64
64
  tabDisplayMode: 'scroll',
65
65
  persistFindState: true,
66
- showDotFiles: false
66
+ showDotFiles: false,
67
+ branchStateRestore: true
67
68
  };
68
69
 
70
+ // Detect the minimum number of leading spaces used for indentation across all
71
+ // indented lines in the code. Returns 0 if no space-indented lines are found
72
+ // (e.g. file already uses tabs or has no indented lines).
73
+ function detectIndentWidth(code) {
74
+ var min = Infinity;
75
+ code.split('\n').forEach(function(line) {
76
+ if (!line.trim()) return;
77
+ var m = line.match(/^( +)/);
78
+ if (m) min = Math.min(min, m[1].length);
79
+ });
80
+ return min === Infinity ? 0 : min;
81
+ }
82
+
83
+ // Convert leading space-based indentation to tabs using the detected unit size.
84
+ function spacesToTabs(code, indentSize) {
85
+ var unit = ' '.repeat(indentSize);
86
+ return code.split('\n').map(function(line) {
87
+ var tabs = '';
88
+ while (line.startsWith(unit)) { tabs += '\t'; line = line.slice(unit.length); }
89
+ return tabs + line;
90
+ }).join('\n');
91
+ }
92
+
93
+ function diffLines(oldLines, newLines) {
94
+ var n = oldLines.length, m = newLines.length;
95
+ var dp = [];
96
+ for (var i = 0; i <= n; i++) { dp.push(new Array(m + 1).fill(0)); }
97
+ for (var i = 1; i <= n; i++) {
98
+ for (var j = 1; j <= m; j++) {
99
+ dp[i][j] = oldLines[i-1] === newLines[j-1] ? dp[i-1][j-1] + 1 : Math.max(dp[i-1][j], dp[i][j-1]);
100
+ }
101
+ }
102
+ var changed = [], i = n, j = m;
103
+ while (i > 0 || j > 0) {
104
+ if (i > 0 && j > 0 && oldLines[i-1] === newLines[j-1]) { i--; j--; }
105
+ else if (j > 0 && (i === 0 || dp[i][j-1] >= dp[i-1][j])) { changed.push(j); j--; }
106
+ else { i--; }
107
+ }
108
+ return changed;
109
+ }
110
+
69
111
  var SidebarActionButton = function SidebarActionButton(_ref) {
70
112
  var title = _ref.title;
71
113
  var iconClass = _ref.iconClass;
@@ -111,6 +153,44 @@ var SectionActionGroup = function SectionActionGroup(_ref2) {
111
153
  );
112
154
  };
113
155
 
156
+ function FileReloadBanner(_ref) {
157
+ var pendingReloads = _ref.pendingReloads;
158
+ var onSaveAndReload = _ref.onSaveAndReload;
159
+ var onDiscardAndReload = _ref.onDiscardAndReload;
160
+ var onKeepMine = _ref.onKeepMine;
161
+ if (!pendingReloads || pendingReloads.length === 0) return null;
162
+ return React.createElement(
163
+ 'div', { className: 'mb-file-reload-banner' },
164
+ pendingReloads.map(function (r) {
165
+ return React.createElement(
166
+ 'div', { key: r.paneId + ':' + r.tabId, className: 'mb-file-reload-item' },
167
+ React.createElement(
168
+ 'span', { className: 'mb-file-reload-msg' },
169
+ React.createElement('i', { className: 'fas fa-sync-alt' }),
170
+ ' ',
171
+ React.createElement('strong', null, r.name),
172
+ ' was updated externally'
173
+ ),
174
+ React.createElement(
175
+ 'div', { className: 'mb-file-reload-actions' },
176
+ React.createElement('button', {
177
+ className: 'mb-btn mb-btn-sm mb-btn-primary',
178
+ onClick: function () { onSaveAndReload(r); }
179
+ }, 'Save & Reload'),
180
+ React.createElement('button', {
181
+ className: 'mb-btn mb-btn-sm mb-btn-warning',
182
+ onClick: function () { onDiscardAndReload(r); }
183
+ }, 'Discard & Reload'),
184
+ React.createElement('button', {
185
+ className: 'mb-btn mb-btn-sm',
186
+ onClick: function () { onKeepMine(r); }
187
+ }, 'Keep Mine')
188
+ )
189
+ );
190
+ })
191
+ );
192
+ }
193
+
114
194
  var MbeditorApp = function MbeditorApp() {
115
195
  var _useState = useState(EditorStore.getState());
116
196
 
@@ -246,6 +326,31 @@ var MbeditorApp = function MbeditorApp() {
246
326
  var sidebarCollapsed = _useStateSC2[0];
247
327
  var setSidebarCollapsed = _useStateSC2[1];
248
328
 
329
+ var _useStateRFMap = useState({});
330
+ var _useStateRFMap2 = _slicedToArray(_useStateRFMap, 2);
331
+ var railsFilesMap = _useStateRFMap2[0];
332
+ var setRailsFilesMap = _useStateRFMap2[1];
333
+
334
+ var _useStateRFC = useState({});
335
+ var _useStateRFC2 = _slicedToArray(_useStateRFC, 2);
336
+ var railsGroupsCollapsed = _useStateRFC2[0];
337
+ var setRailsGroupsCollapsed = _useStateRFC2[1];
338
+
339
+ var _useStateChangelog = useState(null); // null | { content, loading, error }
340
+ var _useStateChangelog2 = _slicedToArray(_useStateChangelog, 2);
341
+ var changelogState = _useStateChangelog2[0];
342
+ var setChangelogState = _useStateChangelog2[1];
343
+
344
+ var _useStateSchemaModal = useState(null);
345
+ var _useStateSchemaModal2 = _slicedToArray(_useStateSchemaModal, 2);
346
+ var schemaModal = _useStateSchemaModal2[0];
347
+ var setSchemaModal = _useStateSchemaModal2[1];
348
+
349
+ var _useStateSchemaLoading = useState(null);
350
+ var _useStateSchemaLoading2 = _slicedToArray(_useStateSchemaLoading, 2);
351
+ var schemaLoadingLabel = _useStateSchemaLoading2[0];
352
+ var setSchemaLoadingLabel = _useStateSchemaLoading2[1];
353
+
249
354
  var _useState9 = useState({});
250
355
 
251
356
  var _useState92 = _slicedToArray(_useState9, 2);
@@ -458,6 +563,16 @@ var MbeditorApp = function MbeditorApp() {
458
563
  var prevGitBranchRef = useRef(null);
459
564
  var isSwitchingBranchRef = useRef(false);
460
565
  var stateRestoredRef = useRef(false);
566
+ var ctrlWPendingRef = useRef(false);
567
+ var ctrlWTimeoutRef = useRef(null);
568
+ var _useStateCP = useState([]);
569
+ var _useStateCP2 = _slicedToArray(_useStateCP, 2);
570
+ var customPaths = _useStateCP2[0];
571
+ var setCustomPaths = _useStateCP2[1];
572
+ var customPathsRef = useRef([]);
573
+ customPathsRef.current = customPaths;
574
+ var recentSavesRef = useRef({});
575
+ var isSavingRef = useRef(false);
461
576
 
462
577
  // ── Draft backup helpers ─────────────────────────────────────────────────
463
578
  var draftWriteTimerRef = useRef({});
@@ -468,7 +583,14 @@ var MbeditorApp = function MbeditorApp() {
468
583
  return 'mbeditor_draft\x00' + base + '\x00' + path;
469
584
  };
470
585
  var _saveDraftNow = function _saveDraftNow(path, content) {
471
- try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
586
+ var doWrite = function() {
587
+ try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
588
+ };
589
+ if (typeof requestIdleCallback !== 'undefined') {
590
+ requestIdleCallback(doWrite, { timeout: 2000 });
591
+ } else {
592
+ doWrite();
593
+ }
472
594
  };
473
595
  var _clearDraft = function _clearDraft(path) {
474
596
  try { localStorage.removeItem(_draftKey(path)); } catch (e) {}
@@ -499,6 +621,11 @@ var MbeditorApp = function MbeditorApp() {
499
621
  var zenMode = _useStateZen2[0];
500
622
  var setZenMode = _useStateZen2[1];
501
623
 
624
+ var _useStateSB = useState(false);
625
+ var _useStateSB2 = _slicedToArray(_useStateSB, 2);
626
+ var isSwitchingBranch = _useStateSB2[0];
627
+ var setIsSwitchingBranch = _useStateSB2[1];
628
+
502
629
  var clamp = function clamp(value, min, max) {
503
630
  return Math.min(max, Math.max(min, value));
504
631
  };
@@ -746,6 +873,9 @@ var MbeditorApp = function MbeditorApp() {
746
873
  if (t.isSettings || t.path === '__settings__') {
747
874
  return Promise.resolve({ content: '' });
748
875
  }
876
+ if (t.isChangelog || t.path === 'mbeditor://changelog') {
877
+ return Promise.resolve({ content: '' });
878
+ }
749
879
  if (t.isDiff && t.repoPath) {
750
880
  return GitService.fetchDiff(t.repoPath, t.diffBaseSha, t.diffHeadSha)
751
881
  .then(function (d) { return { content: 'Diff loaded', diffOriginal: d.original || '', diffModified: d.modified || '', _isDiffResult: true }; })
@@ -849,26 +979,33 @@ var MbeditorApp = function MbeditorApp() {
849
979
  if (!newBranch || newBranch === oldBranch) return;
850
980
  prevGitBranchRef.current = newBranch;
851
981
  if (!oldBranch || isSwitchingBranchRef.current) return;
982
+ // Ignore spurious branch changes triggered by saves (race condition)
983
+ if (isSavingRef.current) return;
852
984
 
853
985
  isSwitchingBranchRef.current = true;
986
+ setIsSwitchingBranch(true);
854
987
 
855
- // Save pane state for old branch before switching
856
- var cur = EditorStore.getState();
857
- var lightweightPanes = cur.panes.map(function (p) {
858
- return {
859
- id: p.id,
860
- activeTabId: p.activeTabId,
861
- tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff; }).map(function (t) {
862
- return {
863
- id: t.id, path: t.path, name: t.name, dirty: t.dirty, viewState: t.viewState,
864
- isSettings: !!t.isSettings, isPreview: !!t.isPreview, previewFor: t.previewFor || null,
865
- isDiff: !!t.isDiff, diffBaseSha: t.diffBaseSha || null, diffHeadSha: t.diffHeadSha || null,
866
- repoPath: t.repoPath || null
867
- };
868
- })
869
- };
870
- });
871
- FileService.saveBranchState(oldBranch, { panes: lightweightPanes, focusedPaneId: cur.focusedPaneId })["catch"](function () {});
988
+ var shouldRestore = (EditorStore.getState().editorPrefs || {}).branchStateRestore !== false;
989
+
990
+ if (shouldRestore) {
991
+ // Save pane state for old branch before switching
992
+ var cur = EditorStore.getState();
993
+ var lightweightPanes = cur.panes.map(function (p) {
994
+ return {
995
+ id: p.id,
996
+ activeTabId: p.activeTabId,
997
+ tabs: p.tabs.filter(function (t) { return !t.isCombinedDiff; }).map(function (t) {
998
+ return {
999
+ id: t.id, path: t.path, name: t.name, dirty: t.dirty, viewState: t.viewState,
1000
+ isSettings: !!t.isSettings, isPreview: !!t.isPreview, previewFor: t.previewFor || null,
1001
+ isDiff: !!t.isDiff, diffBaseSha: t.diffBaseSha || null, diffHeadSha: t.diffHeadSha || null,
1002
+ repoPath: t.repoPath || null, isChangelog: !!t.isChangelog
1003
+ };
1004
+ })
1005
+ };
1006
+ });
1007
+ FileService.saveBranchState(oldBranch, { panes: lightweightPanes, focusedPaneId: cur.focusedPaneId })["catch"](function () {});
1008
+ }
872
1009
 
873
1010
  // Clear all open tabs for the new branch
874
1011
  EditorStore.setState({
@@ -878,18 +1015,27 @@ var MbeditorApp = function MbeditorApp() {
878
1015
  });
879
1016
 
880
1017
  // Load pane state for new branch (or start empty)
881
- FileService.getBranchState(newBranch)["catch"](function () { return null; }).then(function (branchState) {
882
- var hasBranchPanes = branchState && branchState.panes && branchState.panes.some(function (p) { return p.tabs && p.tabs.length > 0; });
883
- if (hasBranchPanes) {
884
- return loadPaneState(branchState.panes, branchState.focusedPaneId || 1);
885
- }
886
- return null;
887
- }).then(function () {
1018
+ var restorePromise;
1019
+ if (shouldRestore) {
1020
+ restorePromise = FileService.getBranchState(newBranch)["catch"](function () { return null; }).then(function (branchState) {
1021
+ var hasBranchPanes = branchState && branchState.panes && branchState.panes.some(function (p) { return p.tabs && p.tabs.length > 0; });
1022
+ if (hasBranchPanes) {
1023
+ return loadPaneState(branchState.panes, branchState.focusedPaneId || 1);
1024
+ }
1025
+ return null;
1026
+ });
1027
+ } else {
1028
+ restorePromise = Promise.resolve(null);
1029
+ }
1030
+
1031
+ restorePromise.then(function () {
888
1032
  // Prune states for deleted branches
889
1033
  FileService.pruneBranchStates()["catch"](function () {});
890
1034
  isSwitchingBranchRef.current = false;
1035
+ setIsSwitchingBranch(false);
891
1036
  })["catch"](function () {
892
1037
  isSwitchingBranchRef.current = false;
1038
+ setIsSwitchingBranch(false);
893
1039
  });
894
1040
  });
895
1041
 
@@ -962,7 +1108,7 @@ var MbeditorApp = function MbeditorApp() {
962
1108
  var rect = body.getBoundingClientRect();
963
1109
  var reservedRight = EDITOR_MIN_WIDTH + (showGitPanelRef.current ? gitPanelWidthRef.current : 0);
964
1110
  var maxSidebarWidth = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, rect.width - reservedRight));
965
- var nextWidth = clientX - rect.left;
1111
+ var nextWidth = clientX - rect.left - SIDEBAR_COLLAPSED_WIDTH;
966
1112
  setSidebarWidth(clamp(nextWidth, SIDEBAR_MIN_WIDTH, maxSidebarWidth));
967
1113
  }
968
1114
 
@@ -1006,8 +1152,46 @@ var MbeditorApp = function MbeditorApp() {
1006
1152
  }
1007
1153
  };
1008
1154
 
1155
+ // Capture-phase listener for vim Ctrl+W window navigation.
1156
+ // Runs before Monaco (and the browser) so neither can swallow the keystrokes.
1157
+ // Phase 1 — Ctrl+W: prevent tab-close and arm the pending flag.
1158
+ // Phase 2 — next key: act on it here (still capture phase) so Monaco-vim
1159
+ // cannot consume it as a word-motion or other binding.
1160
+ var onCtrlWCapture = function(e) {
1161
+ // Phase 2: a previous Ctrl+W is pending — consume the follow-up key.
1162
+ if (ctrlWPendingRef.current) {
1163
+ ctrlWPendingRef.current = false;
1164
+ if (ctrlWTimeoutRef.current) { clearTimeout(ctrlWTimeoutRef.current); ctrlWTimeoutRef.current = null; }
1165
+ var _st = EditorStore.getState();
1166
+ var _cur = _st.focusedPaneId;
1167
+ var _target;
1168
+ if (e.key === '1') _target = 1;
1169
+ else if (e.key === '2') _target = 2;
1170
+ else if (e.key === 'h') _target = 1;
1171
+ else _target = _cur === 1 ? 2 : 1; // w, l, Ctrl+W, or anything else → cycle
1172
+ if (_target !== _cur) {
1173
+ if (typeof TabManager !== 'undefined') TabManager.focusPane(_target);
1174
+ window.dispatchEvent(new CustomEvent('mbeditor:focusPane', { detail: { paneId: _target } }));
1175
+ }
1176
+ e.preventDefault();
1177
+ e.stopPropagation();
1178
+ return;
1179
+ }
1180
+ // Phase 1: intercept Ctrl+W itself when vim mode is on.
1181
+ if (e.metaKey || e.shiftKey || e.altKey) return;
1182
+ if (!e.ctrlKey || (e.key !== 'w' && e.key !== 'W')) return;
1183
+ var prefs = EditorStore.getState().editorPrefs;
1184
+ if (!prefs || !prefs.vimMode) return;
1185
+ e.preventDefault();
1186
+ e.stopPropagation();
1187
+ if (ctrlWTimeoutRef.current) clearTimeout(ctrlWTimeoutRef.current);
1188
+ ctrlWPendingRef.current = true;
1189
+ ctrlWTimeoutRef.current = setTimeout(function() { ctrlWPendingRef.current = false; }, 1500);
1190
+ };
1191
+
1009
1192
  window.addEventListener('keydown', onKeyDown);
1010
1193
  document.addEventListener('keydown', onZenCapture, true);
1194
+ document.addEventListener('keydown', onCtrlWCapture, true);
1011
1195
  window.addEventListener('mousemove', handleMouseMove);
1012
1196
  window.addEventListener('mouseup', handleMouseUp);
1013
1197
  return function () {
@@ -1018,8 +1202,10 @@ var MbeditorApp = function MbeditorApp() {
1018
1202
  cancelAnimationFrame(resizeRafRef.current);
1019
1203
  resizeRafRef.current = null;
1020
1204
  }
1205
+ if (ctrlWTimeoutRef.current) { clearTimeout(ctrlWTimeoutRef.current); ctrlWTimeoutRef.current = null; }
1021
1206
  window.removeEventListener('keydown', onKeyDown);
1022
1207
  document.removeEventListener('keydown', onZenCapture, true);
1208
+ document.removeEventListener('keydown', onCtrlWCapture, true);
1023
1209
  window.removeEventListener('mousemove', handleMouseMove);
1024
1210
  window.removeEventListener('mouseup', handleMouseUp);
1025
1211
  document.body.style.cursor = '';
@@ -1095,16 +1281,83 @@ var MbeditorApp = function MbeditorApp() {
1095
1281
  FileService.getTree().then(function (data) {
1096
1282
  var newData = data || [];
1097
1283
  setTreeData(function (prevData) {
1098
- if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
1099
- SearchService.buildIndex(newData);
1284
+ var sig = function(d) { return d.length + ':' + d.map(function(n) { return n.name; }).join(','); };
1285
+ if (sig(newData) !== sig(prevData)) SearchService.buildIndex(newData);
1100
1286
  return newData;
1101
1287
  });
1288
+ checkOpenTabsForExternalChanges();
1102
1289
  })["catch"](function () {});
1103
1290
  }
1104
1291
  WebSocketService.onFilesChanged(handleFilesChanged);
1105
1292
  return function () { WebSocketService.offFilesChanged(handleFilesChanged); };
1106
1293
  }, []);
1107
1294
 
1295
+ function checkOpenTabsForExternalChanges() {
1296
+ var st = EditorStore.getState();
1297
+ var allTabs = st.panes.reduce(function (acc, p) {
1298
+ return acc.concat(p.tabs.map(function (t) { return { paneId: p.id, tab: t }; }));
1299
+ }, []);
1300
+ var fileTabs = allTabs.filter(function (pt) {
1301
+ var path = pt.tab.path || '';
1302
+ return path &&
1303
+ !path.startsWith('mbeditor://') &&
1304
+ !path.startsWith('diff://') &&
1305
+ !path.startsWith('combined-diff://') &&
1306
+ !pt.tab.isCombinedDiff &&
1307
+ !pt.tab.isSettings &&
1308
+ !pt.tab.isImage &&
1309
+ !pt.tab.isDiff &&
1310
+ typeof pt.tab.content === 'string';
1311
+ });
1312
+ fileTabs.forEach(function (pt) {
1313
+ var savedAt = recentSavesRef.current[pt.tab.path];
1314
+ if (savedAt && Date.now() - savedAt < 3000) return;
1315
+ FileService.getFile(pt.tab.path, { allowMissing: true }).then(function (data) {
1316
+ if (!data || typeof data.content !== 'string') return;
1317
+ var serverNorm = data.content.replace(/\r\n/g, '\n');
1318
+ var tabNorm = (pt.tab.content || '').replace(/\r\n/g, '\n');
1319
+ if (serverNorm === tabNorm) return;
1320
+ if (!pt.tab.dirty) {
1321
+ EditorStore.setState({
1322
+ panes: EditorStore.getState().panes.map(function (p) {
1323
+ if (p.id !== pt.paneId) return p;
1324
+ return Object.assign({}, p, {
1325
+ tabs: p.tabs.map(function (t) {
1326
+ if (t.id !== pt.tab.id) return t;
1327
+ return Object.assign({}, t, {
1328
+ content: data.content,
1329
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1330
+ });
1331
+ })
1332
+ });
1333
+ })
1334
+ });
1335
+ } else {
1336
+ // Re-verify the tab still exists before queuing
1337
+ var currentState = EditorStore.getState();
1338
+ var stillExists = currentState.panes.some(function (p) {
1339
+ return p.id === pt.paneId && p.tabs.some(function (t) { return t.id === pt.tab.id; });
1340
+ });
1341
+ if (!stillExists) return;
1342
+ var existing = EditorStore.getState().pendingReloads.find(function (r) {
1343
+ return r.paneId === pt.paneId && r.tabId === pt.tab.id;
1344
+ });
1345
+ if (!existing) {
1346
+ EditorStore.setState({
1347
+ pendingReloads: EditorStore.getState().pendingReloads.concat([{
1348
+ paneId: pt.paneId,
1349
+ tabId: pt.tab.id,
1350
+ path: pt.tab.path,
1351
+ name: pt.tab.name,
1352
+ serverContent: data.content
1353
+ }])
1354
+ });
1355
+ }
1356
+ }
1357
+ })["catch"](function () {});
1358
+ });
1359
+ }
1360
+
1108
1361
  // Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
1109
1362
  // When an ActionCable WebSocket is connected this acts only as a safety-net fallback —
1110
1363
  // the WebSocket push above handles immediate invalidation after mbeditor mutations.
@@ -1118,8 +1371,8 @@ var MbeditorApp = function MbeditorApp() {
1118
1371
  FileService.getTree().then(function (data) {
1119
1372
  var newData = data || [];
1120
1373
  setTreeData(function (prevData) {
1121
- if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
1122
- SearchService.buildIndex(newData);
1374
+ var sig = function(d) { return d.length + ':' + d.map(function(n) { return n.name; }).join(','); };
1375
+ if (sig(newData) !== sig(prevData)) SearchService.buildIndex(newData);
1123
1376
  return newData;
1124
1377
  });
1125
1378
  }).catch(function () {}); // silently ignore auto-refresh errors
@@ -1127,8 +1380,8 @@ var MbeditorApp = function MbeditorApp() {
1127
1380
  return function () { clearInterval(intervalId); };
1128
1381
  }, []);
1129
1382
 
1130
- var handleSelectFile = function handleSelectFile(path, name, line) {
1131
- TabManager.openTab(path, name, line);
1383
+ var handleSelectFile = function handleSelectFile(path, name, line, col) {
1384
+ TabManager.openTab(path, name, line, null, false, col);
1132
1385
  handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
1133
1386
  setQuickOpen(false);
1134
1387
  };
@@ -1168,6 +1421,13 @@ var MbeditorApp = function MbeditorApp() {
1168
1421
  setClosingTabId(id);
1169
1422
  } else {
1170
1423
  TabManager.closeTab(paneId, id);
1424
+ EditorStore.setState({
1425
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1426
+ return EditorStore.getState().panes.some(function (p) {
1427
+ return p.tabs.some(function (t) { return t.id === r.tabId; });
1428
+ });
1429
+ })
1430
+ });
1171
1431
  }
1172
1432
  };
1173
1433
 
@@ -1189,7 +1449,10 @@ var MbeditorApp = function MbeditorApp() {
1189
1449
  return _extends({}, prev, { save: true });
1190
1450
  });
1191
1451
  EditorStore.setStatus("Saving " + tab.name + "...", "info");
1452
+ isSavingRef.current = true;
1192
1453
  FileService.saveFile(tab.path, tab.content).then(function () {
1454
+ recentSavesRef.current[tab.path] = Date.now();
1455
+ setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
1193
1456
  EditorStore.setStatus("Saved", "success");
1194
1457
  SearchService.invalidate();
1195
1458
  GitService.fetchStatus();
@@ -1199,9 +1462,17 @@ var MbeditorApp = function MbeditorApp() {
1199
1462
  _closeEntry.cleanVersionId = _closeEntry.model.getAlternativeVersionId();
1200
1463
  }
1201
1464
  TabManager.closeTab(closingPaneId, tab.id);
1465
+ EditorStore.setState({
1466
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1467
+ return EditorStore.getState().panes.some(function (p) {
1468
+ return p.tabs.some(function (t) { return t.id === r.tabId; });
1469
+ });
1470
+ })
1471
+ });
1202
1472
  })["catch"](function (err) {
1203
1473
  EditorStore.setStatus("Save failed: " + err.message, "error");
1204
1474
  })["finally"](function () {
1475
+ isSavingRef.current = false;
1205
1476
  setLoading(function (prev) {
1206
1477
  return _extends({}, prev, { save: false });
1207
1478
  });
@@ -1210,6 +1481,13 @@ var MbeditorApp = function MbeditorApp() {
1210
1481
  });
1211
1482
  } else {
1212
1483
  TabManager.closeTab(closingPaneId, tab.id);
1484
+ EditorStore.setState({
1485
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1486
+ return EditorStore.getState().panes.some(function (p) {
1487
+ return p.tabs.some(function (t) { return t.id === r.tabId; });
1488
+ });
1489
+ })
1490
+ });
1213
1491
  setClosingTabId(null);
1214
1492
  setClosingPaneId(null);
1215
1493
  }
@@ -1311,6 +1589,142 @@ var MbeditorApp = function MbeditorApp() {
1311
1589
  return function() { window.removeEventListener('beforeinstallprompt', handler); };
1312
1590
  }, []);
1313
1591
 
1592
+ useEffect(function() {
1593
+ FileService.getClientConfig().then(function(cfg) {
1594
+ setCustomPaths(Array.isArray(cfg.related_files_custom_paths) ? cfg.related_files_custom_paths : []);
1595
+ })['catch'](function() {});
1596
+ }, []);
1597
+
1598
+ // Version-update detection: open the changelog tab automatically when the
1599
+ // gem version has changed since last time the editor was opened.
1600
+ useEffect(function() {
1601
+ var SEEN_KEY = 'mbeditor_seen_version';
1602
+ var current = document.body.dataset.mbeditorVersion || '';
1603
+ var seen = localStorage.getItem(SEEN_KEY) || '';
1604
+ if (current && seen && seen !== current) {
1605
+ // Delay slightly so the editor finishes restoring saved tabs first
1606
+ setTimeout(function() { openChangelogTab(); }, 800);
1607
+ }
1608
+ if (current) localStorage.setItem(SEEN_KEY, current);
1609
+ }, []);
1610
+
1611
+ var resourceLabelFromPath = function(p) {
1612
+ if (!p) return null;
1613
+ var parts = p.split('/');
1614
+ var file = parts[parts.length - 1];
1615
+ var name;
1616
+ if (parts[0] === 'app') {
1617
+ if (parts[1] === 'controllers') name = file.replace(/_controller\.rb$/, '');
1618
+ else if (parts[1] === 'models') name = file.replace(/\.rb$/, '');
1619
+ else if (parts[1] === 'views' && parts.length >= 4) name = parts[2];
1620
+ else if (parts[1] === 'helpers') name = file.replace(/_helper\.rb$/, '');
1621
+ else return null;
1622
+ } else if (parts[0] === 'test' || parts[0] === 'spec') {
1623
+ if (parts[1] === 'controllers') name = file.replace(/_controller_(test|spec)\.rb$/, '');
1624
+ else if (parts[1] === 'models') name = file.replace(/_(test|spec)\.rb$/, '');
1625
+ else return null;
1626
+ } else {
1627
+ // Check custom paths
1628
+ var customPaths = customPathsRef.current;
1629
+ for (var ci = 0; ci < customPaths.length; ci++) {
1630
+ var base = customPaths[ci];
1631
+ if (p.startsWith(base + '/')) {
1632
+ var rest = p.slice(base.length + 1);
1633
+ var resource = rest.split('/')[0].replace(/\.[^.]+$/, '');
1634
+ if (resource) {
1635
+ var seg = resource.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
1636
+ return seg;
1637
+ }
1638
+ }
1639
+ }
1640
+ return null;
1641
+ }
1642
+ var seg = (name || '').split('/').pop() || name || '';
1643
+ // Normalize plural→singular so views/users and models/user share one group
1644
+ seg = seg.replace(/ies$/, 'y')
1645
+ .replace(/([^aeiou])es$/, '$1')
1646
+ .replace(/([^s])s$/, '$1');
1647
+ return seg.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
1648
+ };
1649
+
1650
+ var RAILS_MAX_RESOURCES = 10;
1651
+
1652
+ // Map resource label → representative path (capped at RAILS_MAX_RESOURCES, focused pane first)
1653
+ var railsResourceDeps = (function() {
1654
+ var deps = {};
1655
+ var panesOrdered = state.panes.slice().sort(function(a, b) {
1656
+ return a.id === state.focusedPaneId ? -1 : b.id === state.focusedPaneId ? 1 : 0;
1657
+ });
1658
+ panesOrdered.forEach(function(p) {
1659
+ var tabs = p.tabs.slice().sort(function(a, b) {
1660
+ return a.id === p.activeTabId ? -1 : b.id === p.activeTabId ? 1 : 0;
1661
+ });
1662
+ tabs.forEach(function(t) {
1663
+ if (Object.keys(deps).length >= RAILS_MAX_RESOURCES) return;
1664
+ if (!t.path || t.path === '__settings__' || t.path.startsWith('mbeditor://')) return;
1665
+ var label = resourceLabelFromPath(t.path);
1666
+ if (label && !deps[label]) deps[label] = t.path;
1667
+ });
1668
+ });
1669
+ return deps;
1670
+ })();
1671
+ var railsResourceDepStr = Object.keys(railsResourceDeps).sort().join('|');
1672
+
1673
+ var railsOverflow = (function() {
1674
+ var all = {};
1675
+ state.panes.forEach(function(p) {
1676
+ p.tabs.forEach(function(t) {
1677
+ if (!t.path || t.path === '__settings__' || t.path.startsWith('mbeditor://')) return;
1678
+ var label = resourceLabelFromPath(t.path);
1679
+ if (label) all[label] = true;
1680
+ });
1681
+ });
1682
+ return Math.max(0, Object.keys(all).length - Object.keys(railsResourceDeps).length);
1683
+ })();
1684
+
1685
+ var dirtyPaths = (function() {
1686
+ var set = {};
1687
+ state.panes.forEach(function(p) {
1688
+ p.tabs.forEach(function(t) {
1689
+ if (t.dirty && t.path) set[t.path] = true;
1690
+ });
1691
+ });
1692
+ return set;
1693
+ })();
1694
+
1695
+ useEffect(function() {
1696
+ if (activeSidebarTab !== 'rails') return;
1697
+ var labels = Object.keys(railsResourceDeps);
1698
+ if (labels.length === 0) { setRailsFilesMap({}); return; }
1699
+ setRailsFilesMap(function(prev) {
1700
+ var next = {};
1701
+ labels.forEach(function(label) {
1702
+ next[label] = prev[label] ? { files: prev[label].files, loading: true } : { files: null, loading: true };
1703
+ });
1704
+ return next;
1705
+ });
1706
+ labels.forEach(function(label) {
1707
+ var path = railsResourceDeps[label];
1708
+ FileService.getRelatedFiles(path).then(function(data) {
1709
+ setRailsFilesMap(function(prev) {
1710
+ if (!prev.hasOwnProperty(label)) return prev;
1711
+ var next = Object.assign({}, prev);
1712
+ var update = {};
1713
+ update[label] = { files: data, loading: false };
1714
+ return Object.assign(next, update);
1715
+ });
1716
+ })['catch'](function() {
1717
+ setRailsFilesMap(function(prev) {
1718
+ if (!prev.hasOwnProperty(label)) return prev;
1719
+ var next = Object.assign({}, prev);
1720
+ var update = {};
1721
+ update[label] = { files: null, loading: false };
1722
+ return Object.assign(next, update);
1723
+ });
1724
+ });
1725
+ });
1726
+ }, [activeSidebarTab, railsResourceDepStr]);
1727
+
1314
1728
  var focusedPane = state.panes.find(function (p) {
1315
1729
  return p.id === state.focusedPaneId;
1316
1730
  }) || state.panes[0] || null;
@@ -1438,7 +1852,10 @@ var MbeditorApp = function MbeditorApp() {
1438
1852
  return _extends({}, prev, { save: true });
1439
1853
  });
1440
1854
  EditorStore.setStatus("Saving " + tab.name + "...", "info");
1855
+ isSavingRef.current = true;
1441
1856
  FileService.saveFile(tab.path, tab.content).then(function () {
1857
+ recentSavesRef.current[tab.path] = Date.now();
1858
+ setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
1442
1859
  var newPanes = EditorStore.getState().panes.map(function (p) {
1443
1860
  if (p.id === paneId) {
1444
1861
  return _extends({}, p, { tabs: p.tabs.map(function (t) {
@@ -1455,6 +1872,9 @@ var MbeditorApp = function MbeditorApp() {
1455
1872
  }
1456
1873
  EditorStore.setStatus("Saved", "success");
1457
1874
  _clearDraft(tab.path);
1875
+ if (typeof HistoryService !== 'undefined') {
1876
+ HistoryService.flushForPath(tab.path);
1877
+ }
1458
1878
  SearchService.invalidate();
1459
1879
 
1460
1880
  // Hot reload for Markdown: sync preview tab after save
@@ -1466,12 +1886,76 @@ var MbeditorApp = function MbeditorApp() {
1466
1886
  })["catch"](function (err) {
1467
1887
  EditorStore.setStatus("Save failed: " + err.message, "error");
1468
1888
  })["finally"](function () {
1889
+ isSavingRef.current = false;
1469
1890
  return setLoading(function (prev) {
1470
1891
  return _extends({}, prev, { save: false });
1471
1892
  });
1472
1893
  });
1473
1894
  };
1474
1895
 
1896
+ function dismissPendingReload(reload) {
1897
+ EditorStore.setState({
1898
+ pendingReloads: EditorStore.getState().pendingReloads.filter(function (r) {
1899
+ return !(r.paneId === reload.paneId && r.tabId === reload.tabId);
1900
+ })
1901
+ });
1902
+ }
1903
+
1904
+ function handleSaveAndReload(reload) {
1905
+ var st = EditorStore.getState();
1906
+ var pane = st.panes.find(function (p) { return p.id === reload.paneId; });
1907
+ var tab = pane && pane.tabs.find(function (t) { return t.id === reload.tabId; });
1908
+ if (!tab) { dismissPendingReload(reload); return; }
1909
+ isSavingRef.current = true;
1910
+ FileService.saveFile(tab.path, tab.content).then(function () {
1911
+ recentSavesRef.current[tab.path] = Date.now();
1912
+ setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
1913
+ EditorStore.setState({
1914
+ panes: EditorStore.getState().panes.map(function (p) {
1915
+ if (p.id !== reload.paneId) return p;
1916
+ return Object.assign({}, p, {
1917
+ tabs: p.tabs.map(function (t) {
1918
+ if (t.id !== reload.tabId) return t;
1919
+ return Object.assign({}, t, {
1920
+ content: tab.content,
1921
+ dirty: false,
1922
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1923
+ });
1924
+ })
1925
+ });
1926
+ })
1927
+ });
1928
+ dismissPendingReload(reload);
1929
+ })["catch"](function () {
1930
+ EditorStore.setStatus('Save failed — cannot reload', 'error');
1931
+ })["finally"](function () {
1932
+ isSavingRef.current = false;
1933
+ });
1934
+ }
1935
+
1936
+ function handleDiscardAndReload(reload) {
1937
+ EditorStore.setState({
1938
+ panes: EditorStore.getState().panes.map(function (p) {
1939
+ if (p.id !== reload.paneId) return p;
1940
+ return Object.assign({}, p, {
1941
+ tabs: p.tabs.map(function (t) {
1942
+ if (t.id !== reload.tabId) return t;
1943
+ return Object.assign({}, t, {
1944
+ content: reload.serverContent,
1945
+ dirty: false,
1946
+ externalContentVersion: (t.externalContentVersion || 0) + 1
1947
+ });
1948
+ })
1949
+ });
1950
+ })
1951
+ });
1952
+ dismissPendingReload(reload);
1953
+ }
1954
+
1955
+ function handleKeepMine(reload) {
1956
+ dismissPendingReload(reload);
1957
+ }
1958
+
1475
1959
  var handleSaveAll = function handleSaveAll() {
1476
1960
  var dirtyTabs = state.panes.flatMap(function (p) {
1477
1961
  return p.tabs;
@@ -1484,10 +1968,16 @@ var MbeditorApp = function MbeditorApp() {
1484
1968
  return _extends({}, prev, { saveAll: true });
1485
1969
  });
1486
1970
  EditorStore.setStatus("Saving " + dirtyTabs.length + " files...", "info");
1971
+ isSavingRef.current = true;
1487
1972
  var promises = dirtyTabs.map(function (tab) {
1488
1973
  return FileService.saveFile(tab.path, tab.content);
1489
1974
  });
1490
1975
  Promise.all(promises).then(function () {
1976
+ var now = Date.now();
1977
+ dirtyTabs.forEach(function(tab) {
1978
+ recentSavesRef.current[tab.path] = now;
1979
+ setTimeout(function() { delete recentSavesRef.current[tab.path]; }, 3500);
1980
+ });
1491
1981
  var newPanes = EditorStore.getState().panes.map(function (p) {
1492
1982
  return _extends({}, p, { tabs: p.tabs.map(function (t) {
1493
1983
  return _extends({}, t, { dirty: false, cleanContent: t.content });
@@ -1508,6 +1998,7 @@ var MbeditorApp = function MbeditorApp() {
1508
1998
  })["catch"](function (err) {
1509
1999
  EditorStore.setStatus("Failed to save some files", "error");
1510
2000
  })["finally"](function () {
2001
+ isSavingRef.current = false;
1511
2002
  return setLoading(function (prev) {
1512
2003
  return _extends({}, prev, { saveAll: false });
1513
2004
  });
@@ -1558,10 +2049,33 @@ var MbeditorApp = function MbeditorApp() {
1558
2049
  EditorStore.setStatus('Line endings changed to ' + newEOL, 'info');
1559
2050
  };
1560
2051
 
2052
+ var handleRefreshWorkspace = function handleRefreshWorkspace() {
2053
+ setLoading(function (prev) {
2054
+ return _extends({}, prev, { refreshWorkspace: true });
2055
+ });
2056
+ GitService.fetchStatus()["catch"](function () {});
2057
+ FileService.getTree().then(function (data) {
2058
+ var newData = data || [];
2059
+ setTreeData(function (prevData) {
2060
+ if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
2061
+ SearchService.buildIndex(newData);
2062
+ return newData;
2063
+ });
2064
+ checkOpenTabsForExternalChanges();
2065
+ EditorStore.setStatus("Workspace refreshed", "success");
2066
+ })["catch"](function (err) {
2067
+ EditorStore.setStatus("Failed to refresh workspace", "error");
2068
+ })["finally"](function () {
2069
+ setLoading(function (prev) {
2070
+ return _extends({}, prev, { refreshWorkspace: false });
2071
+ });
2072
+ });
2073
+ };
2074
+
1561
2075
  var handleFormat = function handleFormat() {
1562
2076
  if (!activeTab) return;
1563
2077
 
1564
- var isRubyLang = activeTab.path.endsWith('.rb') || activeTab.path.endsWith('.gemspec') || activeTab.path.endsWith("Rakefile") || activeTab.path.endsWith("Gemfile");
2078
+ var isRubyLang = activeTab.path.endsWith('.rb') || activeTab.path.endsWith('.rake') || activeTab.path.endsWith('.gemspec') || activeTab.path.endsWith("Rakefile") || activeTab.path.endsWith("Gemfile");
1565
2079
 
1566
2080
  if (isRubyLang && !rubocopAvailable) {
1567
2081
  EditorStore.setStatus("RuboCop is not available for this workspace.", "warning");
@@ -1573,7 +2087,13 @@ var MbeditorApp = function MbeditorApp() {
1573
2087
  return _extends({}, prev, { format: true });
1574
2088
  });
1575
2089
  EditorStore.setStatus("Formatting...", "info");
1576
- FileService.formatFile(activeTab.path, activeTab.content).then(function (res) {
2090
+ var originalContent = activeTab.content;
2091
+ var codeToFormat = originalContent;
2092
+ if (editorPrefs.insertSpaces === false) {
2093
+ var detectedWidth = detectIndentWidth(originalContent);
2094
+ if (detectedWidth > 0) codeToFormat = spacesToTabs(originalContent, detectedWidth);
2095
+ }
2096
+ FileService.formatFile(activeTab.path, codeToFormat).then(function (res) {
1577
2097
  if (res.content) {
1578
2098
  // Update content and mark dirty — user decides when to save.
1579
2099
  // The executeEdits path in EditorPanel preserves the undo stack.
@@ -1584,6 +2104,19 @@ var MbeditorApp = function MbeditorApp() {
1584
2104
  return p;
1585
2105
  });
1586
2106
  EditorStore.setState({ panes: newPanes });
2107
+
2108
+ // Highlight changed lines briefly
2109
+ var monacoEditor = window.__mbeditorActiveEditor;
2110
+ if (monacoEditor && res.content !== originalContent) {
2111
+ var changedLineNums = diffLines(originalContent.split('\n'), res.content.split('\n'));
2112
+ if (changedLineNums.length > 0) {
2113
+ var decorations = changedLineNums.map(function(ln) {
2114
+ return { range: new monaco.Range(ln, 1, ln, 1), options: { isWholeLine: true, className: 'mbeditor-format-changed' } };
2115
+ });
2116
+ var ids = monacoEditor.deltaDecorations([], decorations);
2117
+ setTimeout(function() { monacoEditor.deltaDecorations(ids, []); }, 3000);
2118
+ }
2119
+ }
1587
2120
  }
1588
2121
  EditorStore.setStatus("Formatted (Unsaved)", "success");
1589
2122
  GitService.fetchStatus();
@@ -2061,13 +2594,17 @@ var MbeditorApp = function MbeditorApp() {
2061
2594
  document.body.style.userSelect = 'none';
2062
2595
  };
2063
2596
 
2064
- var toggleSidebarCollapsed = function toggleSidebarCollapsed() {
2065
- setSidebarCollapsed(function (prev) { return !prev; });
2066
- };
2067
-
2068
- var expandSidebarTo = function expandSidebarTo(tab) {
2069
- setActiveSidebarTab(tab);
2070
- setSidebarCollapsed(false);
2597
+ var handleActivityBarClick = function handleActivityBarClick(tab) {
2598
+ if (tab === 'settings') {
2599
+ openSettingsTab();
2600
+ return;
2601
+ }
2602
+ if (!sidebarCollapsed && activeSidebarTab === tab) {
2603
+ setSidebarCollapsed(true);
2604
+ } else {
2605
+ setActiveSidebarTab(tab);
2606
+ setSidebarCollapsed(false);
2607
+ }
2071
2608
  };
2072
2609
 
2073
2610
  var openFileFromGitPanel = function openFileFromGitPanel(path, name) {
@@ -2483,6 +3020,45 @@ var MbeditorApp = function MbeditorApp() {
2483
3020
  EditorStore.setState({ panes: newPanes2, focusedPaneId: paneId, activeTabId: '__settings__' });
2484
3021
  }
2485
3022
 
3023
+ var CHANGELOG_TAB_ID = 'mbeditor://changelog';
3024
+ function openChangelogTab() {
3025
+ var st = EditorStore.getState();
3026
+ // Focus existing tab if already open
3027
+ var foundPaneId = null, foundTab = null;
3028
+ st.panes.forEach(function(p) {
3029
+ if (!foundTab) {
3030
+ var t = p.tabs.find(function(tab) { return tab.id === CHANGELOG_TAB_ID; });
3031
+ if (t) { foundTab = t; foundPaneId = p.id; }
3032
+ }
3033
+ });
3034
+ if (foundTab) {
3035
+ var switchPanes = st.panes.map(function(p) {
3036
+ if (p.id === foundPaneId) return Object.assign({}, p, { activeTabId: CHANGELOG_TAB_ID });
3037
+ return p;
3038
+ });
3039
+ EditorStore.setState({ panes: switchPanes, focusedPaneId: foundPaneId });
3040
+ return;
3041
+ }
3042
+ // Open in focused pane
3043
+ var paneId = st.focusedPaneId;
3044
+ var pane = st.panes.find(function(p) { return p.id === paneId; }) || st.panes[0];
3045
+ if (!pane) return;
3046
+ paneId = pane.id;
3047
+ var newTab = { id: CHANGELOG_TAB_ID, path: CHANGELOG_TAB_ID, name: "What's New", dirty: false, content: '', isChangelog: true };
3048
+ var newPanes = st.panes.map(function(p) {
3049
+ if (p.id === paneId) return Object.assign({}, p, { tabs: p.tabs.concat(newTab), activeTabId: CHANGELOG_TAB_ID });
3050
+ return p;
3051
+ });
3052
+ EditorStore.setState({ panes: newPanes, focusedPaneId: paneId });
3053
+ // Fetch content if not already loaded
3054
+ if (!changelogState || changelogState.error) {
3055
+ setChangelogState({ loading: true, content: null, error: null });
3056
+ FileService.getChangelog()
3057
+ .then(function(data) { setChangelogState({ loading: false, content: data.content || '', error: null }); })
3058
+ ['catch'](function() { setChangelogState({ loading: false, content: null, error: 'Could not load changelog.' }); });
3059
+ }
3060
+ }
3061
+
2486
3062
  return React.createElement(
2487
3063
  "div",
2488
3064
  { className: "ide-shell" },
@@ -2589,74 +3165,69 @@ var MbeditorApp = function MbeditorApp() {
2589
3165
  React.createElement(
2590
3166
  "div",
2591
3167
  { className: "ide-body", id: "ide-body-container" },
2592
- React.createElement(
3168
+ /* Activity bar — always visible, 48px wide */
3169
+ !zenMode && React.createElement(
2593
3170
  "div",
2594
- { className: "ide-sidebar" + (sidebarCollapsed ? " ide-sidebar-collapsed" : ""), style: { width: (sidebarCollapsed ? SIDEBAR_COLLAPSED_WIDTH : sidebarWidth) + "px", display: zenMode ? 'none' : undefined } },
2595
- sidebarCollapsed
2596
- ? React.createElement(
2597
- "div",
2598
- { className: "sidebar-icon-strip" },
2599
- React.createElement(
2600
- "div",
2601
- { className: "sidebar-strip-top" },
2602
- React.createElement(
2603
- "div",
2604
- { className: "sidebar-nav-group", "data-group": "nav" },
2605
- React.createElement(
2606
- "button",
2607
- { type: "button", className: "sidebar-strip-btn " + (activeSidebarTab === 'explorer' ? 'active' : ''), title: "Explorer", onClick: function () { return expandSidebarTo('explorer'); } },
2608
- React.createElement("i", { className: "far fa-folder" })
2609
- ),
2610
- React.createElement(
2611
- "button",
2612
- { type: "button", className: "sidebar-strip-btn " + (activeSidebarTab === 'search' ? 'active' : ''), title: "Search", onClick: function () { return expandSidebarTo('search'); } },
2613
- React.createElement("i", { className: "fas fa-search" })
2614
- ),
2615
- React.createElement(
2616
- "button",
2617
- { type: "button", className: "sidebar-strip-btn", title: "Editor Preferences", onClick: openSettingsTab },
2618
- React.createElement("i", { className: "fas fa-cog" })
2619
- )
2620
- )
2621
- ),
2622
- React.createElement(
2623
- "div",
2624
- { className: "sidebar-strip-bottom" },
2625
- React.createElement(
2626
- "button",
2627
- { type: "button", className: "sidebar-strip-btn", title: "Expand sidebar", onClick: toggleSidebarCollapsed },
2628
- React.createElement("i", { className: "fas fa-chevron-right" })
2629
- )
2630
- )
2631
- )
2632
- : React.createElement(
2633
- React.Fragment,
2634
- null,
2635
- React.createElement(
2636
- "div",
2637
- { className: "ide-sidebar-tabs" },
2638
- React.createElement(
2639
- "button",
2640
- { type: "button", className: "ide-sidebar-tab " + (activeSidebarTab === 'explorer' ? 'active' : ''), onClick: function () { return setActiveSidebarTab('explorer'); } },
2641
- "EXPLORER"
2642
- ),
2643
- React.createElement(
2644
- "button",
2645
- { type: "button", className: "ide-sidebar-tab " + (activeSidebarTab === 'search' ? 'active' : ''), onClick: function () { return setActiveSidebarTab('search'); } },
2646
- "SEARCH"
2647
- ),
2648
- React.createElement(
2649
- "button",
2650
- { type: "button", className: "ide-sidebar-tab ide-sidebar-tab-icon", title: "Editor Preferences", onClick: openSettingsTab },
2651
- React.createElement("i", { className: "fas fa-cog" })
2652
- ),
2653
- React.createElement(
2654
- "button",
2655
- { type: "button", className: "sidebar-strip-btn", title: "Collapse sidebar", onClick: toggleSidebarCollapsed },
2656
- React.createElement("i", { className: "fas fa-chevron-left" })
2657
- )
2658
- ),
2659
- activeSidebarTab === 'explorer' && React.createElement(
3171
+ { className: "ide-activity-bar" },
3172
+ React.createElement(
3173
+ "div",
3174
+ { className: "ide-activity-bar-top" },
3175
+ React.createElement(
3176
+ "button",
3177
+ {
3178
+ type: "button",
3179
+ className: "ide-activity-btn" + (!sidebarCollapsed && activeSidebarTab === 'explorer' ? ' active' : ''),
3180
+ title: "Explorer",
3181
+ onClick: function() { handleActivityBarClick('explorer'); }
3182
+ },
3183
+ React.createElement("i", { className: "far fa-folder" })
3184
+ ),
3185
+ React.createElement(
3186
+ "button",
3187
+ {
3188
+ type: "button",
3189
+ className: "ide-activity-btn" + (!sidebarCollapsed && activeSidebarTab === 'search' ? ' active' : ''),
3190
+ title: "Search",
3191
+ onClick: function() { handleActivityBarClick('search'); }
3192
+ },
3193
+ React.createElement("i", { className: "fas fa-search" })
3194
+ ),
3195
+ React.createElement(
3196
+ "button",
3197
+ {
3198
+ type: "button",
3199
+ className: "ide-activity-btn" + (!sidebarCollapsed && activeSidebarTab === 'rails' ? ' active' : ''),
3200
+ title: "Rails",
3201
+ onClick: function() { handleActivityBarClick('rails'); }
3202
+ },
3203
+ React.createElement("i", { className: "far fa-gem" })
3204
+ )
3205
+ ),
3206
+ React.createElement(
3207
+ "div",
3208
+ { className: "ide-activity-bar-bottom" },
3209
+ React.createElement(
3210
+ "button",
3211
+ {
3212
+ type: "button",
3213
+ className: "ide-activity-btn" + (activeTab && activeTab.isSettings ? ' active' : ''),
3214
+ title: "Editor Preferences",
3215
+ onClick: openSettingsTab
3216
+ },
3217
+ React.createElement("i", { className: "fas fa-cog" })
3218
+ )
3219
+ )
3220
+ ),
3221
+ /* Panel content — shown when not collapsed and not in zen mode */
3222
+ !sidebarCollapsed && !zenMode && React.createElement(
3223
+ "div",
3224
+ { className: "ide-sidebar", style: { width: sidebarWidth + "px" } },
3225
+ React.createElement("div", { className: "sidebar-panel-title" },
3226
+ activeSidebarTab === 'explorer' ? 'Explorer' :
3227
+ activeSidebarTab === 'search' ? 'Search' :
3228
+ activeSidebarTab === 'rails' ? 'Rails' : ''
3229
+ ),
3230
+ activeSidebarTab === 'explorer' && React.createElement(
2660
3231
  "div",
2661
3232
  { className: "ide-sidebar-content" },
2662
3233
  React.createElement(
@@ -2789,6 +3360,13 @@ var MbeditorApp = function MbeditorApp() {
2789
3360
  actions: React.createElement(
2790
3361
  SectionActionGroup,
2791
3362
  { ariaLabel: "Project actions" },
3363
+ React.createElement(SidebarActionButton, {
3364
+ title: "Refresh workspace",
3365
+ iconClass: "fas fa-sync-alt",
3366
+ ariaBusy: !!loading.refreshWorkspace,
3367
+ onClick: handleRefreshWorkspace,
3368
+ disabled: !!loading.refreshWorkspace
3369
+ }),
2792
3370
  React.createElement(SidebarActionButton, {
2793
3371
  title: "Collapse all folders",
2794
3372
  iconClass: "fas fa-compress-alt",
@@ -2986,7 +3564,7 @@ var MbeditorApp = function MbeditorApp() {
2986
3564
  {
2987
3565
  key: i,
2988
3566
  className: "search-result-item",
2989
- onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line); }; })(res)
3567
+ onClick: (function(r) { return function() { handleSelectFile(r.file, r.file.split('/').pop(), r.line, r.col); }; })(res)
2990
3568
  },
2991
3569
  React.createElement("i", { className: (window.getFileIcon ? window.getFileIcon(fileName) : 'far fa-file-code') + " search-result-icon" }),
2992
3570
  React.createElement(
@@ -3014,10 +3592,120 @@ var MbeditorApp = function MbeditorApp() {
3014
3592
  )
3015
3593
  );
3016
3594
  })()
3595
+ ),
3596
+ activeSidebarTab === 'rails' && React.createElement(
3597
+ "div",
3598
+ { className: "rails-panel" },
3599
+ (function() {
3600
+ var labels = Object.keys(railsFilesMap).sort();
3601
+ if (labels.length === 0) {
3602
+ return React.createElement("div", { className: "rails-panel-empty" }, "Open a Rails file to see related files.");
3603
+ }
3604
+ var sections = labels.map(function(label) {
3605
+ var entry = railsFilesMap[label];
3606
+ var files = entry && entry.files;
3607
+ var loading = entry && entry.loading;
3608
+ if (loading && !files) {
3609
+ return React.createElement("div", { key: label + '_loading', className: "rails-panel-loading" },
3610
+ React.createElement("i", { className: "fas fa-spinner fa-spin" }),
3611
+ " Loading…"
3612
+ );
3613
+ }
3614
+ if (!files || Object.keys(files).length === 0) return null;
3615
+ var allFiles = [];
3616
+ ['model', 'controller', 'helper', 'concerns', 'tests', 'views'].forEach(function(key) {
3617
+ var group = files[key];
3618
+ if (group && group.length) allFiles = allFiles.concat(group);
3619
+ });
3620
+ var customGroups = files['custom'];
3621
+ if (customGroups && typeof customGroups === 'object') {
3622
+ Object.keys(customGroups).forEach(function(base) {
3623
+ var grpFiles = customGroups[base];
3624
+ if (grpFiles && grpFiles.length) allFiles = allFiles.concat(grpFiles);
3625
+ });
3626
+ }
3627
+ if (allFiles.length === 0) return null;
3628
+ var schemaBtn = React.createElement(
3629
+ 'button',
3630
+ {
3631
+ className: 'rails-schema-btn' + (schemaLoadingLabel === label ? ' rails-schema-btn-loading' : ''),
3632
+ title: 'View database schema for ' + label,
3633
+ onClick: (function(lbl) { return function(e) {
3634
+ e.stopPropagation();
3635
+ if (schemaLoadingLabel === lbl) return;
3636
+ setSchemaLoadingLabel(lbl);
3637
+ var modelName = lbl.replace(/\s+/g, '');
3638
+ FileService.getModelSchema(modelName)
3639
+ .then(function(data) {
3640
+ setSchemaLoadingLabel(null);
3641
+ if (data && data.columns) {
3642
+ setSchemaModal({ label: lbl, data: data });
3643
+ } else {
3644
+ setSchemaModal({ label: lbl, error: 'No schema found for ' + lbl });
3645
+ }
3646
+ })
3647
+ ['catch'](function(err) {
3648
+ setSchemaLoadingLabel(null);
3649
+ var msg = (err && err.response && err.response.data && err.response.data.error)
3650
+ ? err.response.data.error
3651
+ : 'No db/schema.rb found or table not defined';
3652
+ setSchemaModal({ label: lbl, error: msg });
3653
+ });
3654
+ }; })(label)
3655
+ },
3656
+ React.createElement('i', {
3657
+ className: schemaLoadingLabel === label
3658
+ ? 'fas fa-spinner fa-spin'
3659
+ : 'fas fa-table'
3660
+ })
3661
+ );
3662
+ return React.createElement(
3663
+ CollapsibleSection,
3664
+ {
3665
+ key: label,
3666
+ title: label.toUpperCase(),
3667
+ isCollapsed: !!railsGroupsCollapsed[label],
3668
+ actions: schemaBtn,
3669
+ onToggle: (function(captured) { return function(isCollapsed) {
3670
+ setRailsGroupsCollapsed(function(prev) {
3671
+ var next = Object.assign({}, prev);
3672
+ next[captured] = isCollapsed;
3673
+ return next;
3674
+ });
3675
+ }; })(label)
3676
+ },
3677
+ React.createElement(
3678
+ "div",
3679
+ null,
3680
+ allFiles.map(function(f) {
3681
+ return React.createElement(
3682
+ "div", {
3683
+ key: f.path,
3684
+ className: "rails-group-item",
3685
+ onClick: (function(file) { return function() { handleSelectFile(file.path, file.name); }; })(f),
3686
+ title: f.path
3687
+ },
3688
+ React.createElement("i", { className: "tree-item-icon " + (window.getFileIcon ? window.getFileIcon(f.name) : 'far fa-file-code') + " tree-file-icon" }),
3689
+ React.createElement("span", { className: "rails-group-item-name" }, f.name),
3690
+ f.kind && React.createElement("span", { className: "rails-group-item-kind" }, f.kind),
3691
+ dirtyPaths[f.path] && React.createElement("span", { className: "rails-group-item-dirty" }, "●")
3692
+ );
3693
+ })
3694
+ )
3695
+ );
3696
+ });
3697
+ if (railsOverflow > 0) {
3698
+ sections = sections.concat([React.createElement(
3699
+ "div", { key: '__overflow', className: "rails-panel-overflow" },
3700
+ "+" + railsOverflow + " more — close tabs to show all"
3701
+ )]);
3702
+ }
3703
+ return sections;
3704
+ })()
3017
3705
  )
3018
- )
3019
- ),
3020
- React.createElement("div", {
3706
+ ),
3707
+ /* Sidebar resize divider — only when panel is open */
3708
+ !sidebarCollapsed && !zenMode && React.createElement("div", {
3021
3709
  className: "panel-divider sidebar-divider " + (activeResizeMode === 'sidebar' ? 'active' : ''),
3022
3710
  onMouseDown: startSidebarResize,
3023
3711
  role: "separator",
@@ -3029,7 +3717,7 @@ var MbeditorApp = function MbeditorApp() {
3029
3717
  {
3030
3718
  id: "ide-main-split-container",
3031
3719
  className: "ide-main",
3032
- style: { display: 'flex', flexDirection: 'row', width: '100%', height: '100%', cursor: activeResizeMode === 'pane' ? 'col-resize' : 'default', userSelect: activeResizeMode ? 'none' : 'auto' },
3720
+ style: { position: 'relative', display: 'flex', flexDirection: 'row', width: '100%', height: '100%', cursor: activeResizeMode === 'pane' ? 'col-resize' : 'default', userSelect: activeResizeMode ? 'none' : 'auto' },
3033
3721
  onDragOverCapture: function (e) {
3034
3722
  if (!draggedTab) return;
3035
3723
  e.preventDefault();
@@ -3081,6 +3769,9 @@ var MbeditorApp = function MbeditorApp() {
3081
3769
  }
3082
3770
  }
3083
3771
  },
3772
+ isSwitchingBranch && React.createElement('div', { className: 'branch-switch-overlay' },
3773
+ React.createElement('span', null, 'Switching branch…')
3774
+ ),
3084
3775
  state.panes.map(function (pane, idx) {
3085
3776
  // Show empty pane 2 as a drop zone only when the cursor is actively hovering
3086
3777
  // over its half of the editor content (dragOverPaneId === 2).
@@ -3116,6 +3807,18 @@ var MbeditorApp = function MbeditorApp() {
3116
3807
  commits: pActiveTab.commits || [],
3117
3808
  onSelectCommit: handleSelectCommit
3118
3809
  });
3810
+ } else if (pActiveTab.isChangelog) {
3811
+ content = React.createElement(ChangelogView, {
3812
+ changelogState: changelogState,
3813
+ onLoad: function() {
3814
+ if (!changelogState || (!changelogState.content && !changelogState.loading && !changelogState.error)) {
3815
+ setChangelogState({ loading: true, content: null, error: null });
3816
+ FileService.getChangelog()
3817
+ .then(function(data) { setChangelogState({ loading: false, content: data.content || '', error: null }); })
3818
+ ['catch'](function() { setChangelogState({ loading: false, content: null, error: 'Could not load changelog.' }); });
3819
+ }
3820
+ }
3821
+ });
3119
3822
  } else if (pActiveTab.isSettings) {
3120
3823
  content = React.createElement(
3121
3824
  'div',
@@ -3127,7 +3830,7 @@ var MbeditorApp = function MbeditorApp() {
3127
3830
  /* ── Appearance ──────────────────────────────── */
3128
3831
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Appearance'),
3129
3832
  React.createElement(
3130
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
3833
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Color theme for the editor' },
3131
3834
  React.createElement('span', { className: 'ide-settings-label' }, 'Theme'),
3132
3835
  React.createElement(
3133
3836
  'select', {
@@ -3148,7 +3851,7 @@ var MbeditorApp = function MbeditorApp() {
3148
3851
  )
3149
3852
  ),
3150
3853
  React.createElement(
3151
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
3854
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Editor font size in pixels (8–32)' },
3152
3855
  React.createElement('span', { className: 'ide-settings-label' }, 'Font size'),
3153
3856
  React.createElement('input', {
3154
3857
  key: String(editorPrefs.fontSize || 13),
@@ -3166,7 +3869,7 @@ var MbeditorApp = function MbeditorApp() {
3166
3869
  })
3167
3870
  ),
3168
3871
  React.createElement(
3169
- 'label', { className: 'ide-settings-row-full' },
3872
+ 'label', { className: 'ide-settings-row-full', title: 'Font stack used in the editor — the first font available on your system is used' },
3170
3873
  React.createElement('span', { className: 'ide-settings-label' }, 'Font family'),
3171
3874
  React.createElement('input', {
3172
3875
  type: 'text',
@@ -3215,7 +3918,7 @@ var MbeditorApp = function MbeditorApp() {
3215
3918
  /* ── Indentation (unified editor + Prettier) ── */
3216
3919
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Indentation'),
3217
3920
  React.createElement(
3218
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
3921
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Number of spaces per indentation level (also sets Prettier tab width)' },
3219
3922
  React.createElement('span', { className: 'ide-settings-label' }, 'Tab size'),
3220
3923
  React.createElement('input', {
3221
3924
  key: String(editorPrefs.tabSize || 4),
@@ -3233,7 +3936,7 @@ var MbeditorApp = function MbeditorApp() {
3233
3936
  })
3234
3937
  ),
3235
3938
  React.createElement(
3236
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
3939
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Insert spaces instead of tab characters when pressing Tab' },
3237
3940
  React.createElement('span', { className: 'ide-settings-label' }, 'Use spaces'),
3238
3941
  React.createElement('input', {
3239
3942
  type: 'checkbox',
@@ -3246,7 +3949,7 @@ var MbeditorApp = function MbeditorApp() {
3246
3949
  /* ── Editor ──────────────────────────────────── */
3247
3950
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Editor'),
3248
3951
  React.createElement(
3249
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
3952
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'How long lines are handled — Off: scroll horizontally, On: wrap at viewport width, Column: wrap at a fixed column' },
3250
3953
  React.createElement('span', { className: 'ide-settings-label' }, 'Word wrap'),
3251
3954
  React.createElement(
3252
3955
  'select', {
@@ -3259,7 +3962,7 @@ var MbeditorApp = function MbeditorApp() {
3259
3962
  )
3260
3963
  ),
3261
3964
  React.createElement(
3262
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
3965
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Show line numbers in the gutter — On, Off, or Relative (useful with Vim mode)' },
3263
3966
  React.createElement('span', { className: 'ide-settings-label' }, 'Line numbers'),
3264
3967
  React.createElement(
3265
3968
  'select', {
@@ -3272,7 +3975,7 @@ var MbeditorApp = function MbeditorApp() {
3272
3975
  )
3273
3976
  ),
3274
3977
  React.createElement(
3275
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
3978
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Render whitespace characters visually — None, Selection only, Boundary (leading/trailing), or All' },
3276
3979
  React.createElement('span', { className: 'ide-settings-label' }, 'Whitespace'),
3277
3980
  React.createElement(
3278
3981
  'select', {
@@ -3286,7 +3989,7 @@ var MbeditorApp = function MbeditorApp() {
3286
3989
  )
3287
3990
  ),
3288
3991
  React.createElement(
3289
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
3992
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show a scaled-down overview of the file on the right edge of the editor' },
3290
3993
  React.createElement('span', { className: 'ide-settings-label' }, 'Minimap'),
3291
3994
  React.createElement('input', {
3292
3995
  type: 'checkbox',
@@ -3296,7 +3999,7 @@ var MbeditorApp = function MbeditorApp() {
3296
3999
  })
3297
4000
  ),
3298
4001
  React.createElement(
3299
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4002
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Allow scrolling past the last line so it can be positioned at the top of the viewport' },
3300
4003
  React.createElement('span', { className: 'ide-settings-label' }, 'Scroll past end'),
3301
4004
  React.createElement('input', {
3302
4005
  type: 'checkbox',
@@ -3306,7 +4009,7 @@ var MbeditorApp = function MbeditorApp() {
3306
4009
  })
3307
4010
  ),
3308
4011
  React.createElement(
3309
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4012
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Colorize matching bracket pairs with distinct colors to make nesting easier to read' },
3310
4013
  React.createElement('span', { className: 'ide-settings-label' }, 'Bracket colors'),
3311
4014
  React.createElement('input', {
3312
4015
  type: 'checkbox',
@@ -3316,7 +4019,7 @@ var MbeditorApp = function MbeditorApp() {
3316
4019
  })
3317
4020
  ),
3318
4021
  React.createElement(
3319
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4022
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Enable Vim keybindings (Normal/Insert/Visual modes). Press Escape to return to Normal mode.' },
3320
4023
  React.createElement('span', { className: 'ide-settings-label' }, 'Vim mode'),
3321
4024
  React.createElement('input', {
3322
4025
  type: 'checkbox',
@@ -3368,7 +4071,7 @@ var MbeditorApp = function MbeditorApp() {
3368
4071
  )
3369
4072
  ),
3370
4073
  React.createElement(
3371
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
4074
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Shape of the text cursor in the editor' },
3372
4075
  React.createElement('span', { className: 'ide-settings-label' }, 'Cursor style'),
3373
4076
  React.createElement(
3374
4077
  'select', {
@@ -3384,7 +4087,7 @@ var MbeditorApp = function MbeditorApp() {
3384
4087
  )
3385
4088
  ),
3386
4089
  React.createElement(
3387
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
4090
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Cursor animation style — Blink (on/off), Smooth (fade), Phase (offset fade), Expand (grow), or Solid (no animation)' },
3388
4091
  React.createElement('span', { className: 'ide-settings-label' }, 'Cursor blinking'),
3389
4092
  React.createElement(
3390
4093
  'select', {
@@ -3507,7 +4210,7 @@ var MbeditorApp = function MbeditorApp() {
3507
4210
  /* ── Formatting (Prettier) ───────────────────── */
3508
4211
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Formatting'),
3509
4212
  React.createElement(
3510
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
4213
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Prettier: maximum line length before wrapping (40–200)' },
3511
4214
  React.createElement('span', { className: 'ide-settings-label' }, 'Print width'),
3512
4215
  React.createElement('input', {
3513
4216
  key: String(editorPrefs.prettierPrintWidth != null ? editorPrefs.prettierPrintWidth : 80),
@@ -3525,7 +4228,7 @@ var MbeditorApp = function MbeditorApp() {
3525
4228
  })
3526
4229
  ),
3527
4230
  React.createElement(
3528
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
4231
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Prettier: add trailing commas in multi-line expressions — All (ES2017+), ES5 (objects/arrays only), or None' },
3529
4232
  React.createElement('span', { className: 'ide-settings-label' }, 'Trailing commas'),
3530
4233
  React.createElement(
3531
4234
  'select', {
@@ -3538,7 +4241,7 @@ var MbeditorApp = function MbeditorApp() {
3538
4241
  )
3539
4242
  ),
3540
4243
  React.createElement(
3541
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4244
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Prettier: add semicolons at the end of statements' },
3542
4245
  React.createElement('span', { className: 'ide-settings-label' }, 'Semicolons'),
3543
4246
  React.createElement('input', {
3544
4247
  type: 'checkbox',
@@ -3548,7 +4251,7 @@ var MbeditorApp = function MbeditorApp() {
3548
4251
  })
3549
4252
  ),
3550
4253
  React.createElement(
3551
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4254
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: "Prettier: use single quotes instead of double quotes for strings" },
3552
4255
  React.createElement('span', { className: 'ide-settings-label' }, 'Single quotes'),
3553
4256
  React.createElement('input', {
3554
4257
  type: 'checkbox',
@@ -3558,7 +4261,7 @@ var MbeditorApp = function MbeditorApp() {
3558
4261
  })
3559
4262
  ),
3560
4263
  React.createElement(
3561
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4264
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Prettier: add spaces inside object literal braces, e.g. { a: 1 } vs {a: 1}' },
3562
4265
  React.createElement('span', { className: 'ide-settings-label' }, 'Bracket spacing'),
3563
4266
  React.createElement('input', {
3564
4267
  type: 'checkbox',
@@ -3571,7 +4274,7 @@ var MbeditorApp = function MbeditorApp() {
3571
4274
  /* ── Interface ───────────────────────────────── */
3572
4275
  React.createElement('div', { className: 'ide-settings-section-header' }, 'Interface'),
3573
4276
  React.createElement(
3574
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4277
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Automatically scroll the file explorer to reveal and highlight the file you are editing' },
3575
4278
  React.createElement('span', { className: 'ide-settings-label' }, 'Explorer follows active file'),
3576
4279
  React.createElement('input', {
3577
4280
  type: 'checkbox',
@@ -3581,7 +4284,7 @@ var MbeditorApp = function MbeditorApp() {
3581
4284
  })
3582
4285
  ),
3583
4286
  React.createElement(
3584
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4287
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Jump to a file in the explorer by typing its name when the sidebar is focused' },
3585
4288
  React.createElement('span', { className: 'ide-settings-label' }, 'Explorer type-ahead'),
3586
4289
  React.createElement('input', {
3587
4290
  type: 'checkbox',
@@ -3591,7 +4294,7 @@ var MbeditorApp = function MbeditorApp() {
3591
4294
  })
3592
4295
  ),
3593
4296
  React.createElement(
3594
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4297
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Show hidden files and directories (those starting with a dot, e.g. .env, .gitignore) in the file explorer' },
3595
4298
  React.createElement('span', { className: 'ide-settings-label' }, 'Show dotfiles'),
3596
4299
  React.createElement('input', {
3597
4300
  type: 'checkbox',
@@ -3601,7 +4304,7 @@ var MbeditorApp = function MbeditorApp() {
3601
4304
  })
3602
4305
  ),
3603
4306
  React.createElement(
3604
- 'label', { className: 'ide-settings-row ide-settings-row-half' },
4307
+ 'label', { className: 'ide-settings-row ide-settings-row-half', title: 'Scroll: tabs overflow horizontally with a scrollbar; Wrap: tabs flow onto multiple rows' },
3605
4308
  React.createElement('span', { className: 'ide-settings-label' }, 'Tab bar layout'),
3606
4309
  React.createElement(
3607
4310
  'select', {
@@ -3613,7 +4316,7 @@ var MbeditorApp = function MbeditorApp() {
3613
4316
  )
3614
4317
  ),
3615
4318
  React.createElement(
3616
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4319
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Include folder names in the Quick Open picker (Ctrl+P / Cmd+P) results, not just files' },
3617
4320
  React.createElement('span', { className: 'ide-settings-label' }, 'Quick Open: show folders'),
3618
4321
  React.createElement('input', {
3619
4322
  type: 'checkbox',
@@ -3623,7 +4326,7 @@ var MbeditorApp = function MbeditorApp() {
3623
4326
  })
3624
4327
  ),
3625
4328
  React.createElement(
3626
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4329
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Hide toolbar button labels and show only icons, giving more horizontal space' },
3627
4330
  React.createElement('span', { className: 'ide-settings-label' }, 'Toolbar: icons only'),
3628
4331
  React.createElement('input', {
3629
4332
  type: 'checkbox',
@@ -3634,7 +4337,7 @@ var MbeditorApp = function MbeditorApp() {
3634
4337
  ),
3635
4338
 
3636
4339
  React.createElement(
3637
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4340
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Keep the search/replace text when switching between files in the editor' },
3638
4341
  React.createElement('span', { className: 'ide-settings-label' }, 'Persist find state across files'),
3639
4342
  React.createElement('input', {
3640
4343
  type: 'checkbox',
@@ -3643,11 +4346,21 @@ var MbeditorApp = function MbeditorApp() {
3643
4346
  onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { persistFindState: v }); }); }
3644
4347
  })
3645
4348
  ),
4349
+ React.createElement(
4350
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Save which files are open per branch and restore them when switching branches. Disable to always start with a clean slate when switching.' },
4351
+ React.createElement('span', { className: 'ide-settings-label' }, 'Restore tabs on branch switch'),
4352
+ React.createElement('input', {
4353
+ type: 'checkbox',
4354
+ className: 'ide-settings-checkbox',
4355
+ checked: editorPrefs.branchStateRestore !== false,
4356
+ onChange: function(e) { var v = e.target.checked; setEditorPrefs(function(p) { return Object.assign({}, p, { branchStateRestore: v }); }); }
4357
+ })
4358
+ ),
3646
4359
 
3647
4360
  /* ── RuboCop ─────────────────────────────────── */
3648
4361
  React.createElement('div', { className: 'ide-settings-section-header' }, 'RuboCop'),
3649
4362
  React.createElement(
3650
- 'label', { className: 'ide-settings-row ide-settings-row-check' },
4363
+ 'label', { className: 'ide-settings-row ide-settings-row-check', title: 'Run RuboCop in the background and show lint warnings/errors as markers in the editor gutter' },
3651
4364
  React.createElement('span', { className: 'ide-settings-label' }, 'Enable RuboCop linting'),
3652
4365
  React.createElement('input', {
3653
4366
  type: 'checkbox',
@@ -3775,6 +4488,12 @@ var MbeditorApp = function MbeditorApp() {
3775
4488
  React.Fragment,
3776
4489
  null,
3777
4490
  renderTabBar(pane.id, pane.tabs, pane.activeTabId),
4491
+ React.createElement(FileReloadBanner, {
4492
+ pendingReloads: (state.pendingReloads || []).filter(function (r) { return r.paneId === pane.id; }),
4493
+ onSaveAndReload: handleSaveAndReload,
4494
+ onDiscardAndReload: handleDiscardAndReload,
4495
+ onKeepMine: handleKeepMine
4496
+ }),
3778
4497
  React.createElement(
3779
4498
  "div",
3780
4499
  { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', visibility: activeResizeMode === 'pane' ? 'hidden' : 'visible' } },
@@ -3978,8 +4697,8 @@ var MbeditorApp = function MbeditorApp() {
3978
4697
  "ZEN"
3979
4698
  ),
3980
4699
  React.createElement(
3981
- "div",
3982
- { className: "statusbar-version" },
4700
+ "button",
4701
+ { type: "button", className: "statusbar-version statusbar-btn", onClick: openChangelogTab, title: "What's New — click to open changelog" },
3983
4702
  "v" + (document.body.dataset.mbeditorVersion || "")
3984
4703
  )
3985
4704
  ),
@@ -4289,6 +5008,96 @@ var MbeditorApp = function MbeditorApp() {
4289
5008
  )
4290
5009
  )
4291
5010
  )
5011
+ ),
5012
+
5013
+ /* ── Schema modal ──────────────────────────────────────────────────── */
5014
+ schemaModal && React.createElement(
5015
+ 'div',
5016
+ {
5017
+ className: 'schema-modal-overlay',
5018
+ onClick: function() { setSchemaModal(null); }
5019
+ },
5020
+ React.createElement(
5021
+ 'div',
5022
+ {
5023
+ className: 'schema-modal',
5024
+ onClick: function(e) { e.stopPropagation(); }
5025
+ },
5026
+ /* Header */
5027
+ React.createElement(
5028
+ 'div', { className: 'schema-modal-header' },
5029
+ React.createElement(
5030
+ 'div', { className: 'schema-modal-title' },
5031
+ React.createElement('i', { className: 'fas fa-table', style: { marginRight: '8px', opacity: 0.7 } }),
5032
+ schemaModal.label,
5033
+ !schemaModal.error && schemaModal.data && React.createElement(
5034
+ 'span', { className: 'schema-modal-table-name' }, schemaModal.data.table
5035
+ )
5036
+ ),
5037
+ React.createElement(
5038
+ 'button',
5039
+ { className: 'schema-modal-close', onClick: function() { setSchemaModal(null); }, title: 'Close' },
5040
+ React.createElement('i', { className: 'fas fa-times' })
5041
+ )
5042
+ ),
5043
+ /* Body */
5044
+ React.createElement(
5045
+ 'div', { className: 'schema-modal-body' },
5046
+ schemaModal.error
5047
+ ? React.createElement('div', { className: 'schema-modal-error' },
5048
+ React.createElement('i', { className: 'fas fa-exclamation-circle', style: { marginRight: '8px' } }),
5049
+ schemaModal.error
5050
+ )
5051
+ : [
5052
+ /* Columns table */
5053
+ React.createElement(
5054
+ 'table', { key: 'cols', className: 'schema-table' },
5055
+ React.createElement(
5056
+ 'thead', null,
5057
+ React.createElement(
5058
+ 'tr', null,
5059
+ React.createElement('th', null, 'Column'),
5060
+ React.createElement('th', null, 'Type'),
5061
+ React.createElement('th', null, 'Options')
5062
+ )
5063
+ ),
5064
+ React.createElement(
5065
+ 'tbody', null,
5066
+ schemaModal.data.columns.map(function(col) {
5067
+ var opts = [];
5068
+ if (col.null === false) opts.push('NOT NULL');
5069
+ if (col.default !== undefined && col.default !== null) opts.push('default: ' + col.default);
5070
+ if (col.limit) opts.push('limit: ' + col.limit);
5071
+ if (col.precision) opts.push('precision: ' + col.precision + (col.scale ? ', scale: ' + col.scale : ''));
5072
+ if (col.primary_key) opts.push('PK');
5073
+ return React.createElement(
5074
+ 'tr', { key: col.name },
5075
+ React.createElement('td', { className: 'schema-col-name' }, col.name),
5076
+ React.createElement('td', { className: 'schema-col-type schema-type-' + col.type }, col.type),
5077
+ React.createElement('td', { className: 'schema-col-opts' }, opts.join(' · ') || '—')
5078
+ );
5079
+ })
5080
+ )
5081
+ ),
5082
+ /* Indexes */
5083
+ schemaModal.data.indexes && schemaModal.data.indexes.length > 0 && React.createElement(
5084
+ 'div', { key: 'idxs', className: 'schema-indexes' },
5085
+ React.createElement('div', { className: 'schema-indexes-header' }, 'Indexes'),
5086
+ schemaModal.data.indexes.map(function(idx, i) {
5087
+ return React.createElement(
5088
+ 'div', { key: idx.name || i, className: 'schema-index-row' },
5089
+ React.createElement('span', { className: 'schema-index-cols' },
5090
+ React.createElement('i', { className: 'fas fa-key', style: { fontSize: '9px', marginRight: '5px', opacity: 0.5 } }),
5091
+ idx.columns.join(', ')
5092
+ ),
5093
+ idx.unique && React.createElement('span', { className: 'schema-index-unique' }, 'UNIQUE'),
5094
+ idx.name && React.createElement('span', { className: 'schema-index-name' }, idx.name)
5095
+ );
5096
+ })
5097
+ )
5098
+ ]
5099
+ )
5100
+ )
4292
5101
  )
4293
5102
  );
4294
5103
  };