mbeditor 0.5.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -66,6 +66,33 @@ var DEFAULT_EDITOR_PREFS = {
66
66
  showDotFiles: false
67
67
  };
68
68
 
69
+ function spacesToTabs(code, indentSize) {
70
+ var unit = ' '.repeat(indentSize || 2);
71
+ return code.split('\n').map(function(line) {
72
+ var tabs = '';
73
+ while (line.startsWith(unit)) { tabs += '\t'; line = line.slice(unit.length); }
74
+ return tabs + line;
75
+ }).join('\n');
76
+ }
77
+
78
+ function diffLines(oldLines, newLines) {
79
+ var n = oldLines.length, m = newLines.length;
80
+ var dp = [];
81
+ for (var i = 0; i <= n; i++) { dp.push(new Array(m + 1).fill(0)); }
82
+ for (var i = 1; i <= n; i++) {
83
+ for (var j = 1; j <= m; j++) {
84
+ 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]);
85
+ }
86
+ }
87
+ var changed = [], i = n, j = m;
88
+ while (i > 0 || j > 0) {
89
+ if (i > 0 && j > 0 && oldLines[i-1] === newLines[j-1]) { i--; j--; }
90
+ else if (j > 0 && (i === 0 || dp[i][j-1] >= dp[i-1][j])) { changed.push(j); j--; }
91
+ else { i--; }
92
+ }
93
+ return changed;
94
+ }
95
+
69
96
  var SidebarActionButton = function SidebarActionButton(_ref) {
70
97
  var title = _ref.title;
71
98
  var iconClass = _ref.iconClass;
@@ -284,6 +311,16 @@ var MbeditorApp = function MbeditorApp() {
284
311
  var sidebarCollapsed = _useStateSC2[0];
285
312
  var setSidebarCollapsed = _useStateSC2[1];
286
313
 
314
+ var _useStateRFMap = useState({});
315
+ var _useStateRFMap2 = _slicedToArray(_useStateRFMap, 2);
316
+ var railsFilesMap = _useStateRFMap2[0];
317
+ var setRailsFilesMap = _useStateRFMap2[1];
318
+
319
+ var _useStateRFC = useState({});
320
+ var _useStateRFC2 = _slicedToArray(_useStateRFC, 2);
321
+ var railsGroupsCollapsed = _useStateRFC2[0];
322
+ var setRailsGroupsCollapsed = _useStateRFC2[1];
323
+
287
324
  var _useState9 = useState({});
288
325
 
289
326
  var _useState92 = _slicedToArray(_useState9, 2);
@@ -496,6 +533,8 @@ var MbeditorApp = function MbeditorApp() {
496
533
  var prevGitBranchRef = useRef(null);
497
534
  var isSwitchingBranchRef = useRef(false);
498
535
  var stateRestoredRef = useRef(false);
536
+ var ctrlWPendingRef = useRef(false);
537
+ var ctrlWTimeoutRef = useRef(null);
499
538
 
500
539
  // ── Draft backup helpers ─────────────────────────────────────────────────
501
540
  var draftWriteTimerRef = useRef({});
@@ -506,7 +545,14 @@ var MbeditorApp = function MbeditorApp() {
506
545
  return 'mbeditor_draft\x00' + base + '\x00' + path;
507
546
  };
508
547
  var _saveDraftNow = function _saveDraftNow(path, content) {
509
- try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
548
+ var doWrite = function() {
549
+ try { localStorage.setItem(_draftKey(path), JSON.stringify({ content: content, ts: Date.now() })); } catch (e) {}
550
+ };
551
+ if (typeof requestIdleCallback !== 'undefined') {
552
+ requestIdleCallback(doWrite, { timeout: 2000 });
553
+ } else {
554
+ doWrite();
555
+ }
510
556
  };
511
557
  var _clearDraft = function _clearDraft(path) {
512
558
  try { localStorage.removeItem(_draftKey(path)); } catch (e) {}
@@ -1000,7 +1046,7 @@ var MbeditorApp = function MbeditorApp() {
1000
1046
  var rect = body.getBoundingClientRect();
1001
1047
  var reservedRight = EDITOR_MIN_WIDTH + (showGitPanelRef.current ? gitPanelWidthRef.current : 0);
1002
1048
  var maxSidebarWidth = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, rect.width - reservedRight));
1003
- var nextWidth = clientX - rect.left;
1049
+ var nextWidth = clientX - rect.left - SIDEBAR_COLLAPSED_WIDTH;
1004
1050
  setSidebarWidth(clamp(nextWidth, SIDEBAR_MIN_WIDTH, maxSidebarWidth));
1005
1051
  }
1006
1052
 
@@ -1044,8 +1090,46 @@ var MbeditorApp = function MbeditorApp() {
1044
1090
  }
1045
1091
  };
1046
1092
 
1093
+ // Capture-phase listener for vim Ctrl+W window navigation.
1094
+ // Runs before Monaco (and the browser) so neither can swallow the keystrokes.
1095
+ // Phase 1 — Ctrl+W: prevent tab-close and arm the pending flag.
1096
+ // Phase 2 — next key: act on it here (still capture phase) so Monaco-vim
1097
+ // cannot consume it as a word-motion or other binding.
1098
+ var onCtrlWCapture = function(e) {
1099
+ // Phase 2: a previous Ctrl+W is pending — consume the follow-up key.
1100
+ if (ctrlWPendingRef.current) {
1101
+ ctrlWPendingRef.current = false;
1102
+ if (ctrlWTimeoutRef.current) { clearTimeout(ctrlWTimeoutRef.current); ctrlWTimeoutRef.current = null; }
1103
+ var _st = EditorStore.getState();
1104
+ var _cur = _st.focusedPaneId;
1105
+ var _target;
1106
+ if (e.key === '1') _target = 1;
1107
+ else if (e.key === '2') _target = 2;
1108
+ else if (e.key === 'h') _target = 1;
1109
+ else _target = _cur === 1 ? 2 : 1; // w, l, Ctrl+W, or anything else → cycle
1110
+ if (_target !== _cur) {
1111
+ if (typeof TabManager !== 'undefined') TabManager.focusPane(_target);
1112
+ window.dispatchEvent(new CustomEvent('mbeditor:focusPane', { detail: { paneId: _target } }));
1113
+ }
1114
+ e.preventDefault();
1115
+ e.stopPropagation();
1116
+ return;
1117
+ }
1118
+ // Phase 1: intercept Ctrl+W itself when vim mode is on.
1119
+ if (e.metaKey || e.shiftKey || e.altKey) return;
1120
+ if (!e.ctrlKey || (e.key !== 'w' && e.key !== 'W')) return;
1121
+ var prefs = EditorStore.getState().editorPrefs;
1122
+ if (!prefs || !prefs.vimMode) return;
1123
+ e.preventDefault();
1124
+ e.stopPropagation();
1125
+ if (ctrlWTimeoutRef.current) clearTimeout(ctrlWTimeoutRef.current);
1126
+ ctrlWPendingRef.current = true;
1127
+ ctrlWTimeoutRef.current = setTimeout(function() { ctrlWPendingRef.current = false; }, 1500);
1128
+ };
1129
+
1047
1130
  window.addEventListener('keydown', onKeyDown);
1048
1131
  document.addEventListener('keydown', onZenCapture, true);
1132
+ document.addEventListener('keydown', onCtrlWCapture, true);
1049
1133
  window.addEventListener('mousemove', handleMouseMove);
1050
1134
  window.addEventListener('mouseup', handleMouseUp);
1051
1135
  return function () {
@@ -1056,8 +1140,10 @@ var MbeditorApp = function MbeditorApp() {
1056
1140
  cancelAnimationFrame(resizeRafRef.current);
1057
1141
  resizeRafRef.current = null;
1058
1142
  }
1143
+ if (ctrlWTimeoutRef.current) { clearTimeout(ctrlWTimeoutRef.current); ctrlWTimeoutRef.current = null; }
1059
1144
  window.removeEventListener('keydown', onKeyDown);
1060
1145
  document.removeEventListener('keydown', onZenCapture, true);
1146
+ document.removeEventListener('keydown', onCtrlWCapture, true);
1061
1147
  window.removeEventListener('mousemove', handleMouseMove);
1062
1148
  window.removeEventListener('mouseup', handleMouseUp);
1063
1149
  document.body.style.cursor = '';
@@ -1133,8 +1219,8 @@ var MbeditorApp = function MbeditorApp() {
1133
1219
  FileService.getTree().then(function (data) {
1134
1220
  var newData = data || [];
1135
1221
  setTreeData(function (prevData) {
1136
- if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
1137
- SearchService.buildIndex(newData);
1222
+ var sig = function(d) { return d.length + ':' + d.map(function(n) { return n.name; }).join(','); };
1223
+ if (sig(newData) !== sig(prevData)) SearchService.buildIndex(newData);
1138
1224
  return newData;
1139
1225
  });
1140
1226
  checkOpenTabsForExternalChanges();
@@ -1221,8 +1307,8 @@ var MbeditorApp = function MbeditorApp() {
1221
1307
  FileService.getTree().then(function (data) {
1222
1308
  var newData = data || [];
1223
1309
  setTreeData(function (prevData) {
1224
- if (JSON.stringify(newData) === JSON.stringify(prevData)) return prevData;
1225
- SearchService.buildIndex(newData);
1310
+ var sig = function(d) { return d.length + ':' + d.map(function(n) { return n.name; }).join(','); };
1311
+ if (sig(newData) !== sig(prevData)) SearchService.buildIndex(newData);
1226
1312
  return newData;
1227
1313
  });
1228
1314
  }).catch(function () {}); // silently ignore auto-refresh errors
@@ -1435,6 +1521,108 @@ var MbeditorApp = function MbeditorApp() {
1435
1521
  return function() { window.removeEventListener('beforeinstallprompt', handler); };
1436
1522
  }, []);
1437
1523
 
1524
+ var resourceLabelFromPath = function(p) {
1525
+ if (!p) return null;
1526
+ var parts = p.split('/');
1527
+ var file = parts[parts.length - 1];
1528
+ var name;
1529
+ if (parts[0] === 'app') {
1530
+ if (parts[1] === 'controllers') name = file.replace(/_controller\.rb$/, '');
1531
+ else if (parts[1] === 'models') name = file.replace(/\.rb$/, '');
1532
+ else if (parts[1] === 'views' && parts.length >= 4) name = parts[2];
1533
+ else if (parts[1] === 'helpers') name = file.replace(/_helper\.rb$/, '');
1534
+ else return null;
1535
+ } else if (parts[0] === 'test' || parts[0] === 'spec') {
1536
+ if (parts[1] === 'controllers') name = file.replace(/_controller_(test|spec)\.rb$/, '');
1537
+ else if (parts[1] === 'models') name = file.replace(/_(test|spec)\.rb$/, '');
1538
+ else return null;
1539
+ } else { return null; }
1540
+ var seg = (name || '').split('/').pop() || name || '';
1541
+ // Normalize plural→singular so views/users and models/user share one group
1542
+ seg = seg.replace(/ies$/, 'y')
1543
+ .replace(/([^aeiou])es$/, '$1')
1544
+ .replace(/([^s])s$/, '$1');
1545
+ return seg.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
1546
+ };
1547
+
1548
+ var RAILS_MAX_RESOURCES = 10;
1549
+
1550
+ // Map resource label → representative path (capped at RAILS_MAX_RESOURCES, focused pane first)
1551
+ var railsResourceDeps = (function() {
1552
+ var deps = {};
1553
+ var panesOrdered = state.panes.slice().sort(function(a, b) {
1554
+ return a.id === state.focusedPaneId ? -1 : b.id === state.focusedPaneId ? 1 : 0;
1555
+ });
1556
+ panesOrdered.forEach(function(p) {
1557
+ var tabs = p.tabs.slice().sort(function(a, b) {
1558
+ return a.id === p.activeTabId ? -1 : b.id === p.activeTabId ? 1 : 0;
1559
+ });
1560
+ tabs.forEach(function(t) {
1561
+ if (Object.keys(deps).length >= RAILS_MAX_RESOURCES) return;
1562
+ if (!t.path || t.path === '__settings__' || t.path.startsWith('mbeditor://')) return;
1563
+ var label = resourceLabelFromPath(t.path);
1564
+ if (label && !deps[label]) deps[label] = t.path;
1565
+ });
1566
+ });
1567
+ return deps;
1568
+ })();
1569
+ var railsResourceDepStr = Object.keys(railsResourceDeps).sort().join('|');
1570
+
1571
+ var railsOverflow = (function() {
1572
+ var all = {};
1573
+ state.panes.forEach(function(p) {
1574
+ p.tabs.forEach(function(t) {
1575
+ if (!t.path || t.path === '__settings__' || t.path.startsWith('mbeditor://')) return;
1576
+ var label = resourceLabelFromPath(t.path);
1577
+ if (label) all[label] = true;
1578
+ });
1579
+ });
1580
+ return Math.max(0, Object.keys(all).length - Object.keys(railsResourceDeps).length);
1581
+ })();
1582
+
1583
+ var dirtyPaths = (function() {
1584
+ var set = {};
1585
+ state.panes.forEach(function(p) {
1586
+ p.tabs.forEach(function(t) {
1587
+ if (t.dirty && t.path) set[t.path] = true;
1588
+ });
1589
+ });
1590
+ return set;
1591
+ })();
1592
+
1593
+ useEffect(function() {
1594
+ if (activeSidebarTab !== 'rails') return;
1595
+ var labels = Object.keys(railsResourceDeps);
1596
+ if (labels.length === 0) { setRailsFilesMap({}); return; }
1597
+ setRailsFilesMap(function(prev) {
1598
+ var next = {};
1599
+ labels.forEach(function(label) {
1600
+ next[label] = prev[label] ? { files: prev[label].files, loading: true } : { files: null, loading: true };
1601
+ });
1602
+ return next;
1603
+ });
1604
+ labels.forEach(function(label) {
1605
+ var path = railsResourceDeps[label];
1606
+ FileService.getRelatedFiles(path).then(function(data) {
1607
+ setRailsFilesMap(function(prev) {
1608
+ if (!prev.hasOwnProperty(label)) return prev;
1609
+ var next = Object.assign({}, prev);
1610
+ var update = {};
1611
+ update[label] = { files: data, loading: false };
1612
+ return Object.assign(next, update);
1613
+ });
1614
+ })['catch'](function() {
1615
+ setRailsFilesMap(function(prev) {
1616
+ if (!prev.hasOwnProperty(label)) return prev;
1617
+ var next = Object.assign({}, prev);
1618
+ var update = {};
1619
+ update[label] = { files: null, loading: false };
1620
+ return Object.assign(next, update);
1621
+ });
1622
+ });
1623
+ });
1624
+ }, [activeSidebarTab, railsResourceDepStr]);
1625
+
1438
1626
  var focusedPane = state.panes.find(function (p) {
1439
1627
  return p.id === state.focusedPaneId;
1440
1628
  }) || state.panes[0] || null;
@@ -1778,7 +1966,10 @@ var MbeditorApp = function MbeditorApp() {
1778
1966
  return _extends({}, prev, { format: true });
1779
1967
  });
1780
1968
  EditorStore.setStatus("Formatting...", "info");
1781
- FileService.formatFile(activeTab.path, activeTab.content).then(function (res) {
1969
+ var useTabs = editorPrefs.insertSpaces === false;
1970
+ var originalContent = activeTab.content;
1971
+ var codeToFormat = useTabs ? spacesToTabs(originalContent, 2) : originalContent;
1972
+ FileService.formatFile(activeTab.path, codeToFormat).then(function (res) {
1782
1973
  if (res.content) {
1783
1974
  // Update content and mark dirty — user decides when to save.
1784
1975
  // The executeEdits path in EditorPanel preserves the undo stack.
@@ -1789,6 +1980,19 @@ var MbeditorApp = function MbeditorApp() {
1789
1980
  return p;
1790
1981
  });
1791
1982
  EditorStore.setState({ panes: newPanes });
1983
+
1984
+ // Highlight changed lines briefly
1985
+ var monacoEditor = window.__mbeditorActiveEditor;
1986
+ if (monacoEditor && res.content !== originalContent) {
1987
+ var changedLineNums = diffLines(originalContent.split('\n'), res.content.split('\n'));
1988
+ if (changedLineNums.length > 0) {
1989
+ var decorations = changedLineNums.map(function(ln) {
1990
+ return { range: new monaco.Range(ln, 1, ln, 1), options: { isWholeLine: true, className: 'mbeditor-format-changed' } };
1991
+ });
1992
+ var ids = monacoEditor.deltaDecorations([], decorations);
1993
+ setTimeout(function() { monacoEditor.deltaDecorations(ids, []); }, 3000);
1994
+ }
1995
+ }
1792
1996
  }
1793
1997
  EditorStore.setStatus("Formatted (Unsaved)", "success");
1794
1998
  GitService.fetchStatus();
@@ -2266,13 +2470,17 @@ var MbeditorApp = function MbeditorApp() {
2266
2470
  document.body.style.userSelect = 'none';
2267
2471
  };
2268
2472
 
2269
- var toggleSidebarCollapsed = function toggleSidebarCollapsed() {
2270
- setSidebarCollapsed(function (prev) { return !prev; });
2271
- };
2272
-
2273
- var expandSidebarTo = function expandSidebarTo(tab) {
2274
- setActiveSidebarTab(tab);
2275
- setSidebarCollapsed(false);
2473
+ var handleActivityBarClick = function handleActivityBarClick(tab) {
2474
+ if (tab === 'settings') {
2475
+ openSettingsTab();
2476
+ return;
2477
+ }
2478
+ if (!sidebarCollapsed && activeSidebarTab === tab) {
2479
+ setSidebarCollapsed(true);
2480
+ } else {
2481
+ setActiveSidebarTab(tab);
2482
+ setSidebarCollapsed(false);
2483
+ }
2276
2484
  };
2277
2485
 
2278
2486
  var openFileFromGitPanel = function openFileFromGitPanel(path, name) {
@@ -2794,74 +3002,69 @@ var MbeditorApp = function MbeditorApp() {
2794
3002
  React.createElement(
2795
3003
  "div",
2796
3004
  { className: "ide-body", id: "ide-body-container" },
2797
- React.createElement(
3005
+ /* Activity bar — always visible, 48px wide */
3006
+ !zenMode && React.createElement(
2798
3007
  "div",
2799
- { className: "ide-sidebar" + (sidebarCollapsed ? " ide-sidebar-collapsed" : ""), style: { width: (sidebarCollapsed ? SIDEBAR_COLLAPSED_WIDTH : sidebarWidth) + "px", display: zenMode ? 'none' : undefined } },
2800
- sidebarCollapsed
2801
- ? React.createElement(
2802
- "div",
2803
- { className: "sidebar-icon-strip" },
2804
- React.createElement(
2805
- "div",
2806
- { className: "sidebar-strip-top" },
2807
- React.createElement(
2808
- "div",
2809
- { className: "sidebar-nav-group", "data-group": "nav" },
2810
- React.createElement(
2811
- "button",
2812
- { type: "button", className: "sidebar-strip-btn " + (activeSidebarTab === 'explorer' ? 'active' : ''), title: "Explorer", onClick: function () { return expandSidebarTo('explorer'); } },
2813
- React.createElement("i", { className: "far fa-folder" })
2814
- ),
2815
- React.createElement(
2816
- "button",
2817
- { type: "button", className: "sidebar-strip-btn " + (activeSidebarTab === 'search' ? 'active' : ''), title: "Search", onClick: function () { return expandSidebarTo('search'); } },
2818
- React.createElement("i", { className: "fas fa-search" })
2819
- ),
2820
- React.createElement(
2821
- "button",
2822
- { type: "button", className: "sidebar-strip-btn", title: "Editor Preferences", onClick: openSettingsTab },
2823
- React.createElement("i", { className: "fas fa-cog" })
2824
- )
2825
- )
2826
- ),
2827
- React.createElement(
2828
- "div",
2829
- { className: "sidebar-strip-bottom" },
2830
- React.createElement(
2831
- "button",
2832
- { type: "button", className: "sidebar-strip-btn", title: "Expand sidebar", onClick: toggleSidebarCollapsed },
2833
- React.createElement("i", { className: "fas fa-chevron-right" })
2834
- )
2835
- )
2836
- )
2837
- : React.createElement(
2838
- React.Fragment,
2839
- null,
2840
- React.createElement(
2841
- "div",
2842
- { className: "ide-sidebar-tabs" },
2843
- React.createElement(
2844
- "button",
2845
- { type: "button", className: "ide-sidebar-tab " + (activeSidebarTab === 'explorer' ? 'active' : ''), onClick: function () { return setActiveSidebarTab('explorer'); } },
2846
- "EXPLORER"
2847
- ),
2848
- React.createElement(
2849
- "button",
2850
- { type: "button", className: "ide-sidebar-tab " + (activeSidebarTab === 'search' ? 'active' : ''), onClick: function () { return setActiveSidebarTab('search'); } },
2851
- "SEARCH"
2852
- ),
2853
- React.createElement(
2854
- "button",
2855
- { type: "button", className: "ide-sidebar-tab ide-sidebar-tab-icon", title: "Editor Preferences", onClick: openSettingsTab },
2856
- React.createElement("i", { className: "fas fa-cog" })
2857
- ),
2858
- React.createElement(
2859
- "button",
2860
- { type: "button", className: "sidebar-strip-btn", title: "Collapse sidebar", onClick: toggleSidebarCollapsed },
2861
- React.createElement("i", { className: "fas fa-chevron-left" })
2862
- )
2863
- ),
2864
- activeSidebarTab === 'explorer' && React.createElement(
3008
+ { className: "ide-activity-bar" },
3009
+ React.createElement(
3010
+ "div",
3011
+ { className: "ide-activity-bar-top" },
3012
+ React.createElement(
3013
+ "button",
3014
+ {
3015
+ type: "button",
3016
+ className: "ide-activity-btn" + (!sidebarCollapsed && activeSidebarTab === 'explorer' ? ' active' : ''),
3017
+ title: "Explorer",
3018
+ onClick: function() { handleActivityBarClick('explorer'); }
3019
+ },
3020
+ React.createElement("i", { className: "far fa-folder" })
3021
+ ),
3022
+ React.createElement(
3023
+ "button",
3024
+ {
3025
+ type: "button",
3026
+ className: "ide-activity-btn" + (!sidebarCollapsed && activeSidebarTab === 'search' ? ' active' : ''),
3027
+ title: "Search",
3028
+ onClick: function() { handleActivityBarClick('search'); }
3029
+ },
3030
+ React.createElement("i", { className: "fas fa-search" })
3031
+ ),
3032
+ React.createElement(
3033
+ "button",
3034
+ {
3035
+ type: "button",
3036
+ className: "ide-activity-btn" + (!sidebarCollapsed && activeSidebarTab === 'rails' ? ' active' : ''),
3037
+ title: "Rails",
3038
+ onClick: function() { handleActivityBarClick('rails'); }
3039
+ },
3040
+ React.createElement("i", { className: "far fa-gem" })
3041
+ )
3042
+ ),
3043
+ React.createElement(
3044
+ "div",
3045
+ { className: "ide-activity-bar-bottom" },
3046
+ React.createElement(
3047
+ "button",
3048
+ {
3049
+ type: "button",
3050
+ className: "ide-activity-btn" + (activeTab && activeTab.isSettings ? ' active' : ''),
3051
+ title: "Editor Preferences",
3052
+ onClick: openSettingsTab
3053
+ },
3054
+ React.createElement("i", { className: "fas fa-cog" })
3055
+ )
3056
+ )
3057
+ ),
3058
+ /* Panel content — shown when not collapsed and not in zen mode */
3059
+ !sidebarCollapsed && !zenMode && React.createElement(
3060
+ "div",
3061
+ { className: "ide-sidebar", style: { width: sidebarWidth + "px" } },
3062
+ React.createElement("div", { className: "sidebar-panel-title" },
3063
+ activeSidebarTab === 'explorer' ? 'Explorer' :
3064
+ activeSidebarTab === 'search' ? 'Search' :
3065
+ activeSidebarTab === 'rails' ? 'Rails' : ''
3066
+ ),
3067
+ activeSidebarTab === 'explorer' && React.createElement(
2865
3068
  "div",
2866
3069
  { className: "ide-sidebar-content" },
2867
3070
  React.createElement(
@@ -3226,10 +3429,84 @@ var MbeditorApp = function MbeditorApp() {
3226
3429
  )
3227
3430
  );
3228
3431
  })()
3432
+ ),
3433
+ activeSidebarTab === 'rails' && React.createElement(
3434
+ "div",
3435
+ { className: "rails-panel" },
3436
+ (function() {
3437
+ var labels = Object.keys(railsFilesMap).sort();
3438
+ if (labels.length === 0) {
3439
+ return React.createElement("div", { className: "rails-panel-empty" }, "Open a Rails file to see related files.");
3440
+ }
3441
+ var sections = labels.map(function(label) {
3442
+ var entry = railsFilesMap[label];
3443
+ var files = entry && entry.files;
3444
+ var loading = entry && entry.loading;
3445
+ if (loading && !files) {
3446
+ return React.createElement("div", { key: label + '_loading', className: "rails-panel-loading" },
3447
+ React.createElement("i", { className: "fas fa-spinner fa-spin" }),
3448
+ " Loading…"
3449
+ );
3450
+ }
3451
+ if (!files || Object.keys(files).length === 0) return null;
3452
+ var allFiles = [];
3453
+ ['model', 'controller', 'helper', 'tests', 'views'].forEach(function(key) {
3454
+ var group = files[key];
3455
+ if (group && group.length) allFiles = allFiles.concat(group);
3456
+ });
3457
+ var customGroups = files['custom'];
3458
+ if (customGroups && typeof customGroups === 'object') {
3459
+ Object.keys(customGroups).forEach(function(base) {
3460
+ var grpFiles = customGroups[base];
3461
+ if (grpFiles && grpFiles.length) allFiles = allFiles.concat(grpFiles);
3462
+ });
3463
+ }
3464
+ if (allFiles.length === 0) return null;
3465
+ return React.createElement(
3466
+ CollapsibleSection,
3467
+ {
3468
+ key: label,
3469
+ title: label.toUpperCase(),
3470
+ isCollapsed: !!railsGroupsCollapsed[label],
3471
+ onToggle: (function(captured) { return function(isCollapsed) {
3472
+ setRailsGroupsCollapsed(function(prev) {
3473
+ var next = Object.assign({}, prev);
3474
+ next[captured] = isCollapsed;
3475
+ return next;
3476
+ });
3477
+ }; })(label)
3478
+ },
3479
+ React.createElement(
3480
+ "div",
3481
+ null,
3482
+ allFiles.map(function(f) {
3483
+ return React.createElement(
3484
+ "div", {
3485
+ key: f.path,
3486
+ className: "rails-group-item",
3487
+ onClick: (function(file) { return function() { handleSelectFile(file.path, file.name); }; })(f),
3488
+ title: f.path
3489
+ },
3490
+ React.createElement("i", { className: "tree-item-icon " + (window.getFileIcon ? window.getFileIcon(f.name) : 'far fa-file-code') + " tree-file-icon" }),
3491
+ React.createElement("span", { className: "rails-group-item-name" }, f.name),
3492
+ dirtyPaths[f.path] && React.createElement("span", { className: "rails-group-item-dirty" }, "●")
3493
+ );
3494
+ })
3495
+ )
3496
+ );
3497
+ });
3498
+ if (railsOverflow > 0) {
3499
+ sections = sections.concat([React.createElement(
3500
+ "div", { key: '__overflow', className: "rails-panel-overflow" },
3501
+ "+" + railsOverflow + " more — close tabs to show all"
3502
+ )]);
3503
+ }
3504
+ return sections;
3505
+ })()
3229
3506
  )
3230
- )
3231
- ),
3232
- React.createElement("div", {
3507
+ ),
3508
+ /* Sidebar resize divider — only when panel is open */
3509
+ !sidebarCollapsed && !zenMode && React.createElement("div", {
3233
3510
  className: "panel-divider sidebar-divider " + (activeResizeMode === 'sidebar' ? 'active' : ''),
3234
3511
  onMouseDown: startSidebarResize,
3235
3512
  role: "separator",
@@ -84,6 +84,23 @@
84
84
  return lines.join('\n');
85
85
  }
86
86
 
87
+ // Return true if sym is a user-assigned window property (not a browser built-in).
88
+ // Uses the same property-descriptor filter as buildWindowGlobalsShim: browser built-ins
89
+ // are either non-configurable or accessor properties (have a getter), so plain writable
90
+ // configurable data properties reliably identify user-assigned globals.
91
+ function isRuntimeWindowGlobal(sym) {
92
+ if (!sym || typeof window === 'undefined') return false;
93
+ try {
94
+ if (!Object.prototype.hasOwnProperty.call(window, sym)) return false;
95
+ var val;
96
+ try { val = window[sym]; } catch (e) { return false; }
97
+ if (val === null || val === undefined) return false;
98
+ var desc = Object.getOwnPropertyDescriptor(window, sym);
99
+ if (!desc) return false;
100
+ return desc.configurable === true && desc.writable === true && !desc.get;
101
+ } catch (e) { return false; }
102
+ }
103
+
87
104
  // Declare a discovered global in Monaco's extra libs so the TS2304 warning disappears.
88
105
  // Calling addExtraLib with the same URI replaces the previous content in-place.
89
106
  function addDiscoveredGlobal(name) {
@@ -104,7 +121,10 @@
104
121
  return FileService.getJsDefinition(word)
105
122
  .then(function(data) {
106
123
  var results = data && data.results;
107
- if (!results || !results.length) return false;
124
+ if (!results || !results.length) {
125
+ if (isRuntimeWindowGlobal(word)) addDiscoveredGlobal(word);
126
+ return false;
127
+ }
108
128
  var r = results[0];
109
129
  // Only declare as a global when the definition lives in a different file.
110
130
  // Locally-defined functions/classes must not get a duplicate declare var.
@@ -658,6 +678,8 @@
658
678
  var results = data && data.results;
659
679
  if (results && results.length && results[0].file !== modelPath) {
660
680
  addDiscoveredGlobal(sym);
681
+ } else if (!results || !results.length) {
682
+ if (isRuntimeWindowGlobal(sym)) addDiscoveredGlobal(sym);
661
683
  }
662
684
  })
663
685
  .catch(function() {});
@@ -1187,15 +1209,14 @@
1187
1209
  });
1188
1210
 
1189
1211
  // JS/JSX hover provider: looks up workspace definitions for window globals.
1190
- // Only fires for mixed-case identifiers (skips lowercase-only names that are
1191
- // almost always browser builtins or local vars).
1212
+ // Fires for mixed-case identifiers and for any symbol already in discoveredJsGlobals.
1192
1213
  var JS_HOVER_CACHE_TTL_MS = 60000;
1193
1214
  monaco.languages.registerHoverProvider('javascript', {
1194
1215
  provideHover: function(model, position, token) {
1195
1216
  var wordInfo = model.getWordAtPosition(position);
1196
1217
  if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return null;
1197
1218
  var word = wordInfo.word;
1198
- if (!/[A-Z]/.test(word)) return null;
1219
+ if (!discoveredJsGlobals[word] && !/[A-Z]/.test(word)) return null;
1199
1220
  if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return null;
1200
1221
 
1201
1222
  var cached = jsHoverCache[word];
@@ -1212,6 +1233,13 @@
1212
1233
  if (token && token.isCancellationRequested) return null;
1213
1234
  var results = data && data.results;
1214
1235
  if (!results || !results.length) {
1236
+ if (isRuntimeWindowGlobal(word)) {
1237
+ addDiscoveredGlobal(word);
1238
+ var kind = typeof window[word];
1239
+ var rtValue = { contents: [{ value: '**' + word + '** — runtime global (`' + kind + '`)' }] };
1240
+ jsHoverCache[word] = { ts: Date.now(), value: rtValue };
1241
+ return rtValue;
1242
+ }
1215
1243
  jsHoverCache[word] = { ts: Date.now(), value: null };
1216
1244
  return null;
1217
1245
  }
@@ -177,7 +177,6 @@ var FileService = (function () {
177
177
  prefetchCache.delete(path);
178
178
  return null;
179
179
  }
180
- prefetchCache.delete(path);
181
180
  return entry.promise;
182
181
  }
183
182
 
@@ -214,6 +213,11 @@ var FileService = (function () {
214
213
  return axios.get(window.mbeditorBasePath() + '/unused_methods', config).then(function(res) { return res.data; });
215
214
  }
216
215
 
216
+ function getRelatedFiles(path) {
217
+ return axios.get(window.mbeditorBasePath() + '/related_files', { params: { path: path } })
218
+ .then(function(res) { return res.data; });
219
+ }
220
+
217
221
  return {
218
222
  getWorkspace: getWorkspace,
219
223
  getTree: getTree,
@@ -242,6 +246,7 @@ var FileService = (function () {
242
246
  cancelPrefetch: cancelPrefetch,
243
247
  getModuleMembers: getModuleMembers,
244
248
  getFileIncludes: getFileIncludes,
245
- getUnusedMethods: getUnusedMethods
249
+ getUnusedMethods: getUnusedMethods,
250
+ getRelatedFiles: getRelatedFiles
246
251
  };
247
252
  })();