mbeditor 0.10.1 → 0.12.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 +191 -0
- data/README.md +226 -3
- data/app/assets/javascripts/mbeditor/application.js +5 -0
- data/app/assets/javascripts/mbeditor/application_iife_tail.js +6 -0
- data/app/assets/javascripts/mbeditor/collaboration_identity.js +234 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +690 -0
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +120 -19
- data/app/assets/javascripts/mbeditor/components/FileTree.js +127 -8
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +12 -3
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +127 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +948 -72
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +565 -0
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +130 -10
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +1 -0
- data/app/assets/javascripts/mbeditor/components/TabBar.js +4 -2
- data/app/assets/javascripts/mbeditor/editor_plugins.js +661 -140
- data/app/assets/javascripts/mbeditor/file_import.js +146 -0
- data/app/assets/javascripts/mbeditor/file_service.js +68 -3
- data/app/assets/javascripts/mbeditor/tab_manager.js +50 -1
- data/app/assets/javascripts/mbeditor/websocket_service.js +89 -0
- data/app/assets/stylesheets/mbeditor/editor.css +273 -10
- data/app/channels/mbeditor/channel_authentication.rb +94 -0
- data/app/channels/mbeditor/collaboration_channel.rb +84 -0
- data/app/channels/mbeditor/editor_channel.rb +40 -1
- data/app/controllers/mbeditor/application_controller.rb +5 -1
- data/app/controllers/mbeditor/editors_controller.rb +481 -19
- data/app/controllers/mbeditor/git_controller.rb +9 -2
- data/app/services/mbeditor/availability_probe.rb +76 -17
- data/app/services/mbeditor/code_search_service.rb +23 -3
- data/app/services/mbeditor/collaboration_doc_store.rb +116 -0
- data/app/services/mbeditor/file_import_service.rb +103 -0
- data/app/services/mbeditor/git_combined_diff_service.rb +36 -5
- data/app/services/mbeditor/git_info_service.rb +6 -0
- data/app/services/mbeditor/git_service.rb +22 -6
- data/app/services/mbeditor/js_globals_service.rb +31 -2
- data/app/services/mbeditor/js_program_service.rb +173 -0
- data/app/services/mbeditor/lsp_diagnostics_translator.rb +99 -5
- data/app/services/mbeditor/model_graph_service.rb +232 -0
- data/app/services/mbeditor/presence_registry.rb +83 -0
- data/app/services/mbeditor/ri_definition_service.rb +39 -5
- data/app/services/mbeditor/search_replace_service.rb +24 -4
- data/app/views/layouts/mbeditor/application.html.erb +2 -0
- data/lib/mbeditor/configuration.rb +43 -3
- data/lib/mbeditor/engine.rb +34 -0
- data/lib/mbeditor/exception_log.rb +84 -0
- data/lib/mbeditor/route_map.rb +6 -0
- data/lib/mbeditor/ruby_lsp_client.rb +28 -1
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- data/vendor/assets/javascripts/yjs-collab.js +12 -0
- metadata +16 -2
|
@@ -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
|
|
@@ -157,24 +275,110 @@
|
|
|
157
275
|
// error, server-side fallback signal, or an empty result. A 422 means the
|
|
158
276
|
// server has decided ruby-lsp is unavailable — flip the flag off so we stop
|
|
159
277
|
// asking.
|
|
160
|
-
|
|
278
|
+
// A Monaco marker's `code` is either a plain string or, when the backend
|
|
279
|
+
// supplied a docs URL, a { value, target } object. Everything that compares
|
|
280
|
+
// or displays a cop name has to go through this.
|
|
281
|
+
function codeValue(code) {
|
|
282
|
+
if (code && typeof code === 'object') return code.value || '';
|
|
283
|
+
return code || '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Identifies a marker across both the backend shape (copName/startLine) and
|
|
287
|
+
// the Monaco shape (code/startLineNumber), so the code-action provider can
|
|
288
|
+
// find the fixes belonging to the marker it was handed. Cop name alone won't
|
|
289
|
+
// do — the same cop fires many times in a file.
|
|
290
|
+
function markerFixKey(m) {
|
|
291
|
+
var cop = m.copName !== undefined ? m.copName : codeValue(m.code);
|
|
292
|
+
var line = m.startLine !== undefined ? m.startLine : m.startLineNumber;
|
|
293
|
+
var col = m.startCol !== undefined ? m.startCol : m.startColumn;
|
|
294
|
+
return cop + ':' + line + ':' + col;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// How long a failure keeps us off ruby-lsp. Long enough that a dead server
|
|
298
|
+
// isn't hammered on every keystroke, short enough that a server which comes
|
|
299
|
+
// back on its own is picked up without a page reload — which is what the old
|
|
300
|
+
// permanent flag flip cost you.
|
|
301
|
+
var LSP_BACKOFF_MS = 60000;
|
|
302
|
+
|
|
303
|
+
// Single owner of the "ruby-lsp is unwell" state. Everything that talks to
|
|
304
|
+
// the bridge routes its failures here so the status indicator and the
|
|
305
|
+
// request guard can never disagree.
|
|
306
|
+
function noteLspFailure(err) {
|
|
307
|
+
var status = err && err.response && err.response.status;
|
|
308
|
+
var data = (err && err.response && err.response.data) || (err && err.lspData) || {};
|
|
309
|
+
// A 422 means the server has decided ruby-lsp isn't there at all; a
|
|
310
|
+
// 'failed' state means it crashed past its restart budget. Both are worth
|
|
311
|
+
// backing off from. An ordinary timeout is not.
|
|
312
|
+
if (status !== 422 && data.lspState !== 'failed') return;
|
|
313
|
+
|
|
314
|
+
window.MBEDITOR_RUBY_LSP_DISABLED_UNTIL = Date.now() + LSP_BACKOFF_MS;
|
|
315
|
+
window.MBEDITOR_RUBY_LSP_REASON = data.reason || data.error ||
|
|
316
|
+
(data.lspState === 'failed' ? 'ruby-lsp crashed repeatedly' : 'ruby-lsp is unavailable');
|
|
161
317
|
try {
|
|
162
|
-
|
|
318
|
+
window.dispatchEvent(new CustomEvent('mbeditor:lsp-health'));
|
|
319
|
+
} catch (e) { /* CustomEvent unavailable — the guard still works */ }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function lspBackedOff() {
|
|
323
|
+
return Date.now() < (window.MBEDITOR_RUBY_LSP_DISABLED_UNTIL || 0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Raw-passthrough LSP methods keep their 0-based ranges on the wire; these
|
|
327
|
+
// two put them back into Monaco's 1-based world at the provider.
|
|
328
|
+
function lspRange(r) {
|
|
329
|
+
if (!r) return new window.monaco.Range(1, 1, 1, 1);
|
|
330
|
+
return new window.monaco.Range(
|
|
331
|
+
(r.start && r.start.line || 0) + 1,
|
|
332
|
+
(r.start && r.start.character || 0) + 1,
|
|
333
|
+
(r.end && r.end.line || r.start && r.start.line || 0) + 1,
|
|
334
|
+
(r.end && r.end.character || r.start && r.start.character || 0) + 1
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// The backend rewrites every in-workspace file:// URI to a relative path and
|
|
339
|
+
// drops the rest, so what arrives here is always workspace-relative. Monaco
|
|
340
|
+
// needs an absolute-looking URI, and registerEditorOpener maps it back.
|
|
341
|
+
function lspUri(relativePath) {
|
|
342
|
+
return window.monaco.Uri.parse('file:///' + String(relativePath).replace(/^\/+/, ''));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Send a raw-passthrough request and hand back result.result, or null when
|
|
346
|
+
// the legacy path should run instead.
|
|
347
|
+
function rawRubyLsp(lspMethod, model, position) {
|
|
348
|
+
return tryRubyLsp(lspMethod, model, position || { lineNumber: 1, column: 1 });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Requests that go through RuboCop rather than just Prism need more than the
|
|
352
|
+
// 6s default: the server's own budget for them is 10s.
|
|
353
|
+
var LSP_SLOW_METHODS = { diagnostics: 15000, formatting: 15000 };
|
|
354
|
+
|
|
355
|
+
function tryRubyLsp(lspMethod, model, position, extraBody) {
|
|
356
|
+
try {
|
|
357
|
+
if (!window.MBEDITOR_RUBY_LSP_AVAILABLE || lspBackedOff()) return Promise.resolve(null);
|
|
163
358
|
if (typeof FileService === 'undefined' || !FileService.rubyLspRequest) return Promise.resolve(null);
|
|
164
359
|
if (!model || !model._mbeditorPath || !position) return Promise.resolve(null);
|
|
165
360
|
if (model.getValueLength() > 5 * 1024 * 1024) return Promise.resolve(null);
|
|
166
|
-
|
|
361
|
+
var config = LSP_SLOW_METHODS[lspMethod] ? { timeout: LSP_SLOW_METHODS[lspMethod] } : null;
|
|
362
|
+
return FileService.rubyLspRequest(lspMethod, model._mbeditorPath, model.getValue(),
|
|
363
|
+
position.lineNumber, position.column, config, extraBody)
|
|
167
364
|
.then(function (data) {
|
|
168
|
-
if (!data || data.fallback || data.error)
|
|
365
|
+
if (!data || data.fallback || data.error) {
|
|
366
|
+
// A 200 can still carry lspState: 'failed' — the server answered,
|
|
367
|
+
// the language server did not.
|
|
368
|
+
if (data) noteLspFailure({ lspData: data });
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
169
371
|
if (lspMethod === 'definition') return (data.results && data.results.length) ? data : null;
|
|
170
372
|
if (lspMethod === 'hover') return data.markdown ? data : null;
|
|
171
373
|
if (lspMethod === 'completion') return (data.suggestions && data.suggestions.length) ? data : null;
|
|
374
|
+
// Raw-passthrough methods answer with { result: <LSP JSON> }. An
|
|
375
|
+
// empty array is a real answer ("no references here"), not a reason
|
|
376
|
+
// to fall back, so only a missing key counts as nothing.
|
|
377
|
+
if (data.result !== undefined) return data.result;
|
|
172
378
|
return null;
|
|
173
379
|
})
|
|
174
380
|
.catch(function (err) {
|
|
175
|
-
|
|
176
|
-
window.MBEDITOR_RUBY_LSP_AVAILABLE = false;
|
|
177
|
-
}
|
|
381
|
+
noteLspFailure(err);
|
|
178
382
|
return null;
|
|
179
383
|
});
|
|
180
384
|
} catch (e) {
|
|
@@ -445,8 +649,6 @@
|
|
|
445
649
|
var suppressInternalEdit = false;
|
|
446
650
|
var keydownDisposable = null;
|
|
447
651
|
var emmetTabDisposable = null;
|
|
448
|
-
var gotoMouseDisposable = null;
|
|
449
|
-
var gotoActionDisposable = null;
|
|
450
652
|
var jsGotoMouseDisposable = null;
|
|
451
653
|
var jsGotoActionDisposable = null;
|
|
452
654
|
|
|
@@ -547,84 +749,9 @@
|
|
|
547
749
|
event.stopPropagation();
|
|
548
750
|
});
|
|
549
751
|
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
function legacyNavigateToWord(word) {
|
|
554
|
-
if (/^[A-Z]/.test(word) && typeof FileService !== 'undefined' && FileService.getModuleMembers) {
|
|
555
|
-
FileService.getModuleMembers(word).then(function(data) {
|
|
556
|
-
if (!data || !data.file) return;
|
|
557
|
-
var filename = data.file.split('/').pop();
|
|
558
|
-
if (typeof TabManager !== 'undefined' && TabManager.openTab) {
|
|
559
|
-
TabManager.openTab(data.file, filename, 1);
|
|
560
|
-
}
|
|
561
|
-
}).catch(function() {});
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
if (typeof FileService === 'undefined' || !FileService.getDefinition) return;
|
|
565
|
-
FileService.getDefinition(word, 'ruby').then(function(data) {
|
|
566
|
-
var results = data && Array.isArray(data.results) ? data.results : [];
|
|
567
|
-
if (results.length === 0) return;
|
|
568
|
-
var r = results[0];
|
|
569
|
-
if (typeof TabManager !== 'undefined' && TabManager.openTab) {
|
|
570
|
-
TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
|
|
571
|
-
}
|
|
572
|
-
}).catch(function() {});
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function navigateToWord(word, position) {
|
|
576
|
-
if (isErbDoc) return legacyNavigateToWord(word);
|
|
577
|
-
|
|
578
|
-
tryRubyLsp('definition', model, position).then(function (lsp) {
|
|
579
|
-
if (lsp && lsp.results && lsp.results.length) {
|
|
580
|
-
var r = lsp.results[0];
|
|
581
|
-
if (typeof TabManager !== 'undefined' && TabManager.openTab) {
|
|
582
|
-
TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
|
|
583
|
-
}
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
legacyNavigateToWord(word);
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Ctrl/Cmd+click — navigate to definition
|
|
591
|
-
gotoMouseDisposable = editor.onMouseDown(function(event) {
|
|
592
|
-
var ctrlOrCmd = event.event.ctrlKey || event.event.metaKey;
|
|
593
|
-
if (!ctrlOrCmd) return;
|
|
594
|
-
// Target type 6 = CONTENT_TEXT in Monaco's MouseTargetType enum
|
|
595
|
-
if (!event.target || event.target.type !== 6) return;
|
|
596
|
-
|
|
597
|
-
var position = event.target.position;
|
|
598
|
-
if (!position) return;
|
|
599
|
-
|
|
600
|
-
var wordInfo = model.getWordAtPosition(position);
|
|
601
|
-
if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
|
|
602
|
-
if (RUBY_KEYWORDS[wordInfo.word]) return;
|
|
603
|
-
if (RUBY_CORE_METHODS[wordInfo.word]) return;
|
|
604
|
-
if (isErbDoc && (!rubyContextAt(position) || RAILS_VIEW_HELPERS[wordInfo.word])) return;
|
|
605
|
-
|
|
606
|
-
event.event.preventDefault();
|
|
607
|
-
navigateToWord(wordInfo.word, position);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
// F12 — go to definition from keyboard
|
|
611
|
-
gotoActionDisposable = editor.addAction({
|
|
612
|
-
id: 'mbeditor.gotoRubyDefinition',
|
|
613
|
-
label: 'Go to Ruby Definition',
|
|
614
|
-
keybindings: [window.monaco.KeyCode.F12],
|
|
615
|
-
contextMenuGroupId: 'navigation',
|
|
616
|
-
contextMenuOrder: 1.5,
|
|
617
|
-
run: function(ed) {
|
|
618
|
-
var pos = ed.getPosition();
|
|
619
|
-
if (!pos) return;
|
|
620
|
-
var wordInfo = model.getWordAtPosition(pos);
|
|
621
|
-
if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
|
|
622
|
-
if (RUBY_KEYWORDS[wordInfo.word]) return;
|
|
623
|
-
if (RUBY_CORE_METHODS[wordInfo.word]) return;
|
|
624
|
-
if (isErbDoc && (!rubyContextAt(pos) || RAILS_VIEW_HELPERS[wordInfo.word])) return;
|
|
625
|
-
navigateToWord(wordInfo.word, pos);
|
|
626
|
-
}
|
|
627
|
-
});
|
|
752
|
+
// Go-to-definition is a global DefinitionProvider (registered in
|
|
753
|
+
// registerGlobalExtensions), not wired per editor. Monaco owns Ctrl+click,
|
|
754
|
+
// F12, Alt+F12 peek and the Ctrl+hover preview from that one registration.
|
|
628
755
|
}
|
|
629
756
|
|
|
630
757
|
if (language === 'javascript') {
|
|
@@ -693,8 +820,6 @@
|
|
|
693
820
|
dispose: function dispose() {
|
|
694
821
|
if (keydownDisposable) keydownDisposable.dispose();
|
|
695
822
|
if (emmetTabDisposable) emmetTabDisposable.dispose();
|
|
696
|
-
if (gotoMouseDisposable) gotoMouseDisposable.dispose();
|
|
697
|
-
if (gotoActionDisposable) gotoActionDisposable.dispose();
|
|
698
823
|
if (jsGotoMouseDisposable) jsGotoMouseDisposable.dispose();
|
|
699
824
|
if (jsGotoActionDisposable) jsGotoActionDisposable.dispose();
|
|
700
825
|
contentDisposable.dispose();
|
|
@@ -848,15 +973,22 @@
|
|
|
848
973
|
);
|
|
849
974
|
}
|
|
850
975
|
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
|
|
976
|
+
// The workspace program (source files) plus the ambient globals it can't
|
|
977
|
+
// supply, loaded once now.
|
|
978
|
+
loadWorkspaceProgram(monaco);
|
|
979
|
+
|
|
980
|
+
// On a change: refresh just the touched files' program entries, and
|
|
981
|
+
// re-run the (cheap, cached) globals scan. The whole tree is never
|
|
982
|
+
// re-sent — see refreshProgramPaths.
|
|
854
983
|
var refreshWorkspaceGlobals = function () { loadWorkspaceGlobals(monaco); };
|
|
855
984
|
if (window._ && window._.debounce) {
|
|
856
985
|
refreshWorkspaceGlobals = window._.debounce(refreshWorkspaceGlobals, 2000);
|
|
857
986
|
}
|
|
858
987
|
if (typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged) {
|
|
859
|
-
WebSocketService.onFilesChanged(function () {
|
|
988
|
+
WebSocketService.onFilesChanged(function (payload) {
|
|
989
|
+
if (payload && payload.paths) refreshProgramPaths(monaco, payload.paths);
|
|
990
|
+
refreshWorkspaceGlobals();
|
|
991
|
+
});
|
|
860
992
|
}
|
|
861
993
|
var _lastGlobalsFocusRefresh = Date.now();
|
|
862
994
|
window.addEventListener('focus', function () {
|
|
@@ -908,6 +1040,24 @@
|
|
|
908
1040
|
return JS_KEEP_CODES[code] === true || JS_SYNTAX_CODE.test(code);
|
|
909
1041
|
}
|
|
910
1042
|
|
|
1043
|
+
// A 2304 whose span is an assignment TARGET (`foo = 1` with no
|
|
1044
|
+
// declaration anywhere) is an implicit global — code the host's babel
|
|
1045
|
+
// pipeline rejects — so it stays an Error, while read-side 2304s
|
|
1046
|
+
// downgrade to Warning (those are usually host globals the language
|
|
1047
|
+
// service can't see). Matches `=` (not `==`/`=>`) and compound
|
|
1048
|
+
// assignment operators after the flagged identifier.
|
|
1049
|
+
var JS_ASSIGN_AFTER = /^\s*(=(?![=>])|(\*\*|<<|>>>?|[+\-*/%&|^]|&&|\|\||\?\?)=)/;
|
|
1050
|
+
var JS_IMPLICIT_GLOBAL_HINT = ' This assignment creates an implicit global — declare the variable with var, let, or const.';
|
|
1051
|
+
function isUndeclaredAssignment(model, marker) {
|
|
1052
|
+
var rest = model.getValueInRange({
|
|
1053
|
+
startLineNumber: marker.endLineNumber,
|
|
1054
|
+
startColumn: marker.endColumn,
|
|
1055
|
+
endLineNumber: marker.endLineNumber,
|
|
1056
|
+
endColumn: model.getLineMaxColumn(marker.endLineNumber)
|
|
1057
|
+
});
|
|
1058
|
+
return JS_ASSIGN_AFTER.test(rest);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
911
1061
|
var _severityPatchActive = false;
|
|
912
1062
|
monaco.editor.onDidChangeMarkers(function(uris) {
|
|
913
1063
|
if (_severityPatchActive) return;
|
|
@@ -924,14 +1074,17 @@
|
|
|
924
1074
|
var patched = markers.filter(function(m) {
|
|
925
1075
|
return entry.keep ? entry.keep(m) : true;
|
|
926
1076
|
}).map(function(m) {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1077
|
+
if (m.severity !== monaco.MarkerSeverity.Error || !entry.warn[String(m.code)]) return m;
|
|
1078
|
+
if (String(m.code) === '2304' && isUndeclaredAssignment(model, m)) {
|
|
1079
|
+
return m.message.indexOf(JS_IMPLICIT_GLOBAL_HINT) !== -1 ? m
|
|
1080
|
+
: Object.assign({}, m, { message: m.message + JS_IMPLICIT_GLOBAL_HINT });
|
|
1081
|
+
}
|
|
1082
|
+
return Object.assign({}, m, { severity: monaco.MarkerSeverity.Warning });
|
|
930
1083
|
});
|
|
931
1084
|
// Re-applying an unchanged set would re-enter this handler
|
|
932
1085
|
// forever, so only write when the patch actually changed something.
|
|
933
1086
|
var changed = patched.length !== markers.length || patched.some(function(m, i) {
|
|
934
|
-
return m.severity !== markers[i].severity;
|
|
1087
|
+
return m.severity !== markers[i].severity || m.message !== markers[i].message;
|
|
935
1088
|
});
|
|
936
1089
|
if (changed) monaco.editor.setModelMarkers(model, entry.owner, patched);
|
|
937
1090
|
});
|
|
@@ -956,17 +1109,7 @@
|
|
|
956
1109
|
var sym = match[1];
|
|
957
1110
|
if (attemptedJsGlobals[sym]) return;
|
|
958
1111
|
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() {});
|
|
1112
|
+
queueJsGlobalLookup(sym, model._mbeditorPath);
|
|
970
1113
|
});
|
|
971
1114
|
});
|
|
972
1115
|
}
|
|
@@ -1335,44 +1478,87 @@
|
|
|
1335
1478
|
|
|
1336
1479
|
// RuboCop quick-fix code-action provider for Ruby files.
|
|
1337
1480
|
// Only registers when RuboCop is available in the workspace.
|
|
1481
|
+
//
|
|
1482
|
+
// Two sources of fixes, preferred in this order:
|
|
1483
|
+
//
|
|
1484
|
+
// 1. Edits ruby-lsp embedded in the diagnostic itself. Applied directly as
|
|
1485
|
+
// a Monaco workspace edit — no request, no subprocess. This is also the
|
|
1486
|
+
// only source of the "Disable <cop> for this line" action, which
|
|
1487
|
+
// ruby-lsp offers even for cops that can never be autocorrected.
|
|
1488
|
+
// 2. The /quick_fix endpoint, which runs `rubocop -A` over the buffer.
|
|
1489
|
+
// Still needed whenever diagnostics came from the plain rubocop path
|
|
1490
|
+
// rather than ruby-lsp.
|
|
1338
1491
|
monaco.languages.registerCodeActionProvider('ruby', {
|
|
1339
1492
|
provideCodeActions: function provideCodeActions(model, _range, context) {
|
|
1340
1493
|
if (!window.MBEDITOR_RUBOCOP_AVAILABLE) return { actions: [], dispose: function() {} };
|
|
1341
1494
|
|
|
1342
1495
|
var correctableCops = model._mbeditorCorrectableCops || new Set();
|
|
1343
|
-
var
|
|
1344
|
-
return m.source === 'rubocop' && m.code && correctableCops.has(m.code);
|
|
1345
|
-
});
|
|
1346
|
-
|
|
1347
|
-
if (rubocopMarkers.length === 0) return { actions: [], dispose: function() {} };
|
|
1348
|
-
|
|
1496
|
+
var embedded = model._mbeditorFixes || {};
|
|
1349
1497
|
var modelPath = model._mbeditorPath || null;
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1498
|
+
var actions = [];
|
|
1499
|
+
|
|
1500
|
+
context.markers.forEach(function (marker) {
|
|
1501
|
+
if (marker.source !== 'rubocop') return;
|
|
1502
|
+
|
|
1503
|
+
var cop = codeValue(marker.code);
|
|
1504
|
+
var fixes = embedded[markerFixKey(marker)];
|
|
1505
|
+
|
|
1506
|
+
if (fixes && fixes.length) {
|
|
1507
|
+
fixes.forEach(function (fix) {
|
|
1508
|
+
actions.push({
|
|
1509
|
+
title: fix.title,
|
|
1510
|
+
kind: 'quickfix',
|
|
1511
|
+
autocorrect: /^Autocorrect/.test(fix.title),
|
|
1512
|
+
diagnostics: [marker],
|
|
1513
|
+
edit: {
|
|
1514
|
+
edits: fix.edits.map(function (e) {
|
|
1515
|
+
return {
|
|
1516
|
+
resource: model.uri,
|
|
1517
|
+
versionId: model.getVersionId(),
|
|
1518
|
+
textEdit: {
|
|
1519
|
+
range: new monaco.Range(e.startLine, e.startCol, e.endLine, e.endCol),
|
|
1520
|
+
text: e.text
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
})
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1353
1529
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
title: 'Fix: ' +
|
|
1530
|
+
if (!cop || !correctableCops.has(cop) || !modelPath) return;
|
|
1531
|
+
actions.push({
|
|
1532
|
+
title: 'Fix: ' + cop,
|
|
1357
1533
|
kind: 'quickfix',
|
|
1358
|
-
isPreferred: rubocopMarkers.length === 1,
|
|
1359
1534
|
diagnostics: [marker],
|
|
1360
1535
|
command: {
|
|
1361
1536
|
id: 'mbeditor.applyRubocopFix',
|
|
1362
|
-
title: 'Apply RuboCop fix for ' +
|
|
1363
|
-
arguments: [model,
|
|
1537
|
+
title: 'Apply RuboCop fix for ' + cop,
|
|
1538
|
+
arguments: [model, cop, model.getValue(), modelPath]
|
|
1364
1539
|
}
|
|
1365
|
-
};
|
|
1540
|
+
});
|
|
1366
1541
|
});
|
|
1367
1542
|
|
|
1543
|
+
// Mark the autocorrect as preferred when it is the only one on offer,
|
|
1544
|
+
// matching what this provider did before embedded fixes existed. With
|
|
1545
|
+
// several offenses under the cursor there is no single obvious fix, so
|
|
1546
|
+
// nothing is preferred and the user picks from the list.
|
|
1547
|
+
// (Note: monaco's `editor.action.autoFix` ignores isPreferred in this
|
|
1548
|
+
// standalone build — verified against a bare probe provider — so this
|
|
1549
|
+
// only affects ordering in the lightbulb menu.)
|
|
1550
|
+
var autocorrects = actions.filter(function (a) { return a.autocorrect; });
|
|
1551
|
+
if (autocorrects.length === 1) autocorrects[0].isPreferred = true;
|
|
1552
|
+
actions.forEach(function (a) { delete a.autocorrect; });
|
|
1553
|
+
|
|
1368
1554
|
return { actions: actions, dispose: function() {} };
|
|
1369
1555
|
}
|
|
1370
1556
|
});
|
|
1371
1557
|
|
|
1372
1558
|
// Command handler that fetches the fix from the backend and applies it.
|
|
1373
|
-
monaco.editor.registerCommand('mbeditor.applyRubocopFix', function(_accessor, model,
|
|
1559
|
+
monaco.editor.registerCommand('mbeditor.applyRubocopFix', function(_accessor, model, copName, code, modelPath) {
|
|
1374
1560
|
if (typeof FileService === 'undefined' || !FileService.quickFixOffense) return;
|
|
1375
|
-
FileService.quickFixOffense(modelPath, code,
|
|
1561
|
+
FileService.quickFixOffense(modelPath, code, copName).then(function(data) {
|
|
1376
1562
|
if (!data || !data.fix) return;
|
|
1377
1563
|
var fix = data.fix;
|
|
1378
1564
|
model.pushEditOperations([], [{
|
|
@@ -1390,6 +1576,333 @@
|
|
|
1390
1576
|
TabManager.openTab(path, String(path).split('/').pop(), line || 1);
|
|
1391
1577
|
});
|
|
1392
1578
|
|
|
1579
|
+
// ── ruby-lsp navigation ──────────────────────────────────────────────────
|
|
1580
|
+
|
|
1581
|
+
// Teaches Monaco how to open a file:// resource in this editor. Without it
|
|
1582
|
+
// every provider below can find a location but nothing can go there:
|
|
1583
|
+
// peek-definition, the references widget and Ctrl+hover previews all route
|
|
1584
|
+
// their "open this" through here.
|
|
1585
|
+
monaco.editor.registerEditorOpener({
|
|
1586
|
+
openCodeEditor: function (_source, resource, selectionOrPosition) {
|
|
1587
|
+
var path = String(resource.path || '').replace(/^\/+/, '');
|
|
1588
|
+
if (!path || typeof TabManager === 'undefined' || !TabManager.openTab) return false;
|
|
1589
|
+
|
|
1590
|
+
var pos = selectionOrPosition || {};
|
|
1591
|
+
var line = pos.startLineNumber || pos.lineNumber || 1;
|
|
1592
|
+
var col = pos.startColumn || pos.column || 1;
|
|
1593
|
+
TabManager.openTab(path, path.split('/').pop(), line, null, false, col);
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
// Words never worth a definition lookup: language keywords, core methods,
|
|
1599
|
+
// and (in ERB) Rails view helpers that live in the framework rather than
|
|
1600
|
+
// the workspace. Guarding here rather than in the request means Ctrl+hover
|
|
1601
|
+
// over `end` doesn't light up as a link.
|
|
1602
|
+
function rubyNavigableWord(model, position, isErb) {
|
|
1603
|
+
var info = model.getWordAtPosition(position);
|
|
1604
|
+
if (!info || !info.word || info.word.length < 2) return null;
|
|
1605
|
+
if (RUBY_KEYWORDS[info.word] || RUBY_CORE_METHODS[info.word]) return null;
|
|
1606
|
+
if (isErb) {
|
|
1607
|
+
if (!isInsideErbTag(model, position)) return null;
|
|
1608
|
+
if (RAILS_VIEW_HELPERS[info.word]) return null;
|
|
1609
|
+
}
|
|
1610
|
+
return info.word;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Definition: ruby-lsp when it can answer, else the grep/Ripper services.
|
|
1614
|
+
// ERB always takes the legacy path — Prism cannot parse ERB.
|
|
1615
|
+
['ruby', 'erb'].forEach(function (languageId) {
|
|
1616
|
+
var isErb = languageId === 'erb';
|
|
1617
|
+
|
|
1618
|
+
monaco.languages.registerDefinitionProvider(languageId, {
|
|
1619
|
+
provideDefinition: function provideDefinition(model, position) {
|
|
1620
|
+
var word = rubyNavigableWord(model, position, isErb);
|
|
1621
|
+
if (!word) return null;
|
|
1622
|
+
|
|
1623
|
+
var lsp = isErb ? Promise.resolve(null) : tryRubyLsp('definition', model, position);
|
|
1624
|
+
return lsp.then(function (data) {
|
|
1625
|
+
if (data && data.results && data.results.length) {
|
|
1626
|
+
return data.results.map(function (r) {
|
|
1627
|
+
var line = r.line || 1;
|
|
1628
|
+
return {
|
|
1629
|
+
uri: lspUri(r.file),
|
|
1630
|
+
range: new monaco.Range(line, r.col || 1, r.endLine || line, r.endCol || 1)
|
|
1631
|
+
};
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
return legacyRubyDefinition(word);
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// The pre-ruby-lsp lookup, now expressed as locations rather than as a
|
|
1641
|
+
// side-effecting "open a tab": constants resolve through /module_members,
|
|
1642
|
+
// everything else through the Ripper-backed /definition index.
|
|
1643
|
+
function legacyRubyDefinition(word) {
|
|
1644
|
+
if (typeof FileService === 'undefined') return null;
|
|
1645
|
+
|
|
1646
|
+
if (/^[A-Z]/.test(word) && FileService.getModuleMembers) {
|
|
1647
|
+
return FileService.getModuleMembers(word).then(function (data) {
|
|
1648
|
+
if (!data || !data.file) return null;
|
|
1649
|
+
return [{ uri: lspUri(data.file), range: new monaco.Range(1, 1, 1, 1) }];
|
|
1650
|
+
}).catch(function () { return null; });
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (!FileService.getDefinition) return null;
|
|
1654
|
+
return FileService.getDefinition(word, 'ruby').then(function (data) {
|
|
1655
|
+
var results = (data && Array.isArray(data.results)) ? data.results : [];
|
|
1656
|
+
return results.map(function (r) {
|
|
1657
|
+
return { uri: lspUri(r.file), range: new monaco.Range(r.line || 1, 1, r.line || 1, 1) };
|
|
1658
|
+
});
|
|
1659
|
+
}).catch(function () { return null; });
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// References. No legacy equivalent exists — the workspace search panel is a
|
|
1663
|
+
// text grep, not a reference index — so an empty list is the honest answer
|
|
1664
|
+
// when ruby-lsp can't help.
|
|
1665
|
+
monaco.languages.registerReferenceProvider('ruby', {
|
|
1666
|
+
provideReferences: function provideReferences(model, position) {
|
|
1667
|
+
if (!rubyNavigableWord(model, position, false)) return null;
|
|
1668
|
+
|
|
1669
|
+
return rawRubyLsp('references', model, position).then(function (locations) {
|
|
1670
|
+
if (!Array.isArray(locations)) return null;
|
|
1671
|
+
return locations.filter(function (loc) { return loc && loc.uri; }).map(function (loc) {
|
|
1672
|
+
return { uri: lspUri(loc.uri), range: lspRange(loc.range) };
|
|
1673
|
+
});
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
// Occurrences of the symbol under the cursor. Monaco has a built-in
|
|
1679
|
+
// word-match highlighter, but it cannot tell a local named `id` from a
|
|
1680
|
+
// method named `id`; ruby-lsp resolves the actual symbol.
|
|
1681
|
+
monaco.languages.registerDocumentHighlightProvider('ruby', {
|
|
1682
|
+
provideDocumentHighlights: function provideDocumentHighlights(model, position) {
|
|
1683
|
+
return rawRubyLsp('document_highlight', model, position).then(function (highlights) {
|
|
1684
|
+
if (!Array.isArray(highlights)) return null;
|
|
1685
|
+
return highlights.filter(Boolean).map(function (h) {
|
|
1686
|
+
return { range: lspRange(h.range), kind: h.kind };
|
|
1687
|
+
});
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
// Document symbols feed Monaco's breadcrumbs, sticky scroll and
|
|
1693
|
+
// Ctrl+Shift+O — all of which are dark for Ruby today.
|
|
1694
|
+
//
|
|
1695
|
+
// This deliberately does NOT replace ruby_outline.js. That lexer supplies
|
|
1696
|
+
// method visibility and test-block (describe/it/test) entries which
|
|
1697
|
+
// ruby-lsp's documentSymbol does not emit, and the outline panel groups on
|
|
1698
|
+
// both. It also still has to cover ERB, HAML, and the case where ruby-lsp
|
|
1699
|
+
// isn't running.
|
|
1700
|
+
var LSP_SYMBOL_KINDS = {
|
|
1701
|
+
1: 'File', 2: 'Module', 3: 'Namespace', 4: 'Package', 5: 'Class',
|
|
1702
|
+
6: 'Method', 7: 'Property', 8: 'Field', 9: 'Constructor', 10: 'Enum',
|
|
1703
|
+
11: 'Interface', 12: 'Function', 13: 'Variable', 14: 'Constant',
|
|
1704
|
+
15: 'String', 16: 'Number', 17: 'Boolean', 18: 'Array', 19: 'Object',
|
|
1705
|
+
20: 'Key', 21: 'Null', 22: 'EnumMember', 23: 'Struct', 24: 'Event',
|
|
1706
|
+
25: 'Operator', 26: 'TypeParameter'
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
function toMonacoSymbols(symbols, depth) {
|
|
1710
|
+
if (!Array.isArray(symbols) || depth > 16) return [];
|
|
1711
|
+
return symbols.filter(Boolean).map(function (s) {
|
|
1712
|
+
var full = lspRange(s.range);
|
|
1713
|
+
return {
|
|
1714
|
+
name: String(s.name || ''),
|
|
1715
|
+
detail: s.detail || '',
|
|
1716
|
+
kind: monaco.languages.SymbolKind[LSP_SYMBOL_KINDS[s.kind] || 'Variable'],
|
|
1717
|
+
tags: [],
|
|
1718
|
+
range: full,
|
|
1719
|
+
selectionRange: s.selectionRange ? lspRange(s.selectionRange) : full,
|
|
1720
|
+
children: toMonacoSymbols(s.children, depth + 1)
|
|
1721
|
+
};
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
monaco.languages.registerDocumentSymbolProvider('ruby', {
|
|
1726
|
+
displayName: 'Ruby',
|
|
1727
|
+
provideDocumentSymbols: function provideDocumentSymbols(model) {
|
|
1728
|
+
return rawRubyLsp('document_symbol', model).then(function (symbols) {
|
|
1729
|
+
if (!Array.isArray(symbols)) return null;
|
|
1730
|
+
return toMonacoSymbols(symbols, 0);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
// Formatting. Until now Ruby had no formatting provider at all, so
|
|
1736
|
+
// Shift+Alt+F and format-on-save silently did nothing — formatting was
|
|
1737
|
+
// reachable only through the toolbar button. /format stays as the fallback
|
|
1738
|
+
// for when ruby-lsp is unavailable.
|
|
1739
|
+
monaco.languages.registerDocumentFormattingEditProvider('ruby', {
|
|
1740
|
+
displayName: 'RuboCop',
|
|
1741
|
+
provideDocumentFormattingEdits: function provideDocumentFormattingEdits(model, options) {
|
|
1742
|
+
var opts = { tab_size: (options && options.tabSize) || 2,
|
|
1743
|
+
insert_spaces: !options || options.insertSpaces !== false };
|
|
1744
|
+
|
|
1745
|
+
return tryRubyLsp('formatting', model, { lineNumber: 1, column: 1 }, opts)
|
|
1746
|
+
.then(function (edits) {
|
|
1747
|
+
if (Array.isArray(edits)) {
|
|
1748
|
+
return edits.map(function (e) {
|
|
1749
|
+
return { range: lspRange(e.range), text: e.newText || '' };
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
return legacyRubyFormat(model);
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
// Pre-provider path: /format returns the whole corrected file, so it
|
|
1758
|
+
// becomes one replacement spanning the buffer.
|
|
1759
|
+
function legacyRubyFormat(model) {
|
|
1760
|
+
if (typeof FileService === 'undefined' || !FileService.formatFile) return [];
|
|
1761
|
+
if (!model._mbeditorPath) return [];
|
|
1762
|
+
|
|
1763
|
+
return FileService.formatFile(model._mbeditorPath, model.getValue()).then(function (data) {
|
|
1764
|
+
var formatted = data && data.content;
|
|
1765
|
+
if (typeof formatted !== 'string' || formatted === model.getValue()) return [];
|
|
1766
|
+
return [{ range: model.getFullModelRange(), text: formatted }];
|
|
1767
|
+
}).catch(function () { return []; });
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Parameter hints while typing a call's arguments.
|
|
1771
|
+
monaco.languages.registerSignatureHelpProvider('ruby', {
|
|
1772
|
+
signatureHelpTriggerCharacters: ['(', ','],
|
|
1773
|
+
provideSignatureHelp: function provideSignatureHelp(model, position) {
|
|
1774
|
+
return rawRubyLsp('signature_help', model, position).then(function (help) {
|
|
1775
|
+
if (!help || !Array.isArray(help.signatures) || !help.signatures.length) return null;
|
|
1776
|
+
// LSP's SignatureHelp is structurally identical to Monaco's, and
|
|
1777
|
+
// MarkupContent {kind, value} is accepted where an IMarkdownString is.
|
|
1778
|
+
return { value: help, dispose: function () {} };
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
// Smart expand/shrink selection (Shift+Alt+Right / Left).
|
|
1784
|
+
monaco.languages.registerSelectionRangeProvider('ruby', {
|
|
1785
|
+
provideSelectionRanges: function provideSelectionRanges(model, positions) {
|
|
1786
|
+
// LSP takes a list of positions and answers one linked list per
|
|
1787
|
+
// position; the bridge sends a single position, so ask per position and
|
|
1788
|
+
// flatten each chain into the array Monaco wants.
|
|
1789
|
+
return Promise.all(positions.map(function (position) {
|
|
1790
|
+
return rawRubyLsp('selection_range', model, position).then(function (result) {
|
|
1791
|
+
var node = Array.isArray(result) ? result[0] : null;
|
|
1792
|
+
var ranges = [];
|
|
1793
|
+
// Bounded: the chain comes from a subprocess and a cycle would spin.
|
|
1794
|
+
while (node && node.range && ranges.length < 64) {
|
|
1795
|
+
ranges.push({ range: lspRange(node.range) });
|
|
1796
|
+
node = node.parent;
|
|
1797
|
+
}
|
|
1798
|
+
return ranges;
|
|
1799
|
+
});
|
|
1800
|
+
})).then(function (perPosition) {
|
|
1801
|
+
return perPosition.some(function (r) { return r.length; }) ? perPosition : null;
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
// F2 rename. ruby-lsp renames *constants only*, so resolveRenameLocation
|
|
1807
|
+
// declines anything else up front — otherwise F2 on a method name would
|
|
1808
|
+
// open the input box and then fail after you'd typed a new name.
|
|
1809
|
+
monaco.languages.registerRenameProvider('ruby', {
|
|
1810
|
+
resolveRenameLocation: function resolveRenameLocation(model, position) {
|
|
1811
|
+
return rawRubyLsp('prepare_rename', model, position).then(function (result) {
|
|
1812
|
+
if (!result) {
|
|
1813
|
+
return { rejectReason: 'Only Ruby constants can be renamed here.' };
|
|
1814
|
+
}
|
|
1815
|
+
// prepareRename answers either a bare Range or { range, placeholder }.
|
|
1816
|
+
var range = result.range || result;
|
|
1817
|
+
var wordInfo = model.getWordAtPosition(position);
|
|
1818
|
+
return {
|
|
1819
|
+
range: lspRange(range),
|
|
1820
|
+
text: result.placeholder || (wordInfo && wordInfo.word) || ''
|
|
1821
|
+
};
|
|
1822
|
+
});
|
|
1823
|
+
},
|
|
1824
|
+
|
|
1825
|
+
provideRenameEdits: function provideRenameEdits(model, position, newName) {
|
|
1826
|
+
if (typeof FileService === 'undefined' || !FileService.rubyRename) return null;
|
|
1827
|
+
|
|
1828
|
+
// Every path with a live model. The server returns their edits for us
|
|
1829
|
+
// to apply rather than writing them, so unsaved buffers survive.
|
|
1830
|
+
var openPaths = monaco.editor.getModels()
|
|
1831
|
+
.filter(function (m) { return m._mbeditorPath && !m.isDisposed(); })
|
|
1832
|
+
.map(function (m) { return m._mbeditorPath; });
|
|
1833
|
+
|
|
1834
|
+
return FileService.rubyRename(model._mbeditorPath, model.getValue(),
|
|
1835
|
+
position.lineNumber, position.column, newName, openPaths)
|
|
1836
|
+
.then(function (data) {
|
|
1837
|
+
var edits = [];
|
|
1838
|
+
Object.keys((data && data.edits) || {}).forEach(function (relPath) {
|
|
1839
|
+
var target = modelForPath(relPath) || null;
|
|
1840
|
+
data.edits[relPath].forEach(function (e) {
|
|
1841
|
+
edits.push({
|
|
1842
|
+
resource: target ? target.uri : lspUri(relPath),
|
|
1843
|
+
versionId: target ? target.getVersionId() : undefined,
|
|
1844
|
+
textEdit: {
|
|
1845
|
+
range: new monaco.Range(e.startLine, e.startCol, e.endLine, e.endCol),
|
|
1846
|
+
text: e.text
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
reportRenameOutcome(data);
|
|
1853
|
+
return { edits: edits };
|
|
1854
|
+
})
|
|
1855
|
+
.catch(function (err) {
|
|
1856
|
+
var message = (err && err.response && err.response.data && err.response.data.error) ||
|
|
1857
|
+
'Rename failed.';
|
|
1858
|
+
return { edits: [], rejectReason: message };
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
function modelForPath(relPath) {
|
|
1864
|
+
return monaco.editor.getModels().filter(function (m) {
|
|
1865
|
+
return m._mbeditorPath === relPath && !m.isDisposed();
|
|
1866
|
+
})[0];
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Files written straight to disk leave no dirty tab and no undo entry, so
|
|
1870
|
+
// say how many were touched — otherwise a workspace-wide rename looks like
|
|
1871
|
+
// it only changed the file you were looking at.
|
|
1872
|
+
function reportRenameOutcome(data) {
|
|
1873
|
+
if (typeof EditorStore === 'undefined' || !EditorStore.setStatus) return;
|
|
1874
|
+
|
|
1875
|
+
var written = (data && data.written) || [];
|
|
1876
|
+
var rejected = (data && data.rejected) || [];
|
|
1877
|
+
var parts = [];
|
|
1878
|
+
if (written.length) parts.push(written.length + ' file' + (written.length === 1 ? '' : 's') + ' saved');
|
|
1879
|
+
if (rejected.length) parts.push(rejected.length + ' skipped (outside the workspace)');
|
|
1880
|
+
if (!parts.length) return;
|
|
1881
|
+
|
|
1882
|
+
EditorStore.setStatus('Renamed — ' + parts.join(', '), rejected.length ? 'warning' : 'success');
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Real class/def/block/heredoc folding. Monaco merges the results of every
|
|
1886
|
+
// folding provider, so the vim-marker provider registered further down
|
|
1887
|
+
// keeps working alongside this one.
|
|
1888
|
+
monaco.languages.registerFoldingRangeProvider('ruby', {
|
|
1889
|
+
provideFoldingRanges: function provideFoldingRanges(model) {
|
|
1890
|
+
return rawRubyLsp('folding_range', model).then(function (ranges) {
|
|
1891
|
+
if (!Array.isArray(ranges)) return null;
|
|
1892
|
+
return ranges.filter(Boolean).map(function (r) {
|
|
1893
|
+
return {
|
|
1894
|
+
start: (r.startLine || 0) + 1,
|
|
1895
|
+
end: (r.endLine || 0) + 1,
|
|
1896
|
+
kind: r.kind === 'comment' ? monaco.languages.FoldingRangeKind.Comment
|
|
1897
|
+
: r.kind === 'imports' ? monaco.languages.FoldingRangeKind.Imports
|
|
1898
|
+
: r.kind === 'region' ? monaco.languages.FoldingRangeKind.Region
|
|
1899
|
+
: undefined
|
|
1900
|
+
};
|
|
1901
|
+
});
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1393
1906
|
// Ruby method definition hover provider.
|
|
1394
1907
|
// Calls the backend /definition endpoint (Ripper-based) and renders
|
|
1395
1908
|
// the method signature and any preceding # comments as hover markdown.
|
|
@@ -1828,6 +2341,14 @@
|
|
|
1828
2341
|
window.MbeditorEditorPlugins = {
|
|
1829
2342
|
registerGlobalExtensions: registerGlobalExtensions,
|
|
1830
2343
|
attachEditorFeatures: attachEditorFeatures,
|
|
2344
|
+
// The one place that decides ruby-lsp is unwell. Anything outside this file
|
|
2345
|
+
// that talks to the bridge reports its failures here rather than writing
|
|
2346
|
+
// the window flags itself.
|
|
2347
|
+
noteLspFailure: noteLspFailure,
|
|
2348
|
+
lspBackedOff: lspBackedOff,
|
|
2349
|
+
// EditorPanel keys the embedded-fix side map with this; the code-action
|
|
2350
|
+
// provider reads it back. One definition so the two cannot drift.
|
|
2351
|
+
markerFixKey: markerFixKey,
|
|
1831
2352
|
// Exposed so the ERB gating can be asserted directly in system tests.
|
|
1832
2353
|
isInsideErbTag: isInsideErbTag,
|
|
1833
2354
|
runRubyEnter: function runRubyEnter(editor) {
|