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 +4 -4
- data/CHANGELOG.md +58 -0
- data/README.md +31 -0
- data/app/assets/javascripts/mbeditor/application.js +2 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +148 -16
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +50 -1
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +91 -16
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +217 -0
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +34 -3
- data/app/assets/javascripts/mbeditor/editor_plugins.js +106 -52
- data/app/assets/javascripts/mbeditor/git_service.js +8 -0
- data/app/assets/javascripts/mbeditor/js_outline.js +110 -0
- data/app/assets/stylesheets/mbeditor/editor.css +176 -4
- data/app/assets/stylesheets/mbeditor/themes.css +35 -0
- data/app/controllers/mbeditor/editors_controller.rb +33 -4
- data/app/controllers/mbeditor/git_controller.rb +11 -0
- data/app/services/mbeditor/git_line_diff_service.rb +99 -0
- data/lib/mbeditor/configuration.rb +2 -1
- data/lib/mbeditor/engine.rb +3 -0
- data/lib/mbeditor/file_watcher.rb +136 -0
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fe258515a6f0945da8bfcbfbe85197262af224c054924367597d97d370530e2
|
|
4
|
+
data.tar.gz: d3f65b3c351fef56291170d9401103d9b73d4b3a7ebad24885713b8deef5df85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
3387
|
-
{
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
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
|
{
|