mbeditor 0.9.0 → 0.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2612297fd4760bf7660ab89589c549ff6a815df395e4f57fb48d6f60c1c519b8
4
- data.tar.gz: d9bf030f5f056754bfe8c633253aba3013c7405aaba259628c0e7aa5e7b19aff
3
+ metadata.gz: 1fe258515a6f0945da8bfcbfbe85197262af224c054924367597d97d370530e2
4
+ data.tar.gz: d3f65b3c351fef56291170d9401103d9b73d4b3a7ebad24885713b8deef5df85
5
5
  SHA512:
6
- metadata.gz: 8660eda73b4cb4e3d38ab7c820286a363340e6a65592e66817c5b0594374e60532769c06999fb9a099b6082ee99f4fd1f604ec6116908f63317daed1153780d7
7
- data.tar.gz: b27df09cb524a48508285abe5892b8f261f44efaa471fe580a839be1e3f2064d385848aa4e2b488af812d88b3181ee9f51e26c3e3cb84bc79353c619184ddd80
6
+ metadata.gz: 4de0d8f9374a370f4df1339166ff648013dafb07b23eeba6936f3ebf1d492c831905d7143d5ece2ece4e397f4f4b314b7461e496cec00003c507db5701347604
7
+ data.tar.gz: 17c4ef992175fc0a5fd8f18102214ac46cce75963ba7d06661f3ffac66eb7e7d55e69bef6c6f71e3ddb9999839c78fd9a2668abb99397442ac7bfb68da6ee561
data/CHANGELOG.md CHANGED
@@ -5,6 +5,64 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.10.0] - 2026-07-27
9
+
10
+ ### Added
11
+ - **Git-status line numbers** — line numbers are tinted by what git thinks of
12
+ the line: green for added, orange for modified, red on the line a deleted
13
+ block sat after. Backed by a new `GET /mbeditor/git/line_diff` endpoint over
14
+ `git diff -U0`, and never a reason to fail a file load: a missing HEAD or a
15
+ non-repo simply leaves the numbers plain.
16
+ - **Problems panel** — error and warning counts in the status bar open a
17
+ drawer listing every diagnostic across the open files, grouped by file with
18
+ the offending source line and click-to-navigate. Counts cover the open tabs,
19
+ which is where Monaco's markers live; RuboCop, ruby-lsp and the TypeScript
20
+ worker all feed it through one subscription.
21
+ - **Outline for JS, JSX and TypeScript** — the Methods/Outline button now
22
+ works in those files, translating the TypeScript worker's own navigation
23
+ tree rather than adding a second lexer. Arrow functions and function
24
+ expressions assigned to variables are listed; data constants, object-literal
25
+ keys and anonymous callbacks are not.
26
+ - **Colour-coded Rails log** — the log drawer distinguishes request
27
+ boundaries, controller dispatch, SQL, renders, redirects and failures, with
28
+ `Completed` lines coloured by status code.
29
+ - **Optional workspace file watching** — with the host's
30
+ [`listen`](https://github.com/guard/listen) gem, changes made outside the
31
+ editor (a terminal `git checkout`, a generator, another editor) refresh the
32
+ file tree and git decorations instead of going stale. Absent the gem,
33
+ behaviour is unchanged. Configurable via `config.watch_files`.
34
+
35
+ ### Fixed
36
+ - **ruby-lsp hover showed a dead link.** ruby-lsp renders its "Definitions"
37
+ line as VS Code `file://` links, which Monaco draws as links but nothing in
38
+ the browser can open, so clicking did nothing. In-workspace links are now
39
+ rewritten to a Monaco command that opens the file at the line; gem and
40
+ stdlib links, which the editor cannot open at all, degrade to plain code
41
+ spans. The `/module_members` breakdown is also restored beneath a constant
42
+ hover — ruby-lsp's constant hover never lists what the class or module
43
+ defines, so taking its output wholesale had dropped it.
44
+ - **False-positive type errors in plain JS/JSX.** The editor kept a denylist
45
+ of TypeScript diagnostic codes to suppress; it had grown to eight and still
46
+ leaked (TS2322 on a spread carrying an extra prop). Untyped JS gives
47
+ TypeScript nothing to check against, and which way it guesses is arbitrary —
48
+ state seeded with `useState({})` errors on every key while the same object
49
+ from `JSON.parse` stays silent. JS/JSX now keeps only the checks that are
50
+ sound without annotations: syntax errors, `Cannot find name`, and unused
51
+ locals. `.ts`/`.tsx` keeps full checking, where the types are hand-written.
52
+
53
+ ### Changed
54
+ - The **Logs** button moved from the top toolbar to the status bar.
55
+ - **Quick-open ranks recently opened files** above the static file-type tier,
56
+ so a file you were just in outranks a never-opened controller. Match quality
57
+ still comes first, so a worse match cannot jump the queue.
58
+ - The title-bar search field now fills 75% of the space between the title and
59
+ the toolbar buttons instead of a fixed 340px.
60
+ - Test-suite compatibility fixes carried over from the unreleased 0.9.1/0.9.2
61
+ work: MiniRacer-dependent parser suites skip in minimal bundles, and the
62
+ Outline system tests exercise navigation through click semantics.
63
+
64
+ ---
65
+
8
66
  ## [0.9.0] - 2026-07-23
9
67
 
10
68
  ### Added
data/README.md CHANGED
@@ -9,9 +9,14 @@ Mbeditor (Mini Browser Editor) is a mountable Rails engine that adds a browser-b
9
9
  - Two-pane tabbed editor with drag-to-move tabs
10
10
  - File tree and project search
11
11
  - Git panel with working tree changes, unpushed file changes, and branch commit titles
12
+ - Line numbers tinted by git status — green for added lines, orange for modified, red where lines were removed
13
+ - Problems panel with error/warning counts in the status bar, listing every diagnostic across the open files
14
+ - Outline dropdown for Ruby (methods, and test/spec structure in test files) and for JS/JSX/TS
15
+ - Colour-coded Rails log drawer
12
16
  - Optional RuboCop lint and format endpoints (uses host app RuboCop)
13
17
  - Optional Ruby language-server integration (definitions, hover, completion, diagnostics)
14
18
  - Optional test runner with inline failure markers and a dedicated results panel (Minitest and RSpec)
19
+ - Optional workspace file watching, so changes made outside the editor refresh the tree and git decorations
15
20
 
16
21
  ## Security Warning
17
22
  Mbeditor exposes read and write access to your Rails application directory over HTTP. It is intended only for local development.
@@ -96,6 +101,7 @@ end
96
101
  | `ruby_lsp` | `:auto` | Use the host's [ruby-lsp](https://github.com/Shopify/ruby-lsp) for Ruby go-to-definition, hover, completion, and diagnostics when it's installed (a persistent process is managed per workspace). `false` disables. Without ruby-lsp everything degrades to the built-in grep/Ripper services — no behavior change. |
97
102
  | `ruby_lsp_command` | `nil` | Override the ruby-lsp launch command (String or Array). `nil` auto-resolves `bin/ruby-lsp` → installed gem → `bundle exec ruby-lsp`. |
98
103
  | `ruby_lsp_timeout` | `3` | Seconds per LSP request; on timeout (e.g. during initial indexing) the editor falls back to the built-in services for that request. |
104
+ | `watch_files` | `:auto` | Watch the workspace for changes made outside the editor (a terminal `git checkout`, a generator, another editor) and push a refresh to open clients. Requires the host's [`listen`](https://github.com/guard/listen) gem; without it the editor behaves as before and only announces its own writes. `false` disables. |
99
105
 
100
106
  ### Authentication
101
107
 
@@ -199,9 +205,34 @@ The gem keeps host/tooling responsibilities in the host app:
199
205
  - `minitest` or `rspec` in the host app's bundle (required for the test runner)
200
206
  - `actioncable` framework/gem (optional, required only for realtime file-change push + websocket state saves)
201
207
  - `ruby-lsp` gem (optional — see below)
208
+ - `listen` gem (optional — see below)
202
209
 
203
210
  All lint and test tools are auto-detected at runtime. The engine gracefully disables features if the tools are not available. Neither `rubocop`, `haml_lint`, nor any test framework are runtime dependencies of the gem itself — they are discovered from the host app's environment.
204
211
 
212
+ ### Workspace file watching (Optional)
213
+
214
+ Add [`listen`](https://github.com/guard/listen) to the host app's development
215
+ group and mbeditor watches the workspace for changes it did not make itself:
216
+
217
+ ```ruby
218
+ group :development do
219
+ gem 'listen'
220
+ end
221
+ ```
222
+
223
+ Without it, only writes made *through* the editor announce themselves, so a
224
+ `git checkout` or a generator run in a terminal leaves the file tree and the
225
+ git line-number colours stale until you reopen the file. With it, those refresh
226
+ on their own.
227
+
228
+ The watcher runs only in `allowed_environments` and only in processes that
229
+ serve requests, so rake tasks and consoles never start one. It respects
230
+ `excluded_paths`, coalesces bursts (a branch switch is one refresh, not
231
+ hundreds), and requires Action Cable to actually deliver the push. If it cannot
232
+ start — inotify limits, permissions — it logs a warning and the editor carries
233
+ on exactly as it would without the gem. Set `config.watch_files = false` to
234
+ disable it even when `listen` is present.
235
+
205
236
  ### Ruby language server (Optional)
206
237
 
207
238
  Add [ruby-lsp](https://github.com/Shopify/ruby-lsp) to the host app's
@@ -12,6 +12,7 @@
12
12
  //= require mbeditor/color_provider
13
13
  //= require mbeditor/editor_plugins
14
14
  //= require mbeditor/ruby_outline
15
+ //= require mbeditor/js_outline
15
16
  //= require mbeditor/components/CollapsibleSection
16
17
  //= require mbeditor/components/ShortcutHelp
17
18
  //= require mbeditor/components/DiffViewer
@@ -21,6 +22,7 @@
21
22
  //= require mbeditor/components/FileHistoryPanel
22
23
  //= require mbeditor/components/TestResultsPanel
23
24
  //= require mbeditor/components/LogPanel
25
+ //= require mbeditor/components/ProblemsPanel
24
26
  //= require mbeditor/components/CodeReviewPanel
25
27
  //= require mbeditor/components/EditorPanel
26
28
  //= require mbeditor/components/FileTree
@@ -69,6 +69,7 @@ var EditorPanel = function EditorPanel(_ref) {
69
69
  var setIsBlameLoading = _useState8[1];
70
70
 
71
71
  var blameDecorationsRef = useRef([]);
72
+ var gitLineDecorationsRef = useRef([]);
72
73
  var blameZoneIdsRef = useRef([]);
73
74
  var testDecorationIdsRef = useRef([]);
74
75
  var testZoneIdsRef = useRef([]);
@@ -105,6 +106,8 @@ var EditorPanel = function EditorPanel(_ref) {
105
106
 
106
107
  var methodsBtnRef = useRef(null);
107
108
  var methodsDropdownRef = useRef(null);
109
+ // Discards a JS outline that resolves after the dropdown was closed or reopened.
110
+ var methodsRequestRef = useRef(0);
108
111
 
109
112
  // Local pagination state — initialized from tab props; updated on page navigation
110
113
  var _useState21 = useState(tab.startLine || 0);
@@ -1281,6 +1284,80 @@ var EditorPanel = function EditorPanel(_ref) {
1281
1284
  }
1282
1285
  }, [tab.path]);
1283
1286
 
1287
+ // Tint line numbers by git status: green added, orange modified, red where
1288
+ // lines were removed.
1289
+ //
1290
+ // The ranges come from `git diff -U0` against HEAD, so they describe the file
1291
+ // as last written to disk. Monaco anchors decorations to the model and shifts
1292
+ // them as you type, which keeps them roughly right mid-edit; the authoritative
1293
+ // refresh happens when the file lands on disk and the server broadcasts it.
1294
+ useEffect(function () {
1295
+ if (!gitAvailable || !tab.path || tab.isDiff || tab.isCombinedDiff) return;
1296
+
1297
+ var cancelled = false;
1298
+
1299
+ function clear() {
1300
+ if (!monacoRef.current || !monacoRef.current.getModel()) return;
1301
+ gitLineDecorationsRef.current =
1302
+ monacoRef.current.deltaDecorations(gitLineDecorationsRef.current, []);
1303
+ }
1304
+
1305
+ function apply(data) {
1306
+ var editor = monacoRef.current;
1307
+ var model = editor && editor.getModel();
1308
+ if (!model || !window.monaco) return;
1309
+
1310
+ var lineCount = model.getLineCount();
1311
+ var decorations = [];
1312
+
1313
+ function push(ranges, className) {
1314
+ (ranges || []).forEach(function (range) {
1315
+ // A deletion above the first line is reported as line 0; mark line 1
1316
+ // so the file's opening line carries the flag.
1317
+ var start = Math.max(1, Math.min(range.start, lineCount));
1318
+ var end = Math.max(start, Math.min(range.end, lineCount));
1319
+ for (var line = start; line <= end; line++) {
1320
+ decorations.push({
1321
+ range: new window.monaco.Range(line, 1, line, 1),
1322
+ options: { isWholeLine: true, lineNumberClassName: className }
1323
+ });
1324
+ }
1325
+ });
1326
+ }
1327
+
1328
+ push(data.added, 'mbeditor-gitline-added');
1329
+ push(data.modified, 'mbeditor-gitline-modified');
1330
+ push(data.deleted, 'mbeditor-gitline-deleted');
1331
+
1332
+ gitLineDecorationsRef.current =
1333
+ editor.deltaDecorations(gitLineDecorationsRef.current, decorations);
1334
+ }
1335
+
1336
+ function refresh() {
1337
+ var path = tab.path;
1338
+ GitService.fetchLineDiff(path).then(function (data) {
1339
+ // The tab can be switched or closed while the request is in flight.
1340
+ if (cancelled || !data || tab.path !== path) return;
1341
+ apply(data);
1342
+ }).catch(function () {
1343
+ // Not a repo, file not readable, git missing — leave the numbers plain.
1344
+ if (!cancelled) clear();
1345
+ });
1346
+ }
1347
+
1348
+ refresh();
1349
+
1350
+ var hasSocket = typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged;
1351
+ var onChanged = hasSocket ? function () { refresh(); } : null;
1352
+ if (onChanged) WebSocketService.onFilesChanged(onChanged);
1353
+
1354
+ return function () {
1355
+ cancelled = true;
1356
+ if (onChanged && WebSocketService.offFilesChanged) WebSocketService.offFilesChanged(onChanged);
1357
+ clear();
1358
+ };
1359
+ }, [tab.path, tab.externalContentVersion, tab.isDiff, tab.isCombinedDiff, gitAvailable]);
1360
+
1284
1361
  // Handle Blame data fetching
1285
1362
  useEffect(function () {
1286
1363
  if (!isBlameVisible) {
@@ -1612,6 +1689,8 @@ var EditorPanel = function EditorPanel(_ref) {
1612
1689
  var fileBaseName = (tab.path || '').split('/').pop().toLowerCase();
1613
1690
  var isRubyFile = ext === 'rb' || ext === 'ruby' || ext === 'gemspec' || ext === 'rake' ||
1614
1691
  fileBaseName === 'gemfile' || fileBaseName === 'gemfile.lock' || fileBaseName === 'rakefile';
1692
+ var isJsFile = ['js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx'].indexOf(ext) !== -1;
1693
+ var hasOutline = isRubyFile || isJsFile;
1615
1694
  var isTestOutline = false;
1616
1695
  try {
1617
1696
  isTestOutline = isRubyFile && window.RubyOutline &&
@@ -1695,6 +1774,33 @@ var EditorPanel = function EditorPanel(_ref) {
1695
1774
  return window.RubyOutline.parse(lines, { path: path });
1696
1775
  }
1697
1776
 
1777
+ // The JS/TS outline comes from the TypeScript worker rather than a lexer of
1778
+ // our own, which makes it async — the worker is where the parse already
1779
+ // lives, and it tracks the live buffer, so unsaved edits are included.
1780
+ function loadJsOutline(model) {
1781
+ var ts = window.monaco && window.monaco.languages && window.monaco.languages.typescript;
1782
+ if (!ts || !window.JsOutline) return Promise.reject(new Error('outline unavailable'));
1783
+
1784
+ var workerFor = model.getLanguageId() === 'typescript' ? ts.getTypeScriptWorker : ts.getJavaScriptWorker;
1785
+ return workerFor().then(function (getWorker) {
1786
+ return getWorker(model.uri);
1787
+ }).then(function (client) {
1788
+ return client.getNavigationTree(model.uri.toString());
1789
+ }).then(function (tree) {
1790
+ return window.JsOutline.fromNavigationTree(tree, {
1791
+ lineAt: function (offset) { return model.getPositionAt(offset).lineNumber; },
1792
+ textAt: function (offset, length) {
1793
+ var start = model.getPositionAt(offset);
1794
+ var end = model.getPositionAt(offset + length);
1795
+ return model.getValueInRange({
1796
+ startLineNumber: start.lineNumber, startColumn: start.column,
1797
+ endLineNumber: end.lineNumber, endColumn: end.column
1798
+ });
1799
+ }
1800
+ });
1801
+ });
1802
+ }
1803
+
1698
1804
  if (tab.fileNotFound) {
1699
1805
  return React.createElement(
1700
1806
  'div',
@@ -1834,31 +1940,55 @@ var EditorPanel = function EditorPanel(_ref) {
1834
1940
  React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
1835
1941
  !editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
1836
1942
  ),
1837
- isRubyFile && React.createElement(
1943
+ hasOutline && React.createElement(
1838
1944
  'button',
1839
1945
  {
1840
1946
  ref: methodsBtnRef,
1841
1947
  className: 'ide-icon-btn' + (methodsOpen ? ' active' : ''),
1842
1948
  onClick: function() {
1843
- var nextOpen = !methodsOpen;
1844
- if (nextOpen) {
1845
- var model = monacoRef.current && monacoRef.current.getModel();
1846
- try {
1847
- var result = model ? parseRubyOutline(model, tab.path) : { entries: [], truncated: false };
1848
- setMethodsList(result.entries || []);
1849
- setMethodsTruncated(!!result.truncated);
1850
- setMethodsUnavailable(false);
1851
- } catch (err) {
1852
- setMethodsList([]);
1853
- setMethodsTruncated(false);
1854
- setMethodsUnavailable(true);
1855
- }
1949
+ // Bump first: a click that closes the dropdown must also cancel an
1950
+ // outline request still in flight from the click that opened it.
1951
+ var requestId = ++methodsRequestRef.current;
1952
+ if (methodsOpen) {
1953
+ setMethodsOpen(false);
1954
+ return;
1955
+ }
1956
+
1957
+ var model = monacoRef.current && monacoRef.current.getModel();
1958
+
1959
+ function openWith(entries, truncated, unavailable) {
1960
+ setMethodsList(entries);
1961
+ setMethodsTruncated(truncated);
1962
+ setMethodsUnavailable(unavailable);
1856
1963
  if (methodsBtnRef.current) {
1857
1964
  var rect = methodsBtnRef.current.getBoundingClientRect();
1858
1965
  setMethodsDropdownPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
1859
1966
  }
1967
+ setMethodsOpen(true);
1968
+ }
1969
+
1970
+ if (isJsFile) {
1971
+ // The worker has already parsed the open buffer for diagnostics,
1972
+ // so this resolves in a few ms — opening only once it answers
1973
+ // avoids flashing "No methods found" on the way.
1974
+ (model ? loadJsOutline(model) : Promise.resolve({ entries: [], truncated: false }))
1975
+ .then(function (result) {
1976
+ if (methodsRequestRef.current !== requestId) return;
1977
+ openWith(result.entries || [], !!result.truncated, false);
1978
+ })
1979
+ .catch(function () {
1980
+ if (methodsRequestRef.current !== requestId) return;
1981
+ openWith([], false, true);
1982
+ });
1983
+ return;
1984
+ }
1985
+
1986
+ try {
1987
+ var result = model ? parseRubyOutline(model, tab.path) : { entries: [], truncated: false };
1988
+ openWith(result.entries || [], !!result.truncated, false);
1989
+ } catch (err) {
1990
+ openWith([], false, true);
1860
1991
  }
1861
- setMethodsOpen(nextOpen);
1862
1992
  },
1863
1993
  title: isTestOutline ? 'Jump to Outline' : 'Jump to Method'
1864
1994
  },
@@ -2099,7 +2229,9 @@ var EditorPanel = function EditorPanel(_ref) {
2099
2229
  var row = React.createElement(
2100
2230
  'button',
2101
2231
  {
2102
- key: entry.kind + '-' + entry.line,
2232
+ // Index-qualified: a JS class and its first member can share
2233
+ // a line, and kind+line alone would collide.
2234
+ key: entry.kind + '-' + entry.line + '-' + entryIndex,
2103
2235
  type: 'button',
2104
2236
  className: 'ide-methods-dropdown-item ide-outline-entry ide-outline-entry-' + entry.kind,
2105
2237
  'data-outline-kind': entry.kind,
@@ -3,6 +3,49 @@
3
3
  // LogPanel — bottom drawer that renders the live Rails log. Auto-scrolls to the
4
4
  // tail, pauses auto-scroll when the user scrolls up, and supports a substring
5
5
  // filter and clear. Driven entirely by LogService.
6
+
7
+ // Classify one Rails log line so CSS can colour it. First match wins, so the
8
+ // order matters: "Completed 500" has to be read as an error before the generic
9
+ // /error/ sweep at the bottom claims it, and a SQL line mentioning "error" in a
10
+ // string literal must not turn the whole row red.
11
+ //
12
+ // Rails writes these lines without ANSI codes when the log goes to a file, so
13
+ // there is nothing to parse — this is pattern matching on the text, and an
14
+ // unrecognised line simply renders in the default colour.
15
+ var LOG_LINE_RULES = [
16
+ // Request lifecycle. The status code decides the colour of a Completed line.
17
+ [/^Completed [45]\d\d\b/, 'error'],
18
+ [/^Completed 3\d\d\b/, 'muted'],
19
+ [/^Completed 2\d\d\b/, 'success'],
20
+ [/^Started [A-Z]+ /, 'request'],
21
+ [/^Processing by /, 'controller'],
22
+ [/^Redirected to /, 'muted'],
23
+ [/^\s*(Rendering|Rendered) /, 'render'],
24
+ // Queries: "User Load (0.3ms) SELECT ..." and its CACHE/TRANSACTION variants.
25
+ [/^\s*(CACHE\s+)?[\w:]+\s*(Load|Create|Update|Destroy|Exists\?|Count|Pluck|Sum|Delete)?\s*\(\d+(\.\d+)?ms\)\s+(SELECT|INSERT|UPDATE|DELETE|BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE)\b/i, 'sql'],
26
+ [/^\s*(TRANSACTION|SQL)\s+\(/, 'sql'],
27
+ // Failures and noise.
28
+ [/^\s*(FATAL|ERROR)\b/, 'error'],
29
+ [/^\s*[\w/.]+:\d+:in [`']/, 'trace'],
30
+ [/DEPRECATION WARNING/, 'warn'],
31
+ [/^\s*(WARN|WARNING)\b/, 'warn'],
32
+ // Rails prints an unhandled exception as "Some::ConstantName (message):",
33
+ // which carries no Error/Exception in its name often enough to need its own
34
+ // rule (ActiveRecord::RecordNotFound, ActionController::RoutingError…).
35
+ [/^\s*[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*\s+\(.*\):\s*$/, 'error'],
36
+ [/\b\w*(Error|Exception)\b\s*[:(]/, 'error'],
37
+ // mbeditor's own diagnostics, so they stand out from host app noise.
38
+ [/^\s*\[mbeditor\]/, 'mbeditor']
39
+ ];
40
+
41
+ function classifyLogLine(line) {
42
+ var text = String(line == null ? '' : line);
43
+ for (var i = 0; i < LOG_LINE_RULES.length; i++) {
44
+ if (LOG_LINE_RULES[i][0].test(text)) return LOG_LINE_RULES[i][1];
45
+ }
46
+ return null;
47
+ }
48
+
6
49
  var LogPanel = function LogPanel(_ref) {
7
50
  var onClose = _ref.onClose;
8
51
 
@@ -118,10 +161,16 @@ var LogPanel = function LogPanel(_ref) {
118
161
  'div',
119
162
  { className: 'ide-log-body', ref: bodyRef, onScroll: onScroll },
120
163
  shown.map(function (line, i) {
121
- return React.createElement('div', { className: 'ide-log-line', key: i }, line);
164
+ var kind = classifyLogLine(line);
165
+ return React.createElement('div', {
166
+ className: 'ide-log-line' + (kind ? ' ide-log-line-' + kind : ''),
167
+ key: i
168
+ }, line);
122
169
  })
123
170
  )
124
171
  );
125
172
  };
126
173
 
174
+ LogPanel.classifyLine = classifyLogLine;
175
+
127
176
  window.LogPanel = LogPanel;
@@ -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];
@@ -1536,6 +1547,31 @@ var MbeditorApp = function MbeditorApp() {
1536
1547
  };
1537
1548
  }, []);
1538
1549
 
1550
+ // Keep the status-bar error/warning tallies in step with Monaco's markers.
1551
+ // Every diagnostic source in the editor — rubocop, ruby-lsp, the TypeScript
1552
+ // worker — lands here, so one subscription covers all of them. Markers change
1553
+ // on every keystroke for JS, so the recount is debounced.
1554
+ useEffect(function () {
1555
+ if (!monacoReady || !window.monaco || !window.monaco.editor || !window.ProblemsPanel) return;
1556
+
1557
+ var timer = null;
1558
+ var recount = function () {
1559
+ if (timer) clearTimeout(timer);
1560
+ timer = setTimeout(function () {
1561
+ timer = null;
1562
+ setProblemCounts(window.ProblemsPanel.counts());
1563
+ }, 250);
1564
+ };
1565
+
1566
+ var sub = window.monaco.editor.onDidChangeMarkers(recount);
1567
+ recount();
1568
+
1569
+ return function () {
1570
+ if (timer) clearTimeout(timer);
1571
+ sub.dispose();
1572
+ };
1573
+ }, [monacoReady]);
1574
+
1539
1575
  var handleSelectFile = function handleSelectFile(path, name, line, col) {
1540
1576
  TabManager.openTab(path, name, line, null, false, col);
1541
1577
  handleNodeSelect({ path: path, name: name || path.split('/').pop(), type: 'file' });
@@ -2747,6 +2783,10 @@ var MbeditorApp = function MbeditorApp() {
2747
2783
  setShowLogPanel(function (prev) { return !prev; });
2748
2784
  };
2749
2785
 
2786
+ var toggleProblemsPanel = function toggleProblemsPanel() {
2787
+ setShowProblemsPanel(function (prev) { return !prev; });
2788
+ };
2789
+
2750
2790
  var toggleZenMode = function toggleZenMode() {
2751
2791
  setZenMode(function (prev) {
2752
2792
  var next = !prev;
@@ -3382,16 +3422,24 @@ var MbeditorApp = function MbeditorApp() {
3382
3422
  "Mini Browser Editor — ",
3383
3423
  window.location.host
3384
3424
  ),
3425
+ // The slot claims all the room between the title and the buttons; the
3426
+ // search pill then takes 75% of it, centred. Sizing the pill against a
3427
+ // slot rather than against the title bar keeps it proportional to the
3428
+ // actual gap as the toolbar's button labels grow and shrink.
3385
3429
  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…')
3430
+ 'div',
3431
+ { className: 'ide-titlebar-search-slot' },
3432
+ React.createElement(
3433
+ 'button',
3434
+ {
3435
+ type: 'button',
3436
+ className: 'ide-titlebar-search',
3437
+ title: 'Search files (Ctrl/Cmd+P)',
3438
+ onClick: function () { setQuickOpen(true); }
3439
+ },
3440
+ React.createElement('i', { className: 'fas fa-search', style: { marginRight: '6px', opacity: 0.7 } }),
3441
+ React.createElement('span', { className: 'ide-titlebar-search-text' }, 'Search files…')
3442
+ )
3395
3443
  ),
3396
3444
  React.createElement(
3397
3445
  "div",
@@ -3455,13 +3503,6 @@ var MbeditorApp = function MbeditorApp() {
3455
3503
  )
3456
3504
  ),
3457
3505
  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
3506
  React.createElement(
3466
3507
  "button",
3467
3508
  { type: "button", className: "statusbar-btn", onClick: function () { return setShowHelp(true); }, title: "Keyboard shortcuts & help" },
@@ -4974,6 +5015,12 @@ var MbeditorApp = function MbeditorApp() {
4974
5015
  ),
4975
5016
  showLogPanel && !zenMode && React.createElement(window.LogPanel || LogPanel, {
4976
5017
  onClose: function () { setShowLogPanel(false); }
5018
+ }),
5019
+ showProblemsPanel && !zenMode && React.createElement(window.ProblemsPanel || ProblemsPanel, {
5020
+ onClose: function () { setShowProblemsPanel(false); },
5021
+ onOpenFile: function (path, line, col) {
5022
+ handleSelectFile(path, path.split('/').pop(), line, col);
5023
+ }
4977
5024
  })
4978
5025
  ),
4979
5026
  React.createElement(
@@ -4998,6 +5045,23 @@ var MbeditorApp = function MbeditorApp() {
4998
5045
  state.gitInfo.behind
4999
5046
  )
5000
5047
  ),
5048
+ React.createElement(
5049
+ "button",
5050
+ {
5051
+ type: "button",
5052
+ className: "statusbar-btn statusbar-problems" + (showProblemsPanel ? " active" : ""),
5053
+ onClick: toggleProblemsPanel,
5054
+ title: problemCounts.errors + " error(s), " + problemCounts.warnings +
5055
+ " warning(s) in open files — click to open Problems"
5056
+ },
5057
+ React.createElement("i", { className: "fas fa-bug statusbar-problems-error-icon" }),
5058
+ React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.errors),
5059
+ React.createElement("i", {
5060
+ className: "fas fa-exclamation-triangle statusbar-problems-warning-icon",
5061
+ style: { marginLeft: "8px" }
5062
+ }),
5063
+ React.createElement("span", { className: "statusbar-problems-count" }, problemCounts.warnings)
5064
+ ),
5001
5065
  !serverOnline && (function () {
5002
5066
  var dirtyCount = state.panes.reduce(function (acc, p) {
5003
5067
  return acc + p.tabs.filter(function (t) { return t.dirty; }).length;
@@ -5027,6 +5091,17 @@ var MbeditorApp = function MbeditorApp() {
5027
5091
  { className: "statusbar-msg " + state.statusMessage.kind },
5028
5092
  state.statusMessage.text
5029
5093
  ),
5094
+ React.createElement(
5095
+ "button",
5096
+ {
5097
+ type: "button",
5098
+ className: "statusbar-btn statusbar-logs-btn" + (showLogPanel ? " active" : ""),
5099
+ onClick: toggleLogPanel,
5100
+ title: "Toggle Rails log (Ctrl+Shift+L)"
5101
+ },
5102
+ React.createElement("i", { className: "fas fa-stream" }),
5103
+ " Logs"
5104
+ ),
5030
5105
  activeEOL && React.createElement(
5031
5106
  "button",
5032
5107
  {