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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2612297fd4760bf7660ab89589c549ff6a815df395e4f57fb48d6f60c1c519b8
4
- data.tar.gz: d9bf030f5f056754bfe8c633253aba3013c7405aaba259628c0e7aa5e7b19aff
3
+ metadata.gz: 47a8199a082957d396dce3cfa9de010ece11a11b2b938cf0cf81adfa6dd171f1
4
+ data.tar.gz: 30dfee949bc6d06245c6bd1ba5b682431b36b064e0ad510b0dac44b320c5a287
5
5
  SHA512:
6
- metadata.gz: 8660eda73b4cb4e3d38ab7c820286a363340e6a65592e66817c5b0594374e60532769c06999fb9a099b6082ee99f4fd1f604ec6116908f63317daed1153780d7
7
- data.tar.gz: b27df09cb524a48508285abe5892b8f261f44efaa471fe580a839be1e3f2064d385848aa4e2b488af812d88b3181ee9f51e26c3e3cb84bc79353c619184ddd80
6
+ metadata.gz: 81c342ce0dc3f80c44259ae3a130710013dcb81f45052d4cc4488d2a7dc112f7decc6b5d67f3f6aaa81cbcd7ef391d82136ee1adccdd369b369c7cfcd4d6afa7
7
+ data.tar.gz: b201dda9b9fe55e9f44981725788b1587bc052f940652e8eb7d86a8dcc3d17c45bb0984f71cd3bbbd9f3a001b3b0125ac09a9e53eedd3e580eab96a445be6a17
data/CHANGELOG.md CHANGED
@@ -5,6 +5,101 @@ 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.1] - 2026-07-27
9
+
10
+ ### Removed
11
+ - **The `listen`-based file watcher, and with it `config.watch_files`.**
12
+ 0.10.0 enabled a workspace watcher by default. On Linux each watched
13
+ directory costs an inotify watch from `fs.inotify.max_user_watches`, a
14
+ *per-user* budget shared with everything else watching files — including the
15
+ host app's own code reloader and any other gem using `listen`. Exhausting it
16
+ raises `iNotify max watches exceeded`, and because `listen` reports some of
17
+ those failures from its own background thread, mbeditor could not even
18
+ rescue them. Claiming a share of a scarce OS resource by default was the
19
+ wrong trade for a development tool, and raising the limit needs root, which
20
+ a developer may not have.
21
+
22
+ Nothing is lost: external changes are picked up by polling, which is how the
23
+ editor already tracked git state. If you set `config.watch_files`, remove it
24
+ — it is now ignored.
25
+
26
+ ### Fixed
27
+ - **The file tree never refreshed for changes made outside the editor.** Its
28
+ 10-second poll returned early whenever the Action Cable socket was connected,
29
+ on the reasoning that the push covered it — but the server only broadcasts
30
+ from mbeditor's own mutation endpoints. With a socket connected, which is the
31
+ normal case, an external `git checkout` or generator run was never picked up.
32
+ The poll now always runs; the push remains the instant path for our own
33
+ writes. This is the bug the 0.10.0 watcher was compensating for.
34
+ - **Git line-number tinting went stale for external changes**, for the same
35
+ reason, and its refresh timer was being cleared and recreated on every
36
+ re-render so it never survived long enough to fire. It now polls on its own
37
+ timer and on window focus, matching the file tree.
38
+ - **Watched paths were dropped when the workspace was reached through a
39
+ symlink** (macOS `/var` → `/private/var`, or a symlinked checkout), because
40
+ reported paths resolve to the real path and no longer matched the configured
41
+ root.
42
+
43
+ ---
44
+
45
+ ## [0.10.0] - 2026-07-27
46
+
47
+ ### Added
48
+ - **Git-status line numbers** — line numbers are tinted by what git thinks of
49
+ the line: green for added, orange for modified, red on the line a deleted
50
+ block sat after. Backed by a new `GET /mbeditor/git/line_diff` endpoint over
51
+ `git diff -U0`, and never a reason to fail a file load: a missing HEAD or a
52
+ non-repo simply leaves the numbers plain.
53
+ - **Problems panel** — error and warning counts in the status bar open a
54
+ drawer listing every diagnostic across the open files, grouped by file with
55
+ the offending source line and click-to-navigate. Counts cover the open tabs,
56
+ which is where Monaco's markers live; RuboCop, ruby-lsp and the TypeScript
57
+ worker all feed it through one subscription.
58
+ - **Outline for JS, JSX and TypeScript** — the Methods/Outline button now
59
+ works in those files, translating the TypeScript worker's own navigation
60
+ tree rather than adding a second lexer. Arrow functions and function
61
+ expressions assigned to variables are listed; data constants, object-literal
62
+ keys and anonymous callbacks are not.
63
+ - **Colour-coded Rails log** — the log drawer distinguishes request
64
+ boundaries, controller dispatch, SQL, renders, redirects and failures, with
65
+ `Completed` lines coloured by status code.
66
+ - **Optional workspace file watching** — with the host's
67
+ [`listen`](https://github.com/guard/listen) gem, changes made outside the
68
+ editor (a terminal `git checkout`, a generator, another editor) refresh the
69
+ file tree and git decorations instead of going stale. Absent the gem,
70
+ behaviour is unchanged. Configurable via `config.watch_files`.
71
+
72
+ ### Fixed
73
+ - **ruby-lsp hover showed a dead link.** ruby-lsp renders its "Definitions"
74
+ line as VS Code `file://` links, which Monaco draws as links but nothing in
75
+ the browser can open, so clicking did nothing. In-workspace links are now
76
+ rewritten to a Monaco command that opens the file at the line; gem and
77
+ stdlib links, which the editor cannot open at all, degrade to plain code
78
+ spans. The `/module_members` breakdown is also restored beneath a constant
79
+ hover — ruby-lsp's constant hover never lists what the class or module
80
+ defines, so taking its output wholesale had dropped it.
81
+ - **False-positive type errors in plain JS/JSX.** The editor kept a denylist
82
+ of TypeScript diagnostic codes to suppress; it had grown to eight and still
83
+ leaked (TS2322 on a spread carrying an extra prop). Untyped JS gives
84
+ TypeScript nothing to check against, and which way it guesses is arbitrary —
85
+ state seeded with `useState({})` errors on every key while the same object
86
+ from `JSON.parse` stays silent. JS/JSX now keeps only the checks that are
87
+ sound without annotations: syntax errors, `Cannot find name`, and unused
88
+ locals. `.ts`/`.tsx` keeps full checking, where the types are hand-written.
89
+
90
+ ### Changed
91
+ - The **Logs** button moved from the top toolbar to the status bar.
92
+ - **Quick-open ranks recently opened files** above the static file-type tier,
93
+ so a file you were just in outranks a never-opened controller. Match quality
94
+ still comes first, so a worse match cannot jump the queue.
95
+ - The title-bar search field now fills 75% of the space between the title and
96
+ the toolbar buttons instead of a fixed 340px.
97
+ - Test-suite compatibility fixes carried over from the unreleased 0.9.1/0.9.2
98
+ work: MiniRacer-dependent parser suites skip in minimal bundles, and the
99
+ Outline system tests exercise navigation through click semantics.
100
+
101
+ ---
102
+
8
103
  ## [0.9.0] - 2026-07-23
9
104
 
10
105
  ### Added
data/README.md CHANGED
@@ -9,6 +9,10 @@ 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)
@@ -202,85 +206,6 @@ The gem keeps host/tooling responsibilities in the host app:
202
206
 
203
207
  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
208
 
205
- ### Ruby language server (Optional)
206
-
207
- Add [ruby-lsp](https://github.com/Shopify/ruby-lsp) to the host app's
208
- development group and mbeditor uses it automatically for Ruby
209
- go-to-definition, hover, completion, and diagnostics:
210
-
211
- ```ruby
212
- gem "ruby-lsp", require: false, group: :development
213
- gem "ruby-lsp-rails", require: false, group: :development # Rails-aware results
214
- ```
215
-
216
- `ruby-lsp-rails` needs no mbeditor configuration — ruby-lsp loads it as an
217
- addon, so associations, model attributes, and route helpers start resolving on
218
- their own.
219
-
220
- What changes when it's present:
221
-
222
- - **Diagnostics.** Ruby files are checked by ruby-lsp instead of booting
223
- RuboCop over HTTP on every debounce, so you also get Prism syntax errors and
224
- warnings alongside RuboCop offenses. Quick-fix lightbulbs still work for
225
- correctable cops. Very large files fall back to syntax-only diagnostics.
226
- - **Definitions, hover, completion.** Answered from the language server's index
227
- rather than a workspace grep, including your unsaved buffer contents.
228
-
229
- Everything degrades on its own: if ruby-lsp is missing, times out (its first
230
- index of a large app takes a while), or crashes, that request falls back to the
231
- built-in grep/Ripper services. ERB templates always use the built-in services —
232
- ruby-lsp cannot parse ERB.
233
-
234
- ### Realtime via Action Cable (Optional)
235
-
236
- Mbeditor works without Action Cable. If Action Cable is unavailable, unreachable, or returns transient errors, the editor automatically falls back to polling.
237
-
238
- To enable realtime features in a host app:
239
-
240
- 1. Ensure Action Cable is enabled in the host app (for apps that do not load it by default, add the framework/gem explicitly).
241
- 2. Mount cable in host routes:
242
-
243
- ```ruby
244
- mount ActionCable.server => '/cable'
245
- ```
246
-
247
- 3. Make Action Cable JavaScript available to the page (for asset-pipeline apps, `actioncable.js` is typically sufficient).
248
-
249
- If any of these are missing, mbeditor still runs in polling mode.
250
-
251
- ### Syntax Highlighting Support
252
- Monaco runtime assets are served from the engine route namespace (`/mbeditor/monaco-editor/*` and `/mbeditor/monaco_worker.js`).
253
- The gem includes syntax highlighting for common Rails and React development file types:
254
-
255
- **Web & Template Languages:**
256
- - **Ruby** (.rb, Gemfile, gemspec, Rakefile)
257
- - **HTML**
258
- - **ERB** (.html.erb, .erb) — dedicated ERB grammar, plus Ruby intellisense
259
- inside `<% %>` tags: hover, completion, go-to-definition (Ctrl/Cmd+click or
260
- F12) and auto-`end`, all inert in the surrounding HTML. ERB uses the built-in
261
- workspace services rather than ruby-lsp, which cannot parse ERB.
262
- - **HAML** (.haml) — plaintext syntax highlighting (no dedicated HAML grammar in Monaco; haml-lint provides inline error markers when available)
263
- - **CSS** and **SCSS** stylesheets
264
-
265
- **JavaScript & React:**
266
- - **JavaScript / JSX** (.js, .jsx, .js.jsx) — Monaco's TypeScript worker runs in
267
- checked-JS mode with JSX enabled. Built for the Sprockets world where every
268
- top-level `var`/`function`/`class` (and `window.X =` assignment) is a global:
269
- the editor scans the workspace once at boot (`GET /js_globals`) and declares
270
- all of them as ambient globals, so cross-file component references need no
271
- `import` and produce no "Cannot find name" diagnostics. The list refreshes
272
- automatically when files change. Runtime-only globals the static scan can't
273
- see (e.g. `Routes`, `I18n`) can be declared via
274
- `config.js_global_identifiers = %w[Routes I18n]`.
275
- Known limits: everything is typed `any` (no cross-file type inference), and
276
- genuinely undefined names are shown as warnings, not errors.
277
- - **TypeScript** (.ts, .tsx)
278
-
279
- **Configuration & Documentation:**
280
- - **YAML** (.yml, .yaml)
281
- - **Markdown** (.md)
282
-
283
- These language modules are packaged locally with the gem for true offline operation. No network fallback is needed—all highlighting works without internet connectivity.
284
209
 
285
210
  ## Asset Pipeline
286
211
 
@@ -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,10 @@ var EditorPanel = function EditorPanel(_ref) {
69
69
  var setIsBlameLoading = _useState8[1];
70
70
 
71
71
  var blameDecorationsRef = useRef([]);
72
+ var gitLineDecorationsRef = useRef([]);
73
+ // Latest git line-diff refresh, read by the poll effect so its interval does
74
+ // not have to be torn down whenever the active tab changes.
75
+ var gitLineRefreshRef = useRef(null);
72
76
  var blameZoneIdsRef = useRef([]);
73
77
  var testDecorationIdsRef = useRef([]);
74
78
  var testZoneIdsRef = useRef([]);
@@ -105,6 +109,8 @@ var EditorPanel = function EditorPanel(_ref) {
105
109
 
106
110
  var methodsBtnRef = useRef(null);
107
111
  var methodsDropdownRef = useRef(null);
112
+ // Discards a JS outline that resolves after the dropdown was closed or reopened.
113
+ var methodsRequestRef = useRef(0);
108
114
 
109
115
  // Local pagination state — initialized from tab props; updated on page navigation
110
116
  var _useState21 = useState(tab.startLine || 0);
@@ -1281,6 +1287,115 @@ var EditorPanel = function EditorPanel(_ref) {
1281
1287
  }
1282
1288
  }, [tab.path]);
1283
1289
 
1290
+ // Tint line numbers by git status: green added, orange modified, red where
1291
+ // lines were removed.
1292
+ //
1293
+ // The ranges come from `git diff -U0` against HEAD, so they describe the file
1294
+ // as last written to disk. Monaco anchors decorations to the model and shifts
1295
+ // them as you type, which keeps them roughly right mid-edit; the authoritative
1296
+ // refresh happens on the signals wired up at the end of this effect.
1297
+
1298
+ // Poll cadence for the tint, matching the file tree's. Slower to react than a
1299
+ // filesystem watcher would be, at the cost of one `git diff -U0` on one file
1300
+ // per visible pane — against a resource nothing else is competing for, rather
1301
+ // than a share of the kernel's inotify budget.
1302
+ var GIT_LINE_POLL_MS = 10000;
1303
+
1304
+ useEffect(function () {
1305
+ if (!gitAvailable || !tab.path || tab.isDiff || tab.isCombinedDiff) return;
1306
+
1307
+ var cancelled = false;
1308
+
1309
+ function clear() {
1310
+ if (!monacoRef.current || !monacoRef.current.getModel()) return;
1311
+ gitLineDecorationsRef.current =
1312
+ monacoRef.current.deltaDecorations(gitLineDecorationsRef.current, []);
1313
+ }
1314
+
1315
+ function apply(data) {
1316
+ var editor = monacoRef.current;
1317
+ var model = editor && editor.getModel();
1318
+ if (!model || !window.monaco) return;
1319
+
1320
+ var lineCount = model.getLineCount();
1321
+ var decorations = [];
1322
+
1323
+ function push(ranges, className) {
1324
+ (ranges || []).forEach(function (range) {
1325
+ // A deletion above the first line is reported as line 0; mark line 1
1326
+ // so the file's opening line carries the flag.
1327
+ var start = Math.max(1, Math.min(range.start, lineCount));
1328
+ var end = Math.max(start, Math.min(range.end, lineCount));
1329
+ for (var line = start; line <= end; line++) {
1330
+ decorations.push({
1331
+ range: new window.monaco.Range(line, 1, line, 1),
1332
+ options: { isWholeLine: true, lineNumberClassName: className }
1333
+ });
1334
+ }
1335
+ });
1336
+ }
1337
+
1338
+ push(data.added, 'mbeditor-gitline-added');
1339
+ push(data.modified, 'mbeditor-gitline-modified');
1340
+ push(data.deleted, 'mbeditor-gitline-deleted');
1341
+
1342
+ gitLineDecorationsRef.current =
1343
+ editor.deltaDecorations(gitLineDecorationsRef.current, decorations);
1344
+ }
1345
+
1346
+ function refresh() {
1347
+ var path = tab.path;
1348
+ GitService.fetchLineDiff(path).then(function (data) {
1349
+ // The tab can be switched or closed while the request is in flight.
1350
+ if (cancelled || !data || tab.path !== path) return;
1351
+ apply(data);
1352
+ }).catch(function () {
1353
+ // Not a repo, file not readable, git missing — leave the numbers plain.
1354
+ if (!cancelled) clear();
1355
+ });
1356
+ }
1357
+
1358
+ refresh();
1359
+
1360
+ // Publish the current refresh for the poll below, which lives in its own
1361
+ // effect so a re-render of this one cannot restart its clock.
1362
+ gitLineRefreshRef.current = refresh;
1363
+
1364
+ // Immediate path for writes mbeditor made itself.
1365
+ var hasSocket = typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged;
1366
+ var onChanged = hasSocket ? function () { refresh(); } : null;
1367
+ if (onChanged) WebSocketService.onFilesChanged(onChanged);
1368
+
1369
+ return function () {
1370
+ cancelled = true;
1371
+ gitLineRefreshRef.current = null;
1372
+ if (onChanged && WebSocketService.offFilesChanged) WebSocketService.offFilesChanged(onChanged);
1373
+ clear();
1374
+ };
1375
+ }, [tab.path, tab.externalContentVersion, tab.isDiff, tab.isCombinedDiff, gitAvailable]);
1376
+
1377
+ // The poll for changes made outside the editor — a terminal commit or branch
1378
+ // switch alters the diff without touching this buffer.
1379
+ //
1380
+ // Deliberately its own effect with no dependencies. Held inside the effect
1381
+ // above, the interval was cleared and recreated on every re-render of that
1382
+ // one and never survived long enough to fire, so the tint only ever updated
1383
+ // via the WebSocket — i.e. never for external changes, which is the whole
1384
+ // point of it. Reading the refresh through a ref keeps this clock running
1385
+ // across tab switches.
1386
+ useEffect(function () {
1387
+ var tick = function () {
1388
+ if (document.hidden) return;
1389
+ if (gitLineRefreshRef.current) gitLineRefreshRef.current();
1390
+ };
1391
+ var intervalId = setInterval(tick, GIT_LINE_POLL_MS);
1392
+ window.addEventListener('focus', tick);
1393
+ return function () {
1394
+ clearInterval(intervalId);
1395
+ window.removeEventListener('focus', tick);
1396
+ };
1397
+ }, []);
1398
+
1284
1399
  // Handle Blame data fetching
1285
1400
  useEffect(function () {
1286
1401
  if (!isBlameVisible) {
@@ -1612,6 +1727,8 @@ var EditorPanel = function EditorPanel(_ref) {
1612
1727
  var fileBaseName = (tab.path || '').split('/').pop().toLowerCase();
1613
1728
  var isRubyFile = ext === 'rb' || ext === 'ruby' || ext === 'gemspec' || ext === 'rake' ||
1614
1729
  fileBaseName === 'gemfile' || fileBaseName === 'gemfile.lock' || fileBaseName === 'rakefile';
1730
+ var isJsFile = ['js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx'].indexOf(ext) !== -1;
1731
+ var hasOutline = isRubyFile || isJsFile;
1615
1732
  var isTestOutline = false;
1616
1733
  try {
1617
1734
  isTestOutline = isRubyFile && window.RubyOutline &&
@@ -1695,6 +1812,33 @@ var EditorPanel = function EditorPanel(_ref) {
1695
1812
  return window.RubyOutline.parse(lines, { path: path });
1696
1813
  }
1697
1814
 
1815
+ // The JS/TS outline comes from the TypeScript worker rather than a lexer of
1816
+ // our own, which makes it async — the worker is where the parse already
1817
+ // lives, and it tracks the live buffer, so unsaved edits are included.
1818
+ function loadJsOutline(model) {
1819
+ var ts = window.monaco && window.monaco.languages && window.monaco.languages.typescript;
1820
+ if (!ts || !window.JsOutline) return Promise.reject(new Error('outline unavailable'));
1821
+
1822
+ var workerFor = model.getLanguageId() === 'typescript' ? ts.getTypeScriptWorker : ts.getJavaScriptWorker;
1823
+ return workerFor().then(function (getWorker) {
1824
+ return getWorker(model.uri);
1825
+ }).then(function (client) {
1826
+ return client.getNavigationTree(model.uri.toString());
1827
+ }).then(function (tree) {
1828
+ return window.JsOutline.fromNavigationTree(tree, {
1829
+ lineAt: function (offset) { return model.getPositionAt(offset).lineNumber; },
1830
+ textAt: function (offset, length) {
1831
+ var start = model.getPositionAt(offset);
1832
+ var end = model.getPositionAt(offset + length);
1833
+ return model.getValueInRange({
1834
+ startLineNumber: start.lineNumber, startColumn: start.column,
1835
+ endLineNumber: end.lineNumber, endColumn: end.column
1836
+ });
1837
+ }
1838
+ });
1839
+ });
1840
+ }
1841
+
1698
1842
  if (tab.fileNotFound) {
1699
1843
  return React.createElement(
1700
1844
  'div',
@@ -1834,31 +1978,55 @@ var EditorPanel = function EditorPanel(_ref) {
1834
1978
  React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
1835
1979
  !editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
1836
1980
  ),
1837
- isRubyFile && React.createElement(
1981
+ hasOutline && React.createElement(
1838
1982
  'button',
1839
1983
  {
1840
1984
  ref: methodsBtnRef,
1841
1985
  className: 'ide-icon-btn' + (methodsOpen ? ' active' : ''),
1842
1986
  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
- }
1987
+ // Bump first: a click that closes the dropdown must also cancel an
1988
+ // outline request still in flight from the click that opened it.
1989
+ var requestId = ++methodsRequestRef.current;
1990
+ if (methodsOpen) {
1991
+ setMethodsOpen(false);
1992
+ return;
1993
+ }
1994
+
1995
+ var model = monacoRef.current && monacoRef.current.getModel();
1996
+
1997
+ function openWith(entries, truncated, unavailable) {
1998
+ setMethodsList(entries);
1999
+ setMethodsTruncated(truncated);
2000
+ setMethodsUnavailable(unavailable);
1856
2001
  if (methodsBtnRef.current) {
1857
2002
  var rect = methodsBtnRef.current.getBoundingClientRect();
1858
2003
  setMethodsDropdownPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
1859
2004
  }
2005
+ setMethodsOpen(true);
2006
+ }
2007
+
2008
+ if (isJsFile) {
2009
+ // The worker has already parsed the open buffer for diagnostics,
2010
+ // so this resolves in a few ms — opening only once it answers
2011
+ // avoids flashing "No methods found" on the way.
2012
+ (model ? loadJsOutline(model) : Promise.resolve({ entries: [], truncated: false }))
2013
+ .then(function (result) {
2014
+ if (methodsRequestRef.current !== requestId) return;
2015
+ openWith(result.entries || [], !!result.truncated, false);
2016
+ })
2017
+ .catch(function () {
2018
+ if (methodsRequestRef.current !== requestId) return;
2019
+ openWith([], false, true);
2020
+ });
2021
+ return;
2022
+ }
2023
+
2024
+ try {
2025
+ var result = model ? parseRubyOutline(model, tab.path) : { entries: [], truncated: false };
2026
+ openWith(result.entries || [], !!result.truncated, false);
2027
+ } catch (err) {
2028
+ openWith([], false, true);
1860
2029
  }
1861
- setMethodsOpen(nextOpen);
1862
2030
  },
1863
2031
  title: isTestOutline ? 'Jump to Outline' : 'Jump to Method'
1864
2032
  },
@@ -2099,7 +2267,9 @@ var EditorPanel = function EditorPanel(_ref) {
2099
2267
  var row = React.createElement(
2100
2268
  'button',
2101
2269
  {
2102
- key: entry.kind + '-' + entry.line,
2270
+ // Index-qualified: a JS class and its first member can share
2271
+ // a line, and kind+line alone would collide.
2272
+ key: entry.kind + '-' + entry.line + '-' + entryIndex,
2103
2273
  type: 'button',
2104
2274
  className: 'ide-methods-dropdown-item ide-outline-entry ide-outline-entry-' + entry.kind,
2105
2275
  '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;