mbeditor 0.9.0 → 0.10.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.
@@ -440,6 +440,17 @@ var MbeditorApp = function MbeditorApp() {
440
440
  var showLogPanel = _useStateLog2[0];
441
441
  var setShowLogPanel = _useStateLog2[1];
442
442
 
443
+ var _useStateProblems = useState(false);
444
+ var _useStateProblems2 = _slicedToArray(_useStateProblems, 2);
445
+ var showProblemsPanel = _useStateProblems2[0];
446
+ var setShowProblemsPanel = _useStateProblems2[1];
447
+
448
+ // Error/warning tallies across the open tabs, mirrored into the status bar.
449
+ var _useStateProblemCounts = useState({ errors: 0, warnings: 0 });
450
+ var _useStateProblemCounts2 = _slicedToArray(_useStateProblemCounts, 2);
451
+ var problemCounts = _useStateProblemCounts2[0];
452
+ var setProblemCounts = _useStateProblemCounts2[1];
453
+
443
454
  var _useState18g = useState(320);
444
455
  var _useState18g2 = _slicedToArray(_useState18g, 2);
445
456
  var gitPanelWidth = _useState18g2[0];
@@ -1487,14 +1498,18 @@ var MbeditorApp = function MbeditorApp() {
1487
1498
  });
1488
1499
  }
1489
1500
 
1490
- // Auto-refresh the file tree every 10s to pick up external changes (new files, deletions, etc.)
1491
- // When an ActionCable WebSocket is connected this acts only as a safety-net fallback —
1492
- // the WebSocket push above handles immediate invalidation after mbeditor mutations.
1501
+ // Auto-refresh the file tree every 10s to pick up external changes (new files,
1502
+ // deletions, a branch switch in a terminal).
1503
+ //
1504
+ // This runs whether or not the WebSocket is connected. It used to skip when
1505
+ // connected, on the reasoning that the push covered it — but the server only
1506
+ // broadcasts from mbeditor's own mutation endpoints, so a connected socket
1507
+ // meant external changes were never picked up at all. The push remains the
1508
+ // instant path for our own writes; this is what catches everything else.
1493
1509
  // Uses functional setTreeData to skip the re-render when nothing has changed.
1494
1510
  useEffect(function () {
1495
1511
  var intervalId = setInterval(function () {
1496
1512
  if (document.hidden) return;
1497
- if (WebSocketService.isConnected()) return; // WebSocket is handling refreshes
1498
1513
  FileService.getTree().then(function (data) {
1499
1514
  var newData = data || [];
1500
1515
  setTreeData(function (prevData) {
@@ -1536,6 +1551,31 @@ var MbeditorApp = function MbeditorApp() {
1536
1551
  };
1537
1552
  }, []);
1538
1553
 
1554
+ // Keep the status-bar error/warning tallies in step with Monaco's markers.
1555
+ // Every diagnostic source in the editor — rubocop, ruby-lsp, the TypeScript
1556
+ // worker — lands here, so one subscription covers all of them. Markers change
1557
+ // on every keystroke for JS, so the recount is debounced.
1558
+ useEffect(function () {
1559
+ if (!monacoReady || !window.monaco || !window.monaco.editor || !window.ProblemsPanel) return;
1560
+
1561
+ var timer = null;
1562
+ var recount = function () {
1563
+ if (timer) clearTimeout(timer);
1564
+ timer = setTimeout(function () {
1565
+ timer = null;
1566
+ setProblemCounts(window.ProblemsPanel.counts());
1567
+ }, 250);
1568
+ };
1569
+
1570
+ var sub = window.monaco.editor.onDidChangeMarkers(recount);
1571
+ recount();
1572
+
1573
+ return function () {
1574
+ if (timer) clearTimeout(timer);
1575
+ sub.dispose();
1576
+ };
1577
+ }, [monacoReady]);
1578
+
1539
1579
  var handleSelectFile = function handleSelectFile(path, name, line, col) {
1540
1580
  TabManager.openTab(path, name, line, null, false, col);
1541
1581
  handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
@@ -2747,6 +2787,10 @@ var MbeditorApp = function MbeditorApp() {
2747
2787
  setShowLogPanel(function (prev) { return !prev; });
2748
2788
  };
2749
2789
 
2790
+ var toggleProblemsPanel = function toggleProblemsPanel() {
2791
+ setShowProblemsPanel(function (prev) { return !prev; });
2792
+ };
2793
+
2750
2794
  var toggleZenMode = function toggleZenMode() {
2751
2795
  setZenMode(function (prev) {
2752
2796
  var next = !prev;
@@ -3382,16 +3426,24 @@ var MbeditorApp = function MbeditorApp() {
3382
3426
  "Mini Browser Editor — ",
3383
3427
  window.location.host
3384
3428
  ),
3429
+ // The slot claims all the room between the title and the buttons; the
3430
+ // search pill then takes 75% of it, centred. Sizing the pill against a
3431
+ // slot rather than against the title bar keeps it proportional to the
3432
+ // actual gap as the toolbar's button labels grow and shrink.
3385
3433
  React.createElement(
3386
- 'button',
3387
- {
3388
- type: 'button',
3389
- className: 'ide-titlebar-search',
3390
- title: 'Search files (Ctrl/Cmd+P)',
3391
- onClick: function () { setQuickOpen(true); }
3392
- },
3393
- React.createElement('i', { className: 'fas fa-search', style: { marginRight: '6px', opacity: 0.7 } }),
3394
- React.createElement('span', { className: 'ide-titlebar-search-text' }, 'Search files…')
3434
+ 'div',
3435
+ { className: 'ide-titlebar-search-slot' },
3436
+ React.createElement(
3437
+ 'button',
3438
+ {
3439
+ type: 'button',
3440
+ className: 'ide-titlebar-search',
3441
+ title: 'Search files (Ctrl/Cmd+P)',
3442
+ onClick: function () { setQuickOpen(true); }
3443
+ },
3444
+ React.createElement('i', { className: 'fas fa-search', style: { marginRight: '6px', opacity: 0.7 } }),
3445
+ React.createElement('span', { className: 'ide-titlebar-search-text' }, 'Search files…')
3446
+ )
3395
3447
  ),
3396
3448
  React.createElement(
3397
3449
  "div",
@@ -3455,13 +3507,6 @@ var MbeditorApp = function MbeditorApp() {
3455
3507
  )
3456
3508
  ),
3457
3509
  React.createElement("div", { className: "statusbar-sep" }),
3458
- React.createElement(
3459
- "button",
3460
- { type: "button", className: "statusbar-btn", onClick: toggleLogPanel, title: "Toggle Rails log (Ctrl+Shift+L)" },
3461
- React.createElement("i", { className: "fas fa-stream" }),
3462
- !editorPrefs.toolbarIconOnly && " Logs"
3463
- ),
3464
- React.createElement("div", { className: "statusbar-sep" }),
3465
3510
  React.createElement(
3466
3511
  "button",
3467
3512
  { type: "button", className: "statusbar-btn", onClick: function () { return setShowHelp(true); }, title: "Keyboard shortcuts & help" },
@@ -4974,6 +5019,12 @@ var MbeditorApp = function MbeditorApp() {
4974
5019
  ),
4975
5020
  showLogPanel && !zenMode && React.createElement(window.LogPanel || LogPanel, {
4976
5021
  onClose: function () { setShowLogPanel(false); }
5022
+ }),
5023
+ showProblemsPanel && !zenMode && React.createElement(window.ProblemsPanel || ProblemsPanel, {
5024
+ onClose: function () { setShowProblemsPanel(false); },
5025
+ onOpenFile: function (path, line, col) {
5026
+ handleSelectFile(path, path.split('/').pop(), line, col);
5027
+ }
4977
5028
  })
4978
5029
  ),
4979
5030
  React.createElement(
@@ -4998,6 +5049,23 @@ var MbeditorApp = function MbeditorApp() {
4998
5049
  state.gitInfo.behind
4999
5050
  )
5000
5051
  ),
5052
+ React.createElement(
5053
+ "button",
5054
+ {
5055
+ type: "button",
5056
+ className: "statusbar-btn statusbar-problems" + (showProblemsPanel ? " active" : ""),
5057
+ onClick: toggleProblemsPanel,
5058
+ title: problemCounts.errors + " error(s), " + problemCounts.warnings +
5059
+ " warning(s) in open files — click to open Problems"
5060
+ },
5061
+ React.createElement("i", { className: "fas fa-bug statusbar-problems-error-icon" }),
5062
+ React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.errors),
5063
+ React.createElement("i", {
5064
+ className: "fas fa-exclamation-triangle statusbar-problems-warning-icon",
5065
+ style: { marginLeft: "8px" }
5066
+ }),
5067
+ React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.warnings)
5068
+ ),
5001
5069
  !serverOnline && (function () {
5002
5070
  var dirtyCount = state.panes.reduce(function (acc, p) {
5003
5071
  return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
@@ -5027,6 +5095,17 @@ var MbeditorApp = function MbeditorApp() {
5027
5095
  { className: "statusbar-msg " + state.statusMessage.kind },
5028
5096
  state.statusMessage.text
5029
5097
  ),
5098
+ React.createElement(
5099
+ "button",
5100
+ {
5101
+ type: "button",
5102
+ className: "statusbar-btn statusbar-logs-btn" + (showLogPanel ? " active" : ""),
5103
+ onClick: toggleLogPanel,
5104
+ title: "Toggle Rails log (Ctrl+Shift+L)"
5105
+ },
5106
+ React.createElement("i", { className: "fas fa-stream" }),
5107
+ " Logs"
5108
+ ),
5030
5109
  activeEOL && React.createElement(
5031
5110
  "button",
5032
5111
  {
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ // ProblemsPanel — bottom drawer listing the diagnostics Monaco is holding for
4
+ // the files you have open: rubocop and ruby-lsp for Ruby, the TypeScript
5
+ // worker's syntax errors for JS/JSX, and anything else that writes markers.
6
+ //
7
+ // Scope is deliberately "open tabs", not the workspace: markers only exist for
8
+ // models Monaco has loaded, so a count over anything wider would be a lie.
9
+ // Closing a tab drops its problems, same as VS Code with an unopened file.
10
+ var ProblemsPanel = (function () {
11
+ var SEVERITY_ERROR = 8;
12
+ var SEVERITY_WARNING = 4;
13
+ // Long enough to recognise the statement, short enough not to push the
14
+ // location off the end of the row.
15
+ var CODE_PREVIEW_LIMIT = 120;
16
+
17
+ // The offending source line, trimmed, for display beside the message. Markers
18
+ // can outlive the edit that invalidated them by a frame, so a line number
19
+ // past the end of the buffer yields nothing rather than throwing.
20
+ function codePreview(model, lineNumber) {
21
+ if (!lineNumber || lineNumber < 1 || lineNumber > model.getLineCount()) return '';
22
+
23
+ var text = model.getLineContent(lineNumber).trim();
24
+ return text.length > CODE_PREVIEW_LIMIT ? text.slice(0, CODE_PREVIEW_LIMIT) + '…' : text;
25
+ }
26
+
27
+ // Reading markers means walking every model, so callers that only want the
28
+ // counts share this one pass. Exposed on the component for the status bar.
29
+ function collect() {
30
+ var monaco = window.monaco;
31
+ if (!monaco || !monaco.editor) return { errors: [], warnings: [], byFile: [] };
32
+
33
+ var errors = [];
34
+ var warnings = [];
35
+ var byFile = [];
36
+
37
+ monaco.editor.getModels().forEach(function (model) {
38
+ var path = model._mbeditorPath;
39
+ if (!path || model.isDisposed()) return;
40
+
41
+ var markers = monaco.editor.getModelMarkers({ resource: model.uri }).filter(function (m) {
42
+ return m.severity === SEVERITY_ERROR || m.severity === SEVERITY_WARNING;
43
+ });
44
+ if (markers.length === 0) return;
45
+
46
+ markers.sort(function (a, b) { return a.startLineNumber - b.startLineNumber; });
47
+
48
+ // Carry the source line alongside the marker: read here, while the model
49
+ // is in hand, rather than looking the model up again at render time.
50
+ var entries = markers.map(function (m) {
51
+ (m.severity === SEVERITY_ERROR ? errors : warnings).push(m);
52
+ return { marker: m, code: codePreview(model, m.startLineNumber) };
53
+ });
54
+
55
+ byFile.push({ path: path, markers: entries });
56
+ });
57
+
58
+ byFile.sort(function (a, b) { return a.path < b.path ? -1 : a.path > b.path ? 1 : 0; });
59
+ return { errors: errors, warnings: warnings, byFile: byFile };
60
+ }
61
+
62
+ function counts() {
63
+ var all = collect();
64
+ return { errors: all.errors.length, warnings: all.warnings.length };
65
+ }
66
+
67
+ var Panel = function ProblemsPanelComponent(_ref) {
68
+ var onClose = _ref.onClose;
69
+ var onOpenFile = _ref.onOpenFile;
70
+
71
+ var _problems = React.useState(collect);
72
+ var problems = _problems[0], setProblems = _problems[1];
73
+ var _filter = React.useState('');
74
+ var filter = _filter[0], setFilter = _filter[1];
75
+
76
+ var MIN_HEIGHT = 120;
77
+ var _height = React.useState(function () {
78
+ var saved = parseInt(window.localStorage.getItem('mbeditorProblemsHeight'), 10);
79
+ return (saved && saved >= MIN_HEIGHT) ? saved : 240;
80
+ });
81
+ var height = _height[0], setHeight = _height[1];
82
+ var heightRef = React.useRef(height);
83
+ heightRef.current = height;
84
+
85
+ // Same delta-based resize as the log drawer.
86
+ var onResizeMouseDown = function (e) {
87
+ e.preventDefault();
88
+ var startY = e.clientY;
89
+ var startHeight = heightRef.current;
90
+ var onMove = function (ev) {
91
+ var vh = window.innerHeight || document.documentElement.clientHeight || 0;
92
+ var maxH = vh > 0 ? Math.round(vh * 0.85) : Infinity;
93
+ setHeight(Math.min(maxH, Math.max(MIN_HEIGHT, startHeight + (startY - ev.clientY))));
94
+ };
95
+ var onUp = function () {
96
+ document.removeEventListener('mousemove', onMove);
97
+ document.removeEventListener('mouseup', onUp);
98
+ window.localStorage.setItem('mbeditorProblemsHeight', String(heightRef.current));
99
+ };
100
+ document.addEventListener('mousemove', onMove);
101
+ document.addEventListener('mouseup', onUp);
102
+ };
103
+
104
+ React.useEffect(function () {
105
+ if (!window.monaco || !window.monaco.editor) return;
106
+ var refresh = function () { setProblems(collect()); };
107
+ var sub = window.monaco.editor.onDidChangeMarkers(refresh);
108
+ refresh();
109
+ return function () { sub.dispose(); };
110
+ }, []);
111
+
112
+ var needle = filter.trim().toLowerCase();
113
+ var shown = needle
114
+ ? problems.byFile.map(function (entry) {
115
+ return {
116
+ path: entry.path,
117
+ markers: entry.markers.filter(function (item) {
118
+ return (item.marker.message + ' ' + item.code + ' ' + entry.path)
119
+ .toLowerCase().indexOf(needle) !== -1;
120
+ })
121
+ };
122
+ }).filter(function (entry) { return entry.markers.length > 0; })
123
+ : problems.byFile;
124
+
125
+ var total = problems.errors.length + problems.warnings.length;
126
+
127
+ return React.createElement(
128
+ 'div',
129
+ { className: 'ide-problems-drawer', style: { height: height + 'px' } },
130
+ React.createElement('div', {
131
+ className: 'ide-problems-resize',
132
+ title: 'Drag to resize',
133
+ onMouseDown: onResizeMouseDown
134
+ }),
135
+ React.createElement(
136
+ 'div',
137
+ { className: 'ide-problems-header' },
138
+ React.createElement('i', { className: 'fas fa-bug' }),
139
+ React.createElement('span', { className: 'ide-problems-title' }, 'Problems'),
140
+ React.createElement(
141
+ 'span',
142
+ { className: 'ide-problems-summary' },
143
+ problems.errors.length + ' error' + (problems.errors.length === 1 ? '' : 's') +
144
+ ', ' + problems.warnings.length + ' warning' + (problems.warnings.length === 1 ? '' : 's') +
145
+ ' in open files'
146
+ ),
147
+ React.createElement('input', {
148
+ className: 'ide-problems-filter',
149
+ type: 'text',
150
+ placeholder: 'Filter…',
151
+ value: filter,
152
+ onChange: function (e) { setFilter(e.target.value); }
153
+ }),
154
+ React.createElement('button', {
155
+ type: 'button', className: 'ide-problems-btn',
156
+ title: 'Close', onClick: onClose
157
+ }, React.createElement('i', { className: 'fas fa-times' }))
158
+ ),
159
+ React.createElement(
160
+ 'div',
161
+ { className: 'ide-problems-body' },
162
+ total === 0
163
+ ? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems in the open files')
164
+ : shown.length === 0
165
+ ? React.createElement('div', { className: 'ide-problems-empty' }, 'No problems match the filter')
166
+ : shown.map(function (entry) {
167
+ return React.createElement(
168
+ 'div',
169
+ { className: 'ide-problems-file', key: entry.path },
170
+ React.createElement(
171
+ 'div',
172
+ { className: 'ide-problems-file-name' },
173
+ entry.path,
174
+ React.createElement('span', { className: 'ide-problems-file-count' }, entry.markers.length)
175
+ ),
176
+ entry.markers.map(function (item, index) {
177
+ var marker = item.marker;
178
+ var isError = marker.severity === SEVERITY_ERROR;
179
+ return React.createElement(
180
+ 'button',
181
+ {
182
+ type: 'button',
183
+ className: 'ide-problems-item ide-problems-item-' + (isError ? 'error' : 'warning'),
184
+ key: entry.path + ':' + marker.startLineNumber + ':' + index,
185
+ 'aria-label': (isError ? 'Error' : 'Warning') + ': ' + marker.message +
186
+ ', ' + entry.path + ' line ' + marker.startLineNumber +
187
+ (item.code ? ', source: ' + item.code : ''),
188
+ onClick: function () {
189
+ if (onOpenFile) onOpenFile(entry.path, marker.startLineNumber, marker.startColumn);
190
+ }
191
+ },
192
+ React.createElement('i', {
193
+ className: 'fas ' + (isError ? 'fa-bug' : 'fa-exclamation-triangle') + ' ide-problems-icon',
194
+ 'aria-hidden': 'true'
195
+ }),
196
+ React.createElement('span', { className: 'ide-problems-msg' }, marker.message),
197
+ item.code && React.createElement('code', { className: 'ide-problems-code' }, item.code),
198
+ marker.source && React.createElement('span', { className: 'ide-problems-source' }, marker.source),
199
+ React.createElement(
200
+ 'span',
201
+ { className: 'ide-problems-loc' },
202
+ '[' + marker.startLineNumber + ', ' + marker.startColumn + ']'
203
+ )
204
+ );
205
+ })
206
+ );
207
+ })
208
+ )
209
+ );
210
+ };
211
+
212
+ Panel.collect = collect;
213
+ Panel.counts = counts;
214
+ return Panel;
215
+ })();
216
+
217
+ window.ProblemsPanel = ProblemsPanel;
@@ -94,6 +94,20 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
94
94
  return 50;
95
95
  }
96
96
 
97
+ // Recently opened files, most recent first, as a path -> rank lookup. Built
98
+ // once per query rather than per comparison, since the sort calls this for
99
+ // every pair.
100
+ function recentRanks() {
101
+ var ranks = {};
102
+ var recent = (typeof TabManager !== 'undefined' && TabManager.getRecentFiles)
103
+ ? TabManager.getRecentFiles() : [];
104
+ recent.forEach(function (entry, index) {
105
+ var path = typeof entry === 'string' ? entry : (entry && entry.path);
106
+ if (path && !(path in ranks)) ranks[path] = index;
107
+ });
108
+ return ranks;
109
+ }
110
+
97
111
  // Match relevance within a priority tier: exact basename > prefix > substring > other.
98
112
  function getMatchRelevance(result, q) {
99
113
  if (!q) return 3;
@@ -128,13 +142,30 @@ var QuickOpenDialog = function QuickOpenDialog(_ref) {
128
142
  var res = SearchService.searchFiles(query);
129
143
  // Filter by type: always include files; include dirs only when showFolders is on
130
144
  var filtered = showFolders ? res : res.filter(function(r) { return r.type !== 'dir'; });
131
- // Sort: files always beat dirs; within the same tier exact basename > prefix > substring > other.
132
- // JS sort is stable in modern engines so MiniSearch relevance score order is the tiebreaker
133
- // when match relevance is equal.
145
+ // Sort, in order of precedence:
146
+ // 1. match quality exact basename > prefix > substring > other, so a
147
+ // worse match can never jump the queue however recently it was opened
148
+ // 2. how recently the file was opened, most recent first
149
+ // 3. the static file-type tier (controller > model > … > noise)
150
+ //
151
+ // Recency sits above the type tier deliberately: when two files match a
152
+ // query equally well, the one you were just working in is almost always
153
+ // the one you meant, and that beats a guess made from the directory name.
154
+ // Files never opened all tie here and fall through to the type tier, which
155
+ // is what orders the bulk of a cold result list.
156
+ //
157
+ // Directories keep their +100 penalty inside the type tier so files still
158
+ // come first. JS sort is stable in modern engines, so MiniSearch's own
159
+ // relevance order remains the final tiebreaker.
160
+ var ranks = recentRanks();
161
+ var NEVER_OPENED = Infinity;
134
162
  filtered.sort(function(a, b) {
135
163
  var aRelevance = getMatchRelevance(a, query);
136
164
  var bRelevance = getMatchRelevance(b, query);
137
165
  if (aRelevance !== bRelevance) return aRelevance - bRelevance;
166
+ var aRecent = a.path in ranks ? ranks[a.path] : NEVER_OPENED;
167
+ var bRecent = b.path in ranks ? ranks[b.path] : NEVER_OPENED;
168
+ if (aRecent !== bRecent) return aRecent - bRecent;
138
169
  var aPriority = getFilePriority(a.path) + (a.type === 'dir' ? 100 : 0);
139
170
  var bPriority = getFilePriority(b.path) + (b.type === 'dir' ? 100 : 0);
140
171
  return aPriority - bPriority;