mbeditor 0.10.1 → 0.11.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 +60 -0
- data/README.md +73 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +37 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +144 -29
- data/app/assets/javascripts/mbeditor/file_service.js +16 -0
- data/app/controllers/mbeditor/editors_controller.rb +16 -0
- data/app/services/mbeditor/js_globals_service.rb +31 -2
- data/app/services/mbeditor/js_program_service.rb +173 -0
- data/lib/mbeditor/configuration.rb +10 -0
- data/lib/mbeditor/route_map.rb +1 -0
- data/lib/mbeditor/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a664c7e61d20ca986be983f370ade96b639796633533373068221f45ffdbf9e
|
|
4
|
+
data.tar.gz: d9d6f92fa563330c47bc94f080432fa1e38aeddd4449b5b37f473d0555f513c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c5ed1f9375cb92b32b74fb86f76b0f98a10646e6498407b89be7b4a07f4ab4105d0bb207ce40746b33f3f34a9eaaf4ee3de16f1816a378d16b0d23f70adac04
|
|
7
|
+
data.tar.gz: 2813e2df0341112f437241f7c27537ad03f5e66da90fa756fba95647855ac6d0885421a219a0baf392e9aedcaa5c6c86e9fd4bcffacae5337ca4a8ee899e8ecb
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,66 @@ 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.11.0] - 2026-07-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Real types for your own JavaScript, from your own JavaScript.** The
|
|
12
|
+
workspace's JS source is now loaded into Monaco's TypeScript program instead
|
|
13
|
+
of being grepped for names and declared as ambient `any`. Under Sprockets a
|
|
14
|
+
JS file with no `import`/`export` is a TypeScript *script*, so its top-level
|
|
15
|
+
declarations land in the global scope — which is exactly the Sprockets model.
|
|
16
|
+
Cross-file references now get inferred signatures, member completion, and
|
|
17
|
+
argument-count checking, and genuine unknowns still report `Cannot find
|
|
18
|
+
name`:
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
var c = <Card title="x" />; // Card: (props: any) => JSX.Element
|
|
22
|
+
var s = formatCents(500); // formatCents(value: any): string
|
|
23
|
+
var t = formatCents(1, 2); // Expected 0-1 arguments, but got 2
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Two new options: `config.js_program` (default `true`) and
|
|
27
|
+
`config.js_program_exclude` (default `%w[vendor]`, added on top of
|
|
28
|
+
`excluded_paths`). Measured at ~93 ms/MB to build and ~30 ms per file
|
|
29
|
+
afterwards, so a ~10 MB tree costs under a second, once; only changed files
|
|
30
|
+
are re-sent after that.
|
|
31
|
+
|
|
32
|
+
Ambient declarations are still used for what a program cannot express.
|
|
33
|
+
TypeScript only sees *lexical* declarations: `window.Foo = ...` is not a
|
|
34
|
+
declaration to it, and UMD-wrapped libraries assign their global inside a
|
|
35
|
+
closure — `factory(global.React = {})` — which it cannot follow statically.
|
|
36
|
+
Their source contributes nothing, which is why vendored code is excluded by
|
|
37
|
+
default and React stays typed by a bundled stub. Point
|
|
38
|
+
`js_program_exclude` at any other third-party or generated JS.
|
|
39
|
+
- **A whitespace toggle in the status bar** (¶), showing tabs, spaces and
|
|
40
|
+
hidden characters in the active editor.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- **The editor became very slow on JSX files with many unresolved names.**
|
|
44
|
+
Opening such a file fired one `/js_definition` request per unknown symbol,
|
|
45
|
+
in parallel — each spawning an `rg` process — and called `addExtraLib` once
|
|
46
|
+
per resolution, re-validating every open model each time. A file with a
|
|
47
|
+
thousand warnings meant a thousand greps saturating the dev server and a
|
|
48
|
+
thousand full TypeScript re-validations. That starved the file-tree poll,
|
|
49
|
+
git status, and saves behind it. Lookups are now serialized and capped, and
|
|
50
|
+
the declaration updates are coalesced into a single flush.
|
|
51
|
+
- **Minified bundles crowded out the workspace's real globals.** A minified
|
|
52
|
+
file is one enormous line that usually opens with `var a,b,c,…` running to
|
|
53
|
+
thousands of declarators; split on commas, that single line exhausted the
|
|
54
|
+
3000-symbol cap before the scan reached your own components, so every
|
|
55
|
+
reference to them showed "Cannot find name". Declaring `a`/`n`/`t` as
|
|
56
|
+
ambient `any` also silenced real diagnostics for those names everywhere.
|
|
57
|
+
Minified files are now skipped by filename and by shape, and the endpoint
|
|
58
|
+
reports `truncated` so a workspace that outgrows the cap is diagnosable
|
|
59
|
+
instead of silently incomplete.
|
|
60
|
+
- **"File was edited externally" appeared for files nothing had touched.** The
|
|
61
|
+
check compared the file on disk against the editor buffer — which differ for
|
|
62
|
+
every unsaved tab by definition — so saving one file broadcast a change that
|
|
63
|
+
flagged every *other* dirty tab. It now compares disk against the last disk
|
|
64
|
+
content seen, so only a real on-disk change raises the banner.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
8
68
|
## [0.10.1] - 2026-07-27
|
|
9
69
|
|
|
10
70
|
### Removed
|
data/README.md
CHANGED
|
@@ -77,6 +77,10 @@ Mbeditor.configure do |config|
|
|
|
77
77
|
# config.ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
|
|
78
78
|
# config.related_files_custom_paths = %w[app/assets/javascripts/app app/policies]
|
|
79
79
|
|
|
80
|
+
# JavaScript intelligence (see the "JavaScript intelligence" section below)
|
|
81
|
+
# config.js_program = false # disable the source program entirely
|
|
82
|
+
# config.js_program_exclude = %w[vendor app/assets/javascripts/react] # third-party/generated JS
|
|
83
|
+
|
|
80
84
|
# Resilient routing (see the "Resilient Routing" section below)
|
|
81
85
|
# config.mount_path = "/mbeditor" # explicit prefix override; auto-detected when nil
|
|
82
86
|
# config.resilient_routing = false # escape hatch; true keeps the editor up when host routes break
|
|
@@ -95,6 +99,8 @@ end
|
|
|
95
99
|
| `search_timeout` | `15` | Wall-clock bound on project-search subprocesses; a tripped deadline returns the partial results collected so far. `nil` disables. |
|
|
96
100
|
| `search_respect_gitignore` | `false` | When `true`, project search and definition lookups skip files ignored by `.gitignore`. The default searches them, matching the editor's "show me everything on disk" behaviour. |
|
|
97
101
|
| `js_global_identifiers` | `[]` | Extra JS names declared as ambient globals in the editor — for runtime-only globals the static workspace scan can't see (e.g. `%w[Routes I18n]`). |
|
|
102
|
+
| `js_program` | `true` | Load the workspace's own JS source into Monaco's TypeScript program, so cross-file references get real inferred types instead of ambient `any`. See [JavaScript intelligence](#javascript-intelligence). `false` falls back to ambient declarations alone. |
|
|
103
|
+
| `js_program_exclude` | `%w[vendor]` | Directories excluded from that program, on top of `excluded_paths`. Point this at any third-party or generated JS — vendored libraries are UMD-wrapped, so their source costs parse time and contributes no globals. |
|
|
98
104
|
| `js_syntax_check` | `:auto` | Save-time babel parse check for JS/JSX using the host's `mini_racer` + babel-standalone (auto-detected; no-op when either is absent). `false` disables. |
|
|
99
105
|
| `babel_standalone_path` | `nil` | Explicit path to the babel-standalone bundle for the syntax check; `nil` looks up `babel.min.js`/`babel.js` in the host's asset pipeline. |
|
|
100
106
|
| `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. |
|
|
@@ -141,6 +147,73 @@ See [Resilient Routing](#resilient-routing) for details.
|
|
|
141
147
|
| `mount_path` | `nil` | Explicit URL prefix to serve resilient routing from. When `nil`, auto-detected from your `mount Mbeditor::Engine, at: "..."` line on every healthy boot. Set only to override detection. |
|
|
142
148
|
| `resilient_routing` | `true` | Keeps mbeditor reachable when the host's `config/routes.rb` is broken, by serving its traffic from middleware that dispatches to a private route set. Set to `false` as an escape hatch: no middleware is inserted and the private set is never built. |
|
|
143
149
|
|
|
150
|
+
## JavaScript intelligence
|
|
151
|
+
|
|
152
|
+
Under Sprockets every JS file shares one global scope, with no imports. The
|
|
153
|
+
editor models that in two layers.
|
|
154
|
+
|
|
155
|
+
**1. The source program.** Your workspace's own JS is loaded into Monaco's
|
|
156
|
+
TypeScript program. A JS file with no `import`/`export` is a TypeScript
|
|
157
|
+
*script*, so its top-level declarations land in the global scope — which is
|
|
158
|
+
exactly the Sprockets model. You get real inferred types across files:
|
|
159
|
+
|
|
160
|
+
```jsx
|
|
161
|
+
// app/assets/javascripts/ux/Card.jsx
|
|
162
|
+
var Card = function (props) { return <div>{props.title}</div>; };
|
|
163
|
+
function formatCents(value) { return "$" + (value / 100).toFixed(2); }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```jsx
|
|
167
|
+
// somewhere else — no import needed
|
|
168
|
+
var c = <Card title="x" />; // Card: (props: any) => JSX.Element
|
|
169
|
+
var s = formatCents(500); // formatCents(value: any): string
|
|
170
|
+
var t = formatCents(1, 2); // Expected 0-1 arguments, but got 2
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Unknown names still report `Cannot find name` — this adds type information, it
|
|
174
|
+
doesn't silence errors.
|
|
175
|
+
|
|
176
|
+
**2. Ambient declarations**, for names the program can't supply.
|
|
177
|
+
|
|
178
|
+
Both layers are needed, because TypeScript only sees *lexical* declarations:
|
|
179
|
+
|
|
180
|
+
- `window.Foo = ...` is a runtime global TypeScript does not treat as a
|
|
181
|
+
declaration at all.
|
|
182
|
+
- UMD-wrapped libraries — React, lodash, axios — assign their global inside a
|
|
183
|
+
closure, `factory(global.React = {})`, which TypeScript cannot follow
|
|
184
|
+
statically. **Loading their source gets you nothing**, which is why
|
|
185
|
+
`js_program_exclude` defaults to `vendor` and why React is typed by a
|
|
186
|
+
bundled stub instead.
|
|
187
|
+
|
|
188
|
+
So point `js_program_exclude` at directories of third-party or generated JS,
|
|
189
|
+
and leave your own application code in:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
config.js_program_exclude = %w[vendor app/assets/javascripts/react]
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Cost
|
|
196
|
+
|
|
197
|
+
Measured against the Monaco TypeScript worker:
|
|
198
|
+
|
|
199
|
+
| program size | build | per file opened after |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| 1 MB | 210 ms | 9 ms |
|
|
202
|
+
| 3 MB | 378 ms | 11 ms |
|
|
203
|
+
| 5.5 MB | 443 ms | 23 ms |
|
|
204
|
+
| 9.4 MB | 872 ms | 32 ms |
|
|
205
|
+
|
|
206
|
+
Roughly 93 ms/MB, paid once per session. JS gzips about 4.5:1, so a 10 MB tree
|
|
207
|
+
is ~2.2 MB over the wire — worth knowing if your app runs on a remote host.
|
|
208
|
+
After the initial load only changed files are re-sent, never the whole tree.
|
|
209
|
+
|
|
210
|
+
Nothing is truncated silently: the browser console logs the file count, total
|
|
211
|
+
size, and every skipped file with a reason (minified, oversized, unreadable).
|
|
212
|
+
Minified bundles are skipped by filename and by shape, since they cost parse
|
|
213
|
+
time and declare only one-letter names inside a closure.
|
|
214
|
+
|
|
215
|
+
Set `config.js_program = false` to disable the layer entirely.
|
|
216
|
+
|
|
144
217
|
## Test Runner
|
|
145
218
|
|
|
146
219
|
The Test button appears in the editor toolbar for any `.rb` file when a `test/` or `spec/` directory exists in the workspace root. Clicking it:
|
|
@@ -599,6 +599,10 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
599
599
|
customPathsRef.current = customPaths;
|
|
600
600
|
var recentSavesRef = useRef({});
|
|
601
601
|
var isSavingRef = useRef(false);
|
|
602
|
+
// path -> the file's content as last seen ON DISK (newline-normalised).
|
|
603
|
+
// External-change detection compares disk-to-disk; comparing disk to the
|
|
604
|
+
// buffer flags every dirty tab, which is just the definition of "dirty".
|
|
605
|
+
var lastDiskContentRef = useRef({});
|
|
602
606
|
|
|
603
607
|
// ── Draft backup helpers ─────────────────────────────────────────────────
|
|
604
608
|
var draftWriteTimerRef = useRef({});
|
|
@@ -1456,7 +1460,21 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
1456
1460
|
if (!data || typeof data.content !== 'string') return;
|
|
1457
1461
|
var serverNorm = data.content.replace(/\r\n/g, '\n');
|
|
1458
1462
|
var tabNorm = (pt.tab.content || '').replace(/\r\n/g, '\n');
|
|
1463
|
+
|
|
1464
|
+
// Did the file on disk actually change? Compare disk against the last
|
|
1465
|
+
// disk content we saw, never against the buffer — a dirty buffer
|
|
1466
|
+
// differs from disk by definition, so the old comparison reported
|
|
1467
|
+
// every unsaved tab as "updated externally" whenever a files_changed
|
|
1468
|
+
// push arrived (which our own save of some *other* file triggers).
|
|
1469
|
+
// A clean tab's buffer IS the disk content, so it seeds the baseline;
|
|
1470
|
+
// a dirty tab with no baseline yet can't be judged, so record and wait.
|
|
1471
|
+
var prevDisk = lastDiskContentRef.current[pt.tab.path];
|
|
1472
|
+
lastDiskContentRef.current[pt.tab.path] = serverNorm;
|
|
1459
1473
|
if (serverNorm === tabNorm) return;
|
|
1474
|
+
if (prevDisk === undefined && pt.tab.dirty) return;
|
|
1475
|
+
if (prevDisk === undefined) prevDisk = tabNorm;
|
|
1476
|
+
if (serverNorm === prevDisk) return;
|
|
1477
|
+
|
|
1460
1478
|
if (!pt.tab.dirty) {
|
|
1461
1479
|
EditorStore.setState({
|
|
1462
1480
|
panes: EditorStore.getState().panes.map(function (p) {
|
|
@@ -5106,6 +5124,25 @@ var MbeditorApp = function MbeditorApp() {
|
|
|
5106
5124
|
React.createElement("i", { className: "fas fa-stream" }),
|
|
5107
5125
|
" Logs"
|
|
5108
5126
|
),
|
|
5127
|
+
React.createElement(
|
|
5128
|
+
"button",
|
|
5129
|
+
{
|
|
5130
|
+
type: "button",
|
|
5131
|
+
className: "statusbar-btn" + (editorPrefs.renderWhitespace === 'all' ? " active" : ""),
|
|
5132
|
+
title: editorPrefs.renderWhitespace === 'all'
|
|
5133
|
+
? "Hide whitespace characters"
|
|
5134
|
+
: "Show whitespace characters (tabs, spaces, control characters)",
|
|
5135
|
+
"aria-pressed": editorPrefs.renderWhitespace === 'all',
|
|
5136
|
+
onClick: function () {
|
|
5137
|
+
setEditorPrefs(function (p) {
|
|
5138
|
+
return _extends({}, p, {
|
|
5139
|
+
renderWhitespace: p.renderWhitespace === 'all' ? 'none' : 'all'
|
|
5140
|
+
});
|
|
5141
|
+
});
|
|
5142
|
+
}
|
|
5143
|
+
},
|
|
5144
|
+
React.createElement("i", { className: "fas fa-paragraph" })
|
|
5145
|
+
),
|
|
5109
5146
|
activeEOL && React.createElement(
|
|
5110
5147
|
"button",
|
|
5111
5148
|
{
|
|
@@ -107,26 +107,122 @@
|
|
|
107
107
|
|
|
108
108
|
// Declare a discovered global in Monaco's extra libs so the TS2304 warning disappears.
|
|
109
109
|
// Calling addExtraLib with the same URI replaces the previous content in-place.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
//
|
|
111
|
+
// Coalesced: addExtraLib invalidates the TypeScript worker and re-validates
|
|
112
|
+
// EVERY open model. A JSX file that references 500 host-app globals resolves
|
|
113
|
+
// 500 symbols, and calling addExtraLib once per symbol meant 500 full
|
|
114
|
+
// re-validations — the editor spends minutes pegged at 100% CPU redoing work
|
|
115
|
+
// it is about to redo again. Batch them into one flush instead.
|
|
116
|
+
var _discoveredFlushTimer = null;
|
|
117
|
+
function flushDiscoveredGlobals() {
|
|
118
|
+
_discoveredFlushTimer = null;
|
|
114
119
|
var mts = window.monaco && window.monaco.languages && window.monaco.languages.typescript;
|
|
115
|
-
if (!mts) return;
|
|
120
|
+
if (!mts || !mts.javascriptDefaults) return;
|
|
116
121
|
var decls = Object.keys(discoveredJsGlobals)
|
|
117
122
|
.map(function(k) { return 'declare var ' + k + ': any;'; }).join('\n');
|
|
118
123
|
mts.javascriptDefaults.addExtraLib(decls, 'inmemory://mbeditor/discovered-globals.d.ts');
|
|
119
124
|
}
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
126
|
+
function addDiscoveredGlobal(name) {
|
|
127
|
+
if (discoveredJsGlobals[name]) return;
|
|
128
|
+
if (REACT_MINI_UMD_GLOBALS[name]) return; // already in the mini-UMD
|
|
129
|
+
discoveredJsGlobals[name] = true;
|
|
130
|
+
if (_discoveredFlushTimer) return;
|
|
131
|
+
_discoveredFlushTimer = setTimeout(flushDiscoveredGlobals, 300);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Reactive TS2304 resolution runs ONE /js_definition request at a time.
|
|
135
|
+
// Each request spawns an rg process on the server, so firing one per
|
|
136
|
+
// unresolved symbol in parallel (a big JSX file can have hundreds) saturated
|
|
137
|
+
// the dev server: the file tree poll, git status, and file saves all queued
|
|
138
|
+
// behind hundreds of greps. That is what made the whole editor feel slow and
|
|
139
|
+
// what let the "file was edited externally" check race its own save.
|
|
140
|
+
var JS_LOOKUP_QUEUE_MAX = 400;
|
|
141
|
+
var jsLookupQueue = [];
|
|
142
|
+
var jsLookupBusy = false;
|
|
143
|
+
|
|
144
|
+
function pumpJsLookupQueue() {
|
|
145
|
+
if (jsLookupBusy) return;
|
|
146
|
+
var job = jsLookupQueue.shift();
|
|
147
|
+
if (!job) return;
|
|
148
|
+
jsLookupBusy = true;
|
|
149
|
+
var done = function () { jsLookupBusy = false; pumpJsLookupQueue(); };
|
|
150
|
+
FileService.getJsDefinition(job.sym)
|
|
151
|
+
.then(function (data) {
|
|
152
|
+
var results = data && data.results;
|
|
153
|
+
if (results && results.length && results[0].file !== job.modelPath) {
|
|
154
|
+
addDiscoveredGlobal(job.sym);
|
|
155
|
+
} else if (!results || !results.length) {
|
|
156
|
+
if (isRuntimeWindowGlobal(job.sym)) addDiscoveredGlobal(job.sym);
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
.then(done, done);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function queueJsGlobalLookup(sym, modelPath) {
|
|
163
|
+
if (jsLookupQueue.length >= JS_LOOKUP_QUEUE_MAX) return;
|
|
164
|
+
jsLookupQueue.push({ sym: sym, modelPath: modelPath });
|
|
165
|
+
pumpJsLookupQueue();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── The workspace TypeScript program ──────────────────────────────────────
|
|
169
|
+
//
|
|
170
|
+
// Two layers, because one does not cover everything:
|
|
171
|
+
//
|
|
172
|
+
// 1. /js_program — the workspace's own JS source, added as extraLibs at
|
|
173
|
+
// file:/// URIs. A JS file with no import/export is a *script*, so
|
|
174
|
+
// TypeScript puts its top-level declarations in the global scope: the
|
|
175
|
+
// Sprockets model exactly. This gives REAL types — member completion,
|
|
176
|
+
// inferred signatures, argument-count checks — for the host app's own
|
|
177
|
+
// components, and still reports TS2304 for genuinely unknown names.
|
|
178
|
+
//
|
|
179
|
+
// 2. /js_globals — ambient `declare var X: any` for names the program
|
|
180
|
+
// can't supply. UMD-wrapped libraries (React, lodash, axios) assign
|
|
181
|
+
// their global inside a closure, `factory(global.React = {})`, which
|
|
182
|
+
// TypeScript cannot follow statically, so their source contributes no
|
|
183
|
+
// global at all. Those names only exist as ambient declarations.
|
|
184
|
+
//
|
|
185
|
+
// A global is skipped from layer 2 only when layer 1 genuinely supplies it,
|
|
186
|
+
// so a real inferred type is never shadowed by `any` — and, just as
|
|
187
|
+
// importantly, a name the program can't see never loses its declaration.
|
|
188
|
+
// "In a program file" is NOT sufficient: `window.Foo = ...` is a runtime
|
|
189
|
+
// global that TypeScript does not treat as a declaration at all, so those
|
|
190
|
+
// must keep their ambient `declare var` even though their file is in the
|
|
191
|
+
// program. Only lexical declarations land in TypeScript's global scope.
|
|
192
|
+
//
|
|
193
|
+
// Same-URI addExtraLib replaces content in place; that is how both layers
|
|
194
|
+
// refresh.
|
|
195
|
+
var PROGRAM_VISIBLE_KINDS = { 'var': 1, 'let': 1, 'const': 1, 'function': 1, 'class': 1 };
|
|
196
|
+
var programPaths = {}; // workspace-relative path -> true, for the filter above
|
|
197
|
+
|
|
198
|
+
function programUri(path) {
|
|
199
|
+
return 'file:///' + String(path).replace(/^\/+/, '');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function loadWorkspaceProgram(monaco) {
|
|
203
|
+
if (typeof FileService === 'undefined') return;
|
|
204
|
+
var mts = monaco && monaco.languages && monaco.languages.typescript;
|
|
205
|
+
if (!mts || !mts.javascriptDefaults) return;
|
|
206
|
+
|
|
207
|
+
var programLoaded = FileService.getJsProgram
|
|
208
|
+
? FileService.getJsProgram().then(function (data) {
|
|
209
|
+
if (!data || !data.ok || !data.files) return;
|
|
210
|
+
data.files.forEach(function (f) {
|
|
211
|
+
if (!f || typeof f.content !== 'string' || !f.path) return;
|
|
212
|
+
programPaths[f.path] = true;
|
|
213
|
+
mts.javascriptDefaults.addExtraLib(f.content, programUri(f.path));
|
|
214
|
+
});
|
|
215
|
+
if (data.skipped && data.skipped.length && window.console) {
|
|
216
|
+
console.info('[mbeditor] ' + data.fileCount + ' source files (' +
|
|
217
|
+
Math.round(data.totalBytes / 1024) + ' KB) in the TypeScript program; ' +
|
|
218
|
+
data.skipped.length + ' skipped:', data.skipped);
|
|
219
|
+
}
|
|
220
|
+
}).catch(function () { /* fall through to ambient globals alone */ })
|
|
221
|
+
: Promise.resolve();
|
|
222
|
+
|
|
223
|
+
programLoaded.then(function () { loadWorkspaceGlobals(monaco); });
|
|
224
|
+
}
|
|
225
|
+
|
|
130
226
|
function loadWorkspaceGlobals(monaco) {
|
|
131
227
|
if (typeof FileService === 'undefined' || !FileService.getJsGlobals) return;
|
|
132
228
|
var mts = monaco && monaco.languages && monaco.languages.typescript;
|
|
@@ -139,6 +235,11 @@
|
|
|
139
235
|
if (!name || !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return;
|
|
140
236
|
if (REACT_MINI_UMD_GLOBALS[name]) return;
|
|
141
237
|
if (discoveredJsGlobals[name]) return; // already in discovered-globals.d.ts
|
|
238
|
+
// The program already declares this one, with a real type.
|
|
239
|
+
if (s.file && programPaths[s.file] && PROGRAM_VISIBLE_KINDS[s.kind]) {
|
|
240
|
+
attemptedJsGlobals[name] = true;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
142
243
|
names.push(name);
|
|
143
244
|
// Pre-seed the reactive resolver so the marker patcher never fires a
|
|
144
245
|
// per-symbol /js_definition request for these.
|
|
@@ -149,6 +250,23 @@
|
|
|
149
250
|
}).catch(function () { /* endpoint unavailable — reactive path still works */ });
|
|
150
251
|
}
|
|
151
252
|
|
|
253
|
+
// Incremental refresh: re-send only the files that changed, never the whole
|
|
254
|
+
// tree. A workspace can be tens of MB, so re-fetching it on every save would
|
|
255
|
+
// cost more than the feature is worth.
|
|
256
|
+
function refreshProgramPaths(monaco, paths) {
|
|
257
|
+
var mts = monaco && monaco.languages && monaco.languages.typescript;
|
|
258
|
+
if (!mts || !mts.javascriptDefaults) return;
|
|
259
|
+
if (typeof FileService === 'undefined' || !FileService.getJsProgramFile) return;
|
|
260
|
+
(paths || []).forEach(function (path) {
|
|
261
|
+
if (!path || !/\.(js|jsx|ts|tsx)$/i.test(path)) return;
|
|
262
|
+
FileService.getJsProgramFile(path).then(function (data) {
|
|
263
|
+
if (!data || !data.ok || !data.file) return;
|
|
264
|
+
programPaths[data.file.path] = true;
|
|
265
|
+
mts.javascriptDefaults.addExtraLib(data.file.content, programUri(data.file.path));
|
|
266
|
+
}).catch(function () {});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
152
270
|
// Navigate to the first workspace definition of a JS symbol.
|
|
153
271
|
// Returns a Promise<boolean> — true if a definition was found and opened.
|
|
154
272
|
// Try the host's ruby-lsp (via the /ruby_lsp bridge) for a Ruby language
|
|
@@ -848,15 +966,22 @@
|
|
|
848
966
|
);
|
|
849
967
|
}
|
|
850
968
|
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
|
|
969
|
+
// The workspace program (source files) plus the ambient globals it can't
|
|
970
|
+
// supply, loaded once now.
|
|
971
|
+
loadWorkspaceProgram(monaco);
|
|
972
|
+
|
|
973
|
+
// On a change: refresh just the touched files' program entries, and
|
|
974
|
+
// re-run the (cheap, cached) globals scan. The whole tree is never
|
|
975
|
+
// re-sent — see refreshProgramPaths.
|
|
854
976
|
var refreshWorkspaceGlobals = function () { loadWorkspaceGlobals(monaco); };
|
|
855
977
|
if (window._ && window._.debounce) {
|
|
856
978
|
refreshWorkspaceGlobals = window._.debounce(refreshWorkspaceGlobals, 2000);
|
|
857
979
|
}
|
|
858
980
|
if (typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged) {
|
|
859
|
-
WebSocketService.onFilesChanged(function () {
|
|
981
|
+
WebSocketService.onFilesChanged(function (payload) {
|
|
982
|
+
if (payload && payload.paths) refreshProgramPaths(monaco, payload.paths);
|
|
983
|
+
refreshWorkspaceGlobals();
|
|
984
|
+
});
|
|
860
985
|
}
|
|
861
986
|
var _lastGlobalsFocusRefresh = Date.now();
|
|
862
987
|
window.addEventListener('focus', function () {
|
|
@@ -956,17 +1081,7 @@
|
|
|
956
1081
|
var sym = match[1];
|
|
957
1082
|
if (attemptedJsGlobals[sym]) return;
|
|
958
1083
|
attemptedJsGlobals[sym] = true;
|
|
959
|
-
|
|
960
|
-
FileService.getJsDefinition(sym)
|
|
961
|
-
.then(function(data) {
|
|
962
|
-
var results = data && data.results;
|
|
963
|
-
if (results && results.length && results[0].file !== modelPath) {
|
|
964
|
-
addDiscoveredGlobal(sym);
|
|
965
|
-
} else if (!results || !results.length) {
|
|
966
|
-
if (isRuntimeWindowGlobal(sym)) addDiscoveredGlobal(sym);
|
|
967
|
-
}
|
|
968
|
-
})
|
|
969
|
-
.catch(function() {});
|
|
1084
|
+
queueJsGlobalLookup(sym, model._mbeditorPath);
|
|
970
1085
|
});
|
|
971
1086
|
});
|
|
972
1087
|
}
|
|
@@ -247,6 +247,20 @@ var FileService = (function () {
|
|
|
247
247
|
.then(function(res) { return res.data; });
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// The workspace's own JS source for Monaco's TypeScript program. This is the
|
|
251
|
+
// largest response the editor fetches (a big app is tens of MB before gzip),
|
|
252
|
+
// so it gets a generous timeout and is only ever fetched whole once — later
|
|
253
|
+
// changes go through getJsProgramFile.
|
|
254
|
+
function getJsProgram() {
|
|
255
|
+
return axios.get(window.mbeditorBasePath() + '/js_program', { timeout: 120000 })
|
|
256
|
+
.then(function(res) { return res.data; });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getJsProgramFile(path) {
|
|
260
|
+
return axios.get(window.mbeditorBasePath() + '/js_program', { params: { path: path }, timeout: 15000 })
|
|
261
|
+
.then(function(res) { return res.data; });
|
|
262
|
+
}
|
|
263
|
+
|
|
250
264
|
function getRelatedFiles(path) {
|
|
251
265
|
return axios.get(window.mbeditorBasePath() + '/related_files', { params: { path: path } })
|
|
252
266
|
.then(function(res) { return res.data; });
|
|
@@ -292,6 +306,8 @@ var FileService = (function () {
|
|
|
292
306
|
getFileIncludes: getFileIncludes,
|
|
293
307
|
getClientConfig: getClientConfig,
|
|
294
308
|
getJsGlobals: getJsGlobals,
|
|
309
|
+
getJsProgram: getJsProgram,
|
|
310
|
+
getJsProgramFile: getJsProgramFile,
|
|
295
311
|
rubyLspRequest: rubyLspRequest,
|
|
296
312
|
lspDiagnostics: lspDiagnostics,
|
|
297
313
|
getRelatedFiles: getRelatedFiles,
|
|
@@ -462,6 +462,21 @@ module Mbeditor
|
|
|
462
462
|
render json: { ok: false, error: e.message }, status: :unprocessable_content
|
|
463
463
|
end
|
|
464
464
|
|
|
465
|
+
# GET /mbeditor/js_program
|
|
466
|
+
# The workspace's own JS source, for Monaco's TypeScript program. With
|
|
467
|
+
# ?path= it returns just that one file, which is how the editor refreshes
|
|
468
|
+
# after a change without re-sending the whole tree.
|
|
469
|
+
def js_program
|
|
470
|
+
if params[:path].present?
|
|
471
|
+
entry = JsProgramService.file(workspace_root, params[:path])
|
|
472
|
+
return render json: { ok: true, file: entry }
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
render json: JsProgramService.call(workspace_root)
|
|
476
|
+
rescue StandardError => e
|
|
477
|
+
render json: { ok: false, error: e.message }, status: :unprocessable_content
|
|
478
|
+
end
|
|
479
|
+
|
|
465
480
|
RUBY_LSP_METHODS = {
|
|
466
481
|
"definition" => "textDocument/definition",
|
|
467
482
|
"hover" => "textDocument/hover",
|
|
@@ -964,6 +979,7 @@ module Mbeditor
|
|
|
964
979
|
FileTreeService.invalidate(root)
|
|
965
980
|
SearchReplaceService.invalidate_cache(root)
|
|
966
981
|
JsGlobalsService.invalidate(root)
|
|
982
|
+
JsProgramService.invalidate(root)
|
|
967
983
|
Thread.new do
|
|
968
984
|
GitInfoService.invalidate(root)
|
|
969
985
|
rescue => e
|
|
@@ -22,6 +22,24 @@ module Mbeditor
|
|
|
22
22
|
|
|
23
23
|
IDENTIFIER = /[A-Za-z_$][A-Za-z0-9_$]*/
|
|
24
24
|
|
|
25
|
+
# Minified bundles are the reason for both guards below.
|
|
26
|
+
#
|
|
27
|
+
# A minified file is one enormous line, and it usually opens with a
|
|
28
|
+
# multi-declarator `var a,b,c,d,…` running to thousands of names. Split on
|
|
29
|
+
# commas, that ONE line yields thousands of one-letter symbols — enough to
|
|
30
|
+
# exhaust MAX_SYMBOLS on its own, so the workspace's actual components are
|
|
31
|
+
# never reached and every reference to them shows "Cannot find name".
|
|
32
|
+
# Worse, declaring `a`/`n`/`t` as ambient `any` silences genuine
|
|
33
|
+
# diagnostics for those names everywhere.
|
|
34
|
+
#
|
|
35
|
+
# The name check catches the conventional cases; the line-length check
|
|
36
|
+
# catches bundles that don't say "min" in the filename. Neither is a
|
|
37
|
+
# judgement about vendored code in general — a normally-formatted
|
|
38
|
+
# vendor/assets library still contributes its globals, which is correct
|
|
39
|
+
# under Sprockets.
|
|
40
|
+
MINIFIED_NAME = /[.\-]min\.(js|jsx|ts|tsx)\z/i
|
|
41
|
+
MAX_LINE_LENGTH = 2_000
|
|
42
|
+
|
|
25
43
|
MUTEX = Mutex.new
|
|
26
44
|
private_constant :MUTEX
|
|
27
45
|
|
|
@@ -47,19 +65,27 @@ module Mbeditor
|
|
|
47
65
|
|
|
48
66
|
def compute(root)
|
|
49
67
|
symbols = {}
|
|
68
|
+
truncated = false
|
|
50
69
|
|
|
51
70
|
CodeSearchService.call(PATTERN, root).each do |raw|
|
|
52
|
-
|
|
71
|
+
if symbols.length >= MAX_SYMBOLS
|
|
72
|
+
truncated = true
|
|
73
|
+
break
|
|
74
|
+
end
|
|
53
75
|
|
|
54
76
|
m = raw.chomp.match(/\A(.+?):(\d+):(.*)\z/m)
|
|
55
77
|
next unless m
|
|
56
78
|
|
|
57
79
|
abs_path = m[1]
|
|
58
80
|
next unless abs_path.start_with?(root)
|
|
81
|
+
next if abs_path.match?(MINIFIED_NAME)
|
|
82
|
+
|
|
83
|
+
snippet = m[3].strip
|
|
84
|
+
next if snippet.length > MAX_LINE_LENGTH
|
|
59
85
|
|
|
60
86
|
rel = abs_path.delete_prefix(root).delete_prefix("/")
|
|
61
87
|
line = m[2].to_i
|
|
62
|
-
extract_identifiers(
|
|
88
|
+
extract_identifiers(snippet).each do |name, kind|
|
|
63
89
|
symbols[name] ||= { name: name, file: rel, line: line, kind: kind }
|
|
64
90
|
end
|
|
65
91
|
end
|
|
@@ -71,6 +97,9 @@ module Mbeditor
|
|
|
71
97
|
{
|
|
72
98
|
ok: true,
|
|
73
99
|
generatedAt: Time.now.to_i,
|
|
100
|
+
# Surfaced so a workspace that outgrows the cap is diagnosable from
|
|
101
|
+
# the endpoint instead of silently missing globals.
|
|
102
|
+
truncated: truncated,
|
|
74
103
|
symbols: symbols.values.first(MAX_SYMBOLS).sort_by { |s| s[:name] }
|
|
75
104
|
}
|
|
76
105
|
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mbeditor
|
|
4
|
+
# Enumerates the workspace's own JavaScript source and hands it to the editor
|
|
5
|
+
# so Monaco's TypeScript worker can build a real program from it.
|
|
6
|
+
#
|
|
7
|
+
# This is the file-based replacement for declaring every discovered name as
|
|
8
|
+
# ambient `any` (JsGlobalsService). Under Sprockets a JS file with no
|
|
9
|
+
# import/export is a *script*, so its top-level declarations land in the
|
|
10
|
+
# global scope — which is exactly TypeScript's own model for script files.
|
|
11
|
+
# Giving the worker the sources instead of a name list yields real inferred
|
|
12
|
+
# types, member completions, and argument-count checking, and it still
|
|
13
|
+
# reports "Cannot find name" for genuinely unknown identifiers.
|
|
14
|
+
#
|
|
15
|
+
# What it deliberately does NOT solve: UMD-wrapped libraries. React, lodash
|
|
16
|
+
# and axios all assign their global inside a closure
|
|
17
|
+
# (`factory(global.React = {})`), which TypeScript cannot follow statically —
|
|
18
|
+
# loading their source produces no global at all. Those stay on hand-written
|
|
19
|
+
# declarations (the React mini-UMD stub) or on JsGlobalsService's ambient
|
|
20
|
+
# names, which is why that service is still here.
|
|
21
|
+
#
|
|
22
|
+
# No truncation: a workspace that exceeds any limit reports what it skipped
|
|
23
|
+
# and why, rather than silently returning a partial program.
|
|
24
|
+
class JsProgramService
|
|
25
|
+
SOURCE_EXT = /\.(js|jsx|ts|tsx)\z/i
|
|
26
|
+
|
|
27
|
+
# Minified bundles cost parse time and yield nothing useful — their globals
|
|
28
|
+
# are one-letter names inside a closure. Matched by convention, then by
|
|
29
|
+
# shape for bundles whose filename doesn't say so.
|
|
30
|
+
MINIFIED_NAME = /[.\-]min\.(js|jsx|ts|tsx)\z/i
|
|
31
|
+
MAX_LINE_LENGTH = 2_000
|
|
32
|
+
|
|
33
|
+
# A single source file this large is a bundle or generated output, not
|
|
34
|
+
# something a person edits.
|
|
35
|
+
MAX_FILE_BYTES = 1024 * 1024
|
|
36
|
+
|
|
37
|
+
CACHE_TTL = 10 # seconds
|
|
38
|
+
|
|
39
|
+
MUTEX = Mutex.new
|
|
40
|
+
private_constant :MUTEX
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
def call(workspace_root)
|
|
44
|
+
root = File.expand_path(workspace_root.to_s)
|
|
45
|
+
now = monotonic
|
|
46
|
+
MUTEX.synchronize do
|
|
47
|
+
entry = (@cache ||= {})[root]
|
|
48
|
+
return entry[:data] if entry && (now - entry[:ts]) < CACHE_TTL
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
data = compute(root)
|
|
52
|
+
MUTEX.synchronize { (@cache ||= {})[root] = { ts: monotonic, data: data } }
|
|
53
|
+
data
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Content for a single workspace-relative path, for incremental refresh
|
|
57
|
+
# after a file changes. Returns nil when the path isn't program material.
|
|
58
|
+
def file(workspace_root, relative_path)
|
|
59
|
+
root = File.expand_path(workspace_root.to_s)
|
|
60
|
+
rel = relative_path.to_s.delete_prefix("/")
|
|
61
|
+
return nil unless rel.match?(SOURCE_EXT)
|
|
62
|
+
return nil if rel.match?(MINIFIED_NAME)
|
|
63
|
+
return nil if matcher(root).excluded?(rel)
|
|
64
|
+
|
|
65
|
+
abs = File.expand_path(File.join(root, rel))
|
|
66
|
+
return nil unless abs == File.join(root, rel) # no traversal out of the workspace
|
|
67
|
+
return nil unless File.file?(abs) && !File.symlink?(abs)
|
|
68
|
+
|
|
69
|
+
content = read_source(abs)
|
|
70
|
+
content && { path: rel, content: content }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def invalidate(workspace_root)
|
|
74
|
+
MUTEX.synchronize { (@cache ||= {}).delete(File.expand_path(workspace_root.to_s)) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def compute(root)
|
|
80
|
+
return disabled_result unless Mbeditor.configuration.js_program
|
|
81
|
+
|
|
82
|
+
files = []
|
|
83
|
+
skipped = []
|
|
84
|
+
total = 0
|
|
85
|
+
|
|
86
|
+
each_candidate(root) do |rel, abs|
|
|
87
|
+
if File.size(abs) > MAX_FILE_BYTES
|
|
88
|
+
skipped << { path: rel, reason: "larger than #{MAX_FILE_BYTES} bytes" }
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
content = read_source(abs)
|
|
93
|
+
if content.nil?
|
|
94
|
+
skipped << { path: rel, reason: "minified or unreadable" }
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
total += content.bytesize
|
|
99
|
+
files << { path: rel, content: content }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
ok: true,
|
|
104
|
+
enabled: true,
|
|
105
|
+
generatedAt: Time.now.to_i,
|
|
106
|
+
fileCount: files.length,
|
|
107
|
+
totalBytes: total,
|
|
108
|
+
skipped: skipped,
|
|
109
|
+
files: files.sort_by { |f| f[:path] }
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def disabled_result
|
|
114
|
+
{ ok: true, enabled: false, generatedAt: Time.now.to_i,
|
|
115
|
+
fileCount: 0, totalBytes: 0, skipped: [], files: [] }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Walks the workspace, pruning excluded directories before descending so
|
|
119
|
+
# a node_modules tree is never entered. Symlinks are skipped outright:
|
|
120
|
+
# this walk is an enumeration, not a resolve_path lookup, and following
|
|
121
|
+
# them could both escape the workspace and loop.
|
|
122
|
+
def each_candidate(root)
|
|
123
|
+
m = matcher(root)
|
|
124
|
+
stack = [root]
|
|
125
|
+
while (dir = stack.pop)
|
|
126
|
+
children(dir).each do |name|
|
|
127
|
+
abs = File.join(dir, name)
|
|
128
|
+
rel = abs.delete_prefix(root).delete_prefix("/")
|
|
129
|
+
next if m.excluded?(rel)
|
|
130
|
+
next if File.symlink?(abs)
|
|
131
|
+
|
|
132
|
+
if File.directory?(abs)
|
|
133
|
+
stack.push(abs)
|
|
134
|
+
elsif name.match?(SOURCE_EXT) && !name.match?(MINIFIED_NAME)
|
|
135
|
+
yield rel, abs
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def children(dir)
|
|
142
|
+
Dir.children(dir)
|
|
143
|
+
rescue SystemCallError
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def matcher(root)
|
|
148
|
+
ExclusionMatcher.new(exclusion_patterns, root: root)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def exclusion_patterns
|
|
152
|
+
Array(Mbeditor.configuration.excluded_paths).map(&:to_s) +
|
|
153
|
+
Array(Mbeditor.configuration.js_program_exclude).map(&:to_s)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns nil for anything that isn't usable program source: unreadable,
|
|
157
|
+
# invalid encoding, or minified-by-shape (one very long line).
|
|
158
|
+
def read_source(abs)
|
|
159
|
+
content = File.read(abs, encoding: Encoding::UTF_8)
|
|
160
|
+
return nil unless content.valid_encoding?
|
|
161
|
+
return nil if content.each_line.any? { |line| line.chomp.length > MAX_LINE_LENGTH }
|
|
162
|
+
|
|
163
|
+
content
|
|
164
|
+
rescue SystemCallError, IOError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def monotonic
|
|
169
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -9,6 +9,7 @@ module Mbeditor
|
|
|
9
9
|
:lint_timeout, :base_branch_candidates, :git_timeout, :search_timeout,
|
|
10
10
|
:ruby_def_include_dirs, :related_files_custom_paths,
|
|
11
11
|
:mount_path, :resilient_routing, :js_global_identifiers,
|
|
12
|
+
:js_program, :js_program_exclude,
|
|
12
13
|
:js_syntax_check, :babel_standalone_path,
|
|
13
14
|
:ruby_lsp, :ruby_lsp_command, :ruby_lsp_timeout,
|
|
14
15
|
:search_respect_gitignore
|
|
@@ -35,6 +36,15 @@ module Mbeditor
|
|
|
35
36
|
@related_files_custom_paths = []
|
|
36
37
|
@authentication_cache_ttl = 0
|
|
37
38
|
@js_global_identifiers = [] # extra ambient JS globals for the editor (runtime-only names invisible to static scan, e.g. %w[Routes I18n])
|
|
39
|
+
# Load the workspace's own JS source into Monaco's TypeScript program, so
|
|
40
|
+
# cross-file references get real inferred types instead of ambient `any`.
|
|
41
|
+
@js_program = true
|
|
42
|
+
# Excluded from that program on top of excluded_paths. Vendored libraries
|
|
43
|
+
# are UMD-wrapped (the global is assigned inside a closure), so their
|
|
44
|
+
# source yields no globals to TypeScript and only costs parse time — they
|
|
45
|
+
# stay on ambient declarations instead. Add any other directory of
|
|
46
|
+
# third-party or generated JS here, e.g. "app/assets/javascripts/react".
|
|
47
|
+
@js_program_exclude = %w[vendor]
|
|
38
48
|
@js_syntax_check = :auto # save-time babel parse check via host mini_racer + babel-standalone; false disables
|
|
39
49
|
@babel_standalone_path = nil # explicit path to babel-standalone JS; nil auto-detects via the asset pipeline
|
|
40
50
|
@ruby_lsp = :auto # use the host's ruby-lsp for Ruby definitions/hover/completion when available; false disables
|
data/lib/mbeditor/route_map.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Mbeditor
|
|
|
32
32
|
get 'js_definition', to: 'editors#js_definition'
|
|
33
33
|
get 'js_members', to: 'editors#js_members'
|
|
34
34
|
get 'js_globals', to: 'editors#js_globals'
|
|
35
|
+
get 'js_program', to: 'editors#js_program'
|
|
35
36
|
post 'ruby_lsp', to: 'editors#ruby_lsp'
|
|
36
37
|
get 'module_members', to: 'editors#module_members'
|
|
37
38
|
get 'file_includes', to: 'editors#file_includes'
|
data/lib/mbeditor/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mbeditor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -114,6 +114,7 @@ files:
|
|
|
114
114
|
- app/services/mbeditor/js_definition_service.rb
|
|
115
115
|
- app/services/mbeditor/js_globals_service.rb
|
|
116
116
|
- app/services/mbeditor/js_members_service.rb
|
|
117
|
+
- app/services/mbeditor/js_program_service.rb
|
|
117
118
|
- app/services/mbeditor/js_syntax_check_service.rb
|
|
118
119
|
- app/services/mbeditor/log_tail_service.rb
|
|
119
120
|
- app/services/mbeditor/lsp_diagnostics_translator.rb
|