mbeditor 0.8.1 → 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 +85 -0
- data/README.md +31 -0
- data/app/assets/javascripts/mbeditor/application.js +3 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +281 -60
- 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 +112 -52
- data/app/assets/javascripts/mbeditor/git_service.js +8 -0
- data/app/assets/javascripts/mbeditor/js_outline.js +110 -0
- data/app/assets/javascripts/mbeditor/ruby_outline.js +427 -0
- data/app/assets/stylesheets/mbeditor/editor.css +224 -5
- data/app/assets/stylesheets/mbeditor/themes.css +35 -0
- data/app/controllers/mbeditor/application_controller.rb +5 -11
- data/app/controllers/mbeditor/editors_controller.rb +34 -5
- data/app/controllers/mbeditor/git_controller.rb +11 -0
- data/app/services/mbeditor/exclusion_matcher.rb +105 -3
- data/app/services/mbeditor/file_tree_service.rb +1 -1
- data/app/services/mbeditor/git_line_diff_service.rb +99 -0
- data/app/services/mbeditor/git_service.rb +3 -10
- data/app/services/mbeditor/ruby_definition_service.rb +1 -1
- data/app/services/mbeditor/safe_path.rb +57 -0
- data/app/services/mbeditor/search_replace_service.rb +2 -2
- 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 +8 -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,91 @@ 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
|
+
|
|
66
|
+
## [0.9.0] - 2026-07-23
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
- **Test-aware Ruby Outline** — the editor now recognises Minitest and RSpec
|
|
70
|
+
suites, tests, and helper methods in `_test.rb` and `_spec.rb` files. It
|
|
71
|
+
reads the unsaved Monaco buffer, preserves nested suite depth and method
|
|
72
|
+
visibility, supports keyboard navigation, and safely reports truncation or
|
|
73
|
+
parser failures without disrupting the editor.
|
|
74
|
+
- **Ruby test-DSL highlighting** — Minitest/RSpec declarations, hooks, and
|
|
75
|
+
helpers now receive structural Monaco tokens while Ruby files retain the
|
|
76
|
+
existing `ruby` language identity and integrations.
|
|
77
|
+
|
|
78
|
+
### Security
|
|
79
|
+
- **Dangling symlink containment** — every editor and Git path check now
|
|
80
|
+
rejects dangling symlinks that resolve outside the workspace or repository,
|
|
81
|
+
closing a write-through escape for create, save, rename, and directory
|
|
82
|
+
operations.
|
|
83
|
+
- **Filesystem-aware exclusions** — exclusion matching now normalizes Unicode
|
|
84
|
+
spellings and folds case only on case-insensitive filesystems, preventing
|
|
85
|
+
case or normalization variants from reaching excluded directories such as
|
|
86
|
+
`.git`.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- Generated `graphify-out/` artifacts are ignored by Git.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
8
93
|
## [0.8.1] - 2026-07-22
|
|
9
94
|
|
|
10
95
|
### Fixed
|
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
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
//= require mbeditor/tab_manager
|
|
12
12
|
//= require mbeditor/color_provider
|
|
13
13
|
//= require mbeditor/editor_plugins
|
|
14
|
+
//= require mbeditor/ruby_outline
|
|
15
|
+
//= require mbeditor/js_outline
|
|
14
16
|
//= require mbeditor/components/CollapsibleSection
|
|
15
17
|
//= require mbeditor/components/ShortcutHelp
|
|
16
18
|
//= require mbeditor/components/DiffViewer
|
|
@@ -20,6 +22,7 @@
|
|
|
20
22
|
//= require mbeditor/components/FileHistoryPanel
|
|
21
23
|
//= require mbeditor/components/TestResultsPanel
|
|
22
24
|
//= require mbeditor/components/LogPanel
|
|
25
|
+
//= require mbeditor/components/ProblemsPanel
|
|
23
26
|
//= require mbeditor/components/CodeReviewPanel
|
|
24
27
|
//= require mbeditor/components/EditorPanel
|
|
25
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([]);
|
|
@@ -88,33 +89,46 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
88
89
|
var methodsList = _useState14[0];
|
|
89
90
|
var setMethodsList = _useState14[1];
|
|
90
91
|
|
|
91
|
-
var _useState15 = useState(
|
|
92
|
+
var _useState15 = useState(false);
|
|
92
93
|
var _useState16 = _slicedToArray(_useState15, 2);
|
|
93
|
-
var
|
|
94
|
-
var
|
|
94
|
+
var methodsTruncated = _useState16[0];
|
|
95
|
+
var setMethodsTruncated = _useState16[1];
|
|
95
96
|
|
|
96
|
-
var
|
|
97
|
-
|
|
98
|
-
// Local pagination state — initialized from tab props; updated on page navigation
|
|
99
|
-
var _useState17 = useState(tab.startLine || 0);
|
|
97
|
+
var _useState17 = useState(false);
|
|
100
98
|
var _useState18 = _slicedToArray(_useState17, 2);
|
|
101
|
-
var
|
|
102
|
-
var
|
|
99
|
+
var methodsUnavailable = _useState18[0];
|
|
100
|
+
var setMethodsUnavailable = _useState18[1];
|
|
103
101
|
|
|
104
|
-
var _useState19 = useState(
|
|
102
|
+
var _useState19 = useState(null);
|
|
105
103
|
var _useState20 = _slicedToArray(_useState19, 2);
|
|
106
|
-
var
|
|
107
|
-
var
|
|
104
|
+
var methodsDropdownPos = _useState20[0];
|
|
105
|
+
var setMethodsDropdownPos = _useState20[1];
|
|
106
|
+
|
|
107
|
+
var methodsBtnRef = useRef(null);
|
|
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
|
|
113
|
+
var _useState21 = useState(tab.startLine || 0);
|
|
110
114
|
var _useState22 = _slicedToArray(_useState21, 2);
|
|
111
|
-
var
|
|
112
|
-
var
|
|
115
|
+
var pageStartLine = _useState22[0];
|
|
116
|
+
var setPageStartLine = _useState22[1];
|
|
113
117
|
|
|
114
|
-
var _useState23 = useState(tab.
|
|
118
|
+
var _useState23 = useState(tab.lineCount || 0);
|
|
115
119
|
var _useState24 = _slicedToArray(_useState23, 2);
|
|
116
|
-
var
|
|
117
|
-
var
|
|
120
|
+
var pageLineCount = _useState24[0];
|
|
121
|
+
var setPageLineCount = _useState24[1];
|
|
122
|
+
|
|
123
|
+
var _useState25 = useState(tab.totalLines || 0);
|
|
124
|
+
var _useState26 = _slicedToArray(_useState25, 2);
|
|
125
|
+
var pageTotalLines = _useState26[0];
|
|
126
|
+
var setPageTotalLines = _useState26[1];
|
|
127
|
+
|
|
128
|
+
var _useState27 = useState(tab.totalBytes || 0);
|
|
129
|
+
var _useState28 = _slicedToArray(_useState27, 2);
|
|
130
|
+
var pageTotalBytes = _useState28[0];
|
|
131
|
+
var setPageTotalBytes = _useState28[1];
|
|
118
132
|
|
|
119
133
|
var onFormatRef = useRef(onFormat);
|
|
120
134
|
onFormatRef.current = onFormat;
|
|
@@ -1270,6 +1284,80 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1270
1284
|
}
|
|
1271
1285
|
}, [tab.path]);
|
|
1272
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
|
+
|
|
1273
1361
|
// Handle Blame data fetching
|
|
1274
1362
|
useEffect(function () {
|
|
1275
1363
|
if (!isBlameVisible) {
|
|
@@ -1601,6 +1689,15 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1601
1689
|
var fileBaseName = (tab.path || '').split('/').pop().toLowerCase();
|
|
1602
1690
|
var isRubyFile = ext === 'rb' || ext === 'ruby' || ext === 'gemspec' || ext === 'rake' ||
|
|
1603
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;
|
|
1694
|
+
var isTestOutline = false;
|
|
1695
|
+
try {
|
|
1696
|
+
isTestOutline = isRubyFile && window.RubyOutline &&
|
|
1697
|
+
typeof window.RubyOutline.isTestPath === 'function' && window.RubyOutline.isTestPath(tab.path);
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
isTestOutline = false;
|
|
1700
|
+
}
|
|
1604
1701
|
|
|
1605
1702
|
useEffect(function () {
|
|
1606
1703
|
if (isMarkdown && window.marked) {
|
|
@@ -1638,8 +1735,8 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1638
1735
|
if (!methodsOpen) return;
|
|
1639
1736
|
function handleClickOutside(e) {
|
|
1640
1737
|
var btn = methodsBtnRef.current;
|
|
1641
|
-
|
|
1642
|
-
if (btn && !btn.contains(e.target)) {
|
|
1738
|
+
var dropdown = methodsDropdownRef.current;
|
|
1739
|
+
if (btn && !btn.contains(e.target) && (!dropdown || !dropdown.contains(e.target))) {
|
|
1643
1740
|
setMethodsOpen(false);
|
|
1644
1741
|
}
|
|
1645
1742
|
}
|
|
@@ -1667,20 +1764,41 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1667
1764
|
});
|
|
1668
1765
|
}
|
|
1669
1766
|
|
|
1670
|
-
// Parse
|
|
1671
|
-
function
|
|
1672
|
-
var methods = [];
|
|
1767
|
+
// Parse the current Monaco buffer so unsaved Ruby test declarations appear too.
|
|
1768
|
+
function parseRubyOutline(model, path) {
|
|
1673
1769
|
var lineCount = model.getLineCount();
|
|
1674
|
-
var
|
|
1770
|
+
var lines = [];
|
|
1675
1771
|
for (var i = 1; i <= lineCount; i++) {
|
|
1676
|
-
|
|
1677
|
-
var m = DEF_RE.exec(line);
|
|
1678
|
-
if (m) {
|
|
1679
|
-
var selfPrefix = m[1] ? 'self.' : '';
|
|
1680
|
-
methods.push({ line: i, name: selfPrefix + m[2] });
|
|
1681
|
-
}
|
|
1772
|
+
lines.push(model.getLineContent(i));
|
|
1682
1773
|
}
|
|
1683
|
-
return
|
|
1774
|
+
return window.RubyOutline.parse(lines, { path: path });
|
|
1775
|
+
}
|
|
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
|
+
});
|
|
1684
1802
|
}
|
|
1685
1803
|
|
|
1686
1804
|
if (tab.fileNotFound) {
|
|
@@ -1822,27 +1940,60 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
1822
1940
|
React.createElement('i', { className: 'fas fa-history', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
1823
1941
|
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'History')
|
|
1824
1942
|
),
|
|
1825
|
-
|
|
1943
|
+
hasOutline && React.createElement(
|
|
1826
1944
|
'button',
|
|
1827
1945
|
{
|
|
1828
1946
|
ref: methodsBtnRef,
|
|
1829
1947
|
className: 'ide-icon-btn' + (methodsOpen ? ' active' : ''),
|
|
1830
1948
|
onClick: function() {
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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);
|
|
1835
1963
|
if (methodsBtnRef.current) {
|
|
1836
1964
|
var rect = methodsBtnRef.current.getBoundingClientRect();
|
|
1837
1965
|
setMethodsDropdownPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
|
1838
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);
|
|
1839
1991
|
}
|
|
1840
|
-
setMethodsOpen(nextOpen);
|
|
1841
1992
|
},
|
|
1842
|
-
title: 'Jump to Method'
|
|
1993
|
+
title: isTestOutline ? 'Jump to Outline' : 'Jump to Method'
|
|
1843
1994
|
},
|
|
1844
1995
|
React.createElement('i', { className: 'fas fa-list-ul', style: { marginRight: editorPrefs.toolbarIconOnly ? 0 : '5px', flexShrink: 0 } }),
|
|
1845
|
-
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, 'Methods')
|
|
1996
|
+
!editorPrefs.toolbarIconOnly && React.createElement('span', { className: 'ide-toolbar-label' }, isTestOutline ? 'Outline' : 'Methods')
|
|
1846
1997
|
),
|
|
1847
1998
|
gitAvailable && React.createElement(
|
|
1848
1999
|
'button',
|
|
@@ -2029,31 +2180,101 @@ var EditorPanel = function EditorPanel(_ref) {
|
|
|
2029
2180
|
methodsOpen && methodsDropdownPos && React.createElement(
|
|
2030
2181
|
'div',
|
|
2031
2182
|
{
|
|
2183
|
+
ref: methodsDropdownRef,
|
|
2032
2184
|
className: 'ide-methods-dropdown',
|
|
2033
2185
|
style: { position: 'fixed', top: methodsDropdownPos.top + 'px', right: methodsDropdownPos.right + 'px', left: 'auto', zIndex: 9900 }
|
|
2034
2186
|
},
|
|
2035
|
-
|
|
2036
|
-
? React.createElement('div', { className: 'ide-methods-dropdown-
|
|
2037
|
-
: methodsList.
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2187
|
+
methodsUnavailable
|
|
2188
|
+
? React.createElement('div', { className: 'ide-methods-dropdown-message' }, 'Outline unavailable')
|
|
2189
|
+
: methodsList.length === 0
|
|
2190
|
+
? React.createElement('div', { className: 'ide-methods-dropdown-empty' }, isTestOutline ? 'No outline entries found' : 'No methods found')
|
|
2191
|
+
: (function() {
|
|
2192
|
+
var rows = [];
|
|
2193
|
+
var visibility = null;
|
|
2194
|
+
var visibilityStartLine = null;
|
|
2195
|
+
var visibilityRows = [];
|
|
2196
|
+
var icons = { method: 'fa-code', suite: 'fa-layer-group', test: 'fa-flask' };
|
|
2197
|
+
|
|
2198
|
+
function activateEntry(entry) {
|
|
2199
|
+
setMethodsOpen(false);
|
|
2200
|
+
if (monacoRef.current) {
|
|
2201
|
+
monacoRef.current.revealLineInCenter(entry.line);
|
|
2202
|
+
monacoRef.current.setPosition({ lineNumber: entry.line, column: 1 });
|
|
2203
|
+
monacoRef.current.focus();
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function flushVisibilityRows() {
|
|
2208
|
+
if (visibilityRows.length === 0) return;
|
|
2209
|
+
rows.push(React.createElement(
|
|
2210
|
+
'div',
|
|
2211
|
+
{
|
|
2212
|
+
key: 'visibility-group-' + visibilityStartLine,
|
|
2213
|
+
className: 'ide-methods-dropdown-visibility-group'
|
|
2214
|
+
},
|
|
2215
|
+
React.createElement(
|
|
2216
|
+
'div',
|
|
2217
|
+
{ className: 'ide-methods-dropdown-visibility' },
|
|
2218
|
+
visibility
|
|
2219
|
+
),
|
|
2220
|
+
visibilityRows
|
|
2221
|
+
));
|
|
2222
|
+
visibility = null;
|
|
2223
|
+
visibilityStartLine = null;
|
|
2224
|
+
visibilityRows = [];
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
methodsList.forEach(function(entry, entryIndex) {
|
|
2228
|
+
var depth = typeof entry.depth === 'number' ? entry.depth : 0;
|
|
2229
|
+
var row = React.createElement(
|
|
2230
|
+
'button',
|
|
2231
|
+
{
|
|
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,
|
|
2235
|
+
type: 'button',
|
|
2236
|
+
className: 'ide-methods-dropdown-item ide-outline-entry ide-outline-entry-' + entry.kind,
|
|
2237
|
+
'data-outline-kind': entry.kind,
|
|
2238
|
+
'data-outline-depth': depth,
|
|
2239
|
+
'aria-label': entry.name + ', line ' + entry.line,
|
|
2240
|
+
tabIndex: 0,
|
|
2241
|
+
autoFocus: entryIndex === 0,
|
|
2242
|
+
style: { paddingLeft: (12 + depth * 14) + 'px' },
|
|
2243
|
+
onClick: function() {
|
|
2244
|
+
activateEntry(entry);
|
|
2245
|
+
}
|
|
2246
|
+
},
|
|
2247
|
+
React.createElement('span', { className: 'ide-methods-dropdown-line' }, entry.line),
|
|
2248
|
+
React.createElement('i', {
|
|
2249
|
+
className: 'fas ' + (icons[entry.kind] || icons.method) + ' ide-outline-entry-icon',
|
|
2250
|
+
'aria-hidden': 'true'
|
|
2251
|
+
}),
|
|
2252
|
+
React.createElement('span', { className: 'ide-outline-entry-name' }, entry.name)
|
|
2253
|
+
);
|
|
2254
|
+
|
|
2255
|
+
if (entry.kind === 'method' && entry.visibility) {
|
|
2256
|
+
if (visibility !== entry.visibility) {
|
|
2257
|
+
flushVisibilityRows();
|
|
2258
|
+
visibility = entry.visibility;
|
|
2259
|
+
visibilityStartLine = entry.line;
|
|
2050
2260
|
}
|
|
2261
|
+
visibilityRows.push(row);
|
|
2262
|
+
} else {
|
|
2263
|
+
flushVisibilityRows();
|
|
2264
|
+
rows.push(row);
|
|
2051
2265
|
}
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2266
|
+
});
|
|
2267
|
+
flushVisibilityRows();
|
|
2268
|
+
|
|
2269
|
+
if (methodsTruncated) {
|
|
2270
|
+
rows.push(React.createElement(
|
|
2271
|
+
'div',
|
|
2272
|
+
{ key: 'truncated', className: 'ide-methods-dropdown-message' },
|
|
2273
|
+
'Results truncated at 5,000 entries'
|
|
2274
|
+
));
|
|
2275
|
+
}
|
|
2276
|
+
return rows;
|
|
2277
|
+
})()
|
|
2057
2278
|
),
|
|
2058
2279
|
React.createElement('div', { ref: vimStatusRef, className: 'vim-statusbar', style: { display: editorPrefs.vimMode ? 'flex' : 'none', height: '22px', alignItems: 'center', padding: '0 10px', fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", fontSize: '12px', background: 'var(--ide-statusbar-bg, #1e1e2e)', color: 'var(--ide-statusbar-fg, #9cdcfe)', borderTop: '1px solid var(--ide-border, #3e3e3e)', flexShrink: 0, userSelect: 'none', letterSpacing: '0.02em' } })
|
|
2059
2280
|
);
|
|
@@ -2073,4 +2294,4 @@ window.EditorPanel = React.memo(EditorPanel, function(prev, next) {
|
|
|
2073
2294
|
prev.testInlineVisible === next.testInlineVisible &&
|
|
2074
2295
|
prev.editorPrefs === next.editorPrefs &&
|
|
2075
2296
|
prev.monacoReady === next.monacoReady;
|
|
2076
|
-
});
|
|
2297
|
+
});
|