mbeditor 0.11.0 → 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 +131 -0
- data/README.md +153 -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 +911 -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 +517 -111
- data/app/assets/javascripts/mbeditor/file_import.js +146 -0
- data/app/assets/javascripts/mbeditor/file_service.js +52 -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 +465 -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/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 +33 -3
- data/lib/mbeditor/engine.rb +34 -0
- data/lib/mbeditor/exception_log.rb +84 -0
- data/lib/mbeditor/route_map.rb +5 -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 +15 -2
|
@@ -275,24 +275,110 @@
|
|
|
275
275
|
// error, server-side fallback signal, or an empty result. A 422 means the
|
|
276
276
|
// server has decided ruby-lsp is unavailable — flip the flag off so we stop
|
|
277
277
|
// asking.
|
|
278
|
-
|
|
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');
|
|
317
|
+
try {
|
|
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) {
|
|
279
356
|
try {
|
|
280
|
-
if (!window.MBEDITOR_RUBY_LSP_AVAILABLE) return Promise.resolve(null);
|
|
357
|
+
if (!window.MBEDITOR_RUBY_LSP_AVAILABLE || lspBackedOff()) return Promise.resolve(null);
|
|
281
358
|
if (typeof FileService === 'undefined' || !FileService.rubyLspRequest) return Promise.resolve(null);
|
|
282
359
|
if (!model || !model._mbeditorPath || !position) return Promise.resolve(null);
|
|
283
360
|
if (model.getValueLength() > 5 * 1024 * 1024) return Promise.resolve(null);
|
|
284
|
-
|
|
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)
|
|
285
364
|
.then(function (data) {
|
|
286
|
-
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
|
+
}
|
|
287
371
|
if (lspMethod === 'definition') return (data.results && data.results.length) ? data : null;
|
|
288
372
|
if (lspMethod === 'hover') return data.markdown ? data : null;
|
|
289
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;
|
|
290
378
|
return null;
|
|
291
379
|
})
|
|
292
380
|
.catch(function (err) {
|
|
293
|
-
|
|
294
|
-
window.MBEDITOR_RUBY_LSP_AVAILABLE = false;
|
|
295
|
-
}
|
|
381
|
+
noteLspFailure(err);
|
|
296
382
|
return null;
|
|
297
383
|
});
|
|
298
384
|
} catch (e) {
|
|
@@ -563,8 +649,6 @@
|
|
|
563
649
|
var suppressInternalEdit = false;
|
|
564
650
|
var keydownDisposable = null;
|
|
565
651
|
var emmetTabDisposable = null;
|
|
566
|
-
var gotoMouseDisposable = null;
|
|
567
|
-
var gotoActionDisposable = null;
|
|
568
652
|
var jsGotoMouseDisposable = null;
|
|
569
653
|
var jsGotoActionDisposable = null;
|
|
570
654
|
|
|
@@ -665,84 +749,9 @@
|
|
|
665
749
|
event.stopPropagation();
|
|
666
750
|
});
|
|
667
751
|
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
function legacyNavigateToWord(word) {
|
|
672
|
-
if (/^[A-Z]/.test(word) && typeof FileService !== 'undefined' && FileService.getModuleMembers) {
|
|
673
|
-
FileService.getModuleMembers(word).then(function(data) {
|
|
674
|
-
if (!data || !data.file) return;
|
|
675
|
-
var filename = data.file.split('/').pop();
|
|
676
|
-
if (typeof TabManager !== 'undefined' && TabManager.openTab) {
|
|
677
|
-
TabManager.openTab(data.file, filename, 1);
|
|
678
|
-
}
|
|
679
|
-
}).catch(function() {});
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
if (typeof FileService === 'undefined' || !FileService.getDefinition) return;
|
|
683
|
-
FileService.getDefinition(word, 'ruby').then(function(data) {
|
|
684
|
-
var results = data && Array.isArray(data.results) ? data.results : [];
|
|
685
|
-
if (results.length === 0) return;
|
|
686
|
-
var r = results[0];
|
|
687
|
-
if (typeof TabManager !== 'undefined' && TabManager.openTab) {
|
|
688
|
-
TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
|
|
689
|
-
}
|
|
690
|
-
}).catch(function() {});
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function navigateToWord(word, position) {
|
|
694
|
-
if (isErbDoc) return legacyNavigateToWord(word);
|
|
695
|
-
|
|
696
|
-
tryRubyLsp('definition', model, position).then(function (lsp) {
|
|
697
|
-
if (lsp && lsp.results && lsp.results.length) {
|
|
698
|
-
var r = lsp.results[0];
|
|
699
|
-
if (typeof TabManager !== 'undefined' && TabManager.openTab) {
|
|
700
|
-
TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
|
|
701
|
-
}
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
legacyNavigateToWord(word);
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// Ctrl/Cmd+click — navigate to definition
|
|
709
|
-
gotoMouseDisposable = editor.onMouseDown(function(event) {
|
|
710
|
-
var ctrlOrCmd = event.event.ctrlKey || event.event.metaKey;
|
|
711
|
-
if (!ctrlOrCmd) return;
|
|
712
|
-
// Target type 6 = CONTENT_TEXT in Monaco's MouseTargetType enum
|
|
713
|
-
if (!event.target || event.target.type !== 6) return;
|
|
714
|
-
|
|
715
|
-
var position = event.target.position;
|
|
716
|
-
if (!position) return;
|
|
717
|
-
|
|
718
|
-
var wordInfo = model.getWordAtPosition(position);
|
|
719
|
-
if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
|
|
720
|
-
if (RUBY_KEYWORDS[wordInfo.word]) return;
|
|
721
|
-
if (RUBY_CORE_METHODS[wordInfo.word]) return;
|
|
722
|
-
if (isErbDoc && (!rubyContextAt(position) || RAILS_VIEW_HELPERS[wordInfo.word])) return;
|
|
723
|
-
|
|
724
|
-
event.event.preventDefault();
|
|
725
|
-
navigateToWord(wordInfo.word, position);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// F12 — go to definition from keyboard
|
|
729
|
-
gotoActionDisposable = editor.addAction({
|
|
730
|
-
id: 'mbeditor.gotoRubyDefinition',
|
|
731
|
-
label: 'Go to Ruby Definition',
|
|
732
|
-
keybindings: [window.monaco.KeyCode.F12],
|
|
733
|
-
contextMenuGroupId: 'navigation',
|
|
734
|
-
contextMenuOrder: 1.5,
|
|
735
|
-
run: function(ed) {
|
|
736
|
-
var pos = ed.getPosition();
|
|
737
|
-
if (!pos) return;
|
|
738
|
-
var wordInfo = model.getWordAtPosition(pos);
|
|
739
|
-
if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return;
|
|
740
|
-
if (RUBY_KEYWORDS[wordInfo.word]) return;
|
|
741
|
-
if (RUBY_CORE_METHODS[wordInfo.word]) return;
|
|
742
|
-
if (isErbDoc && (!rubyContextAt(pos) || RAILS_VIEW_HELPERS[wordInfo.word])) return;
|
|
743
|
-
navigateToWord(wordInfo.word, pos);
|
|
744
|
-
}
|
|
745
|
-
});
|
|
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.
|
|
746
755
|
}
|
|
747
756
|
|
|
748
757
|
if (language === 'javascript') {
|
|
@@ -811,8 +820,6 @@
|
|
|
811
820
|
dispose: function dispose() {
|
|
812
821
|
if (keydownDisposable) keydownDisposable.dispose();
|
|
813
822
|
if (emmetTabDisposable) emmetTabDisposable.dispose();
|
|
814
|
-
if (gotoMouseDisposable) gotoMouseDisposable.dispose();
|
|
815
|
-
if (gotoActionDisposable) gotoActionDisposable.dispose();
|
|
816
823
|
if (jsGotoMouseDisposable) jsGotoMouseDisposable.dispose();
|
|
817
824
|
if (jsGotoActionDisposable) jsGotoActionDisposable.dispose();
|
|
818
825
|
contentDisposable.dispose();
|
|
@@ -1033,6 +1040,24 @@
|
|
|
1033
1040
|
return JS_KEEP_CODES[code] === true || JS_SYNTAX_CODE.test(code);
|
|
1034
1041
|
}
|
|
1035
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
|
+
|
|
1036
1061
|
var _severityPatchActive = false;
|
|
1037
1062
|
monaco.editor.onDidChangeMarkers(function(uris) {
|
|
1038
1063
|
if (_severityPatchActive) return;
|
|
@@ -1049,14 +1074,17 @@
|
|
|
1049
1074
|
var patched = markers.filter(function(m) {
|
|
1050
1075
|
return entry.keep ? entry.keep(m) : true;
|
|
1051
1076
|
}).map(function(m) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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 });
|
|
1055
1083
|
});
|
|
1056
1084
|
// Re-applying an unchanged set would re-enter this handler
|
|
1057
1085
|
// forever, so only write when the patch actually changed something.
|
|
1058
1086
|
var changed = patched.length !== markers.length || patched.some(function(m, i) {
|
|
1059
|
-
return m.severity !== markers[i].severity;
|
|
1087
|
+
return m.severity !== markers[i].severity || m.message !== markers[i].message;
|
|
1060
1088
|
});
|
|
1061
1089
|
if (changed) monaco.editor.setModelMarkers(model, entry.owner, patched);
|
|
1062
1090
|
});
|
|
@@ -1450,44 +1478,87 @@
|
|
|
1450
1478
|
|
|
1451
1479
|
// RuboCop quick-fix code-action provider for Ruby files.
|
|
1452
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.
|
|
1453
1491
|
monaco.languages.registerCodeActionProvider('ruby', {
|
|
1454
1492
|
provideCodeActions: function provideCodeActions(model, _range, context) {
|
|
1455
1493
|
if (!window.MBEDITOR_RUBOCOP_AVAILABLE) return { actions: [], dispose: function() {} };
|
|
1456
1494
|
|
|
1457
1495
|
var correctableCops = model._mbeditorCorrectableCops || new Set();
|
|
1458
|
-
var
|
|
1459
|
-
return m.source === 'rubocop' && m.code && correctableCops.has(m.code);
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
if (rubocopMarkers.length === 0) return { actions: [], dispose: function() {} };
|
|
1463
|
-
|
|
1496
|
+
var embedded = model._mbeditorFixes || {};
|
|
1464
1497
|
var modelPath = model._mbeditorPath || null;
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
+
}
|
|
1468
1529
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
title: 'Fix: ' +
|
|
1530
|
+
if (!cop || !correctableCops.has(cop) || !modelPath) return;
|
|
1531
|
+
actions.push({
|
|
1532
|
+
title: 'Fix: ' + cop,
|
|
1472
1533
|
kind: 'quickfix',
|
|
1473
|
-
isPreferred: rubocopMarkers.length === 1,
|
|
1474
1534
|
diagnostics: [marker],
|
|
1475
1535
|
command: {
|
|
1476
1536
|
id: 'mbeditor.applyRubocopFix',
|
|
1477
|
-
title: 'Apply RuboCop fix for ' +
|
|
1478
|
-
arguments: [model,
|
|
1537
|
+
title: 'Apply RuboCop fix for ' + cop,
|
|
1538
|
+
arguments: [model, cop, model.getValue(), modelPath]
|
|
1479
1539
|
}
|
|
1480
|
-
};
|
|
1540
|
+
});
|
|
1481
1541
|
});
|
|
1482
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
|
+
|
|
1483
1554
|
return { actions: actions, dispose: function() {} };
|
|
1484
1555
|
}
|
|
1485
1556
|
});
|
|
1486
1557
|
|
|
1487
1558
|
// Command handler that fetches the fix from the backend and applies it.
|
|
1488
|
-
monaco.editor.registerCommand('mbeditor.applyRubocopFix', function(_accessor, model,
|
|
1559
|
+
monaco.editor.registerCommand('mbeditor.applyRubocopFix', function(_accessor, model, copName, code, modelPath) {
|
|
1489
1560
|
if (typeof FileService === 'undefined' || !FileService.quickFixOffense) return;
|
|
1490
|
-
FileService.quickFixOffense(modelPath, code,
|
|
1561
|
+
FileService.quickFixOffense(modelPath, code, copName).then(function(data) {
|
|
1491
1562
|
if (!data || !data.fix) return;
|
|
1492
1563
|
var fix = data.fix;
|
|
1493
1564
|
model.pushEditOperations([], [{
|
|
@@ -1505,6 +1576,333 @@
|
|
|
1505
1576
|
TabManager.openTab(path, String(path).split('/').pop(), line || 1);
|
|
1506
1577
|
});
|
|
1507
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
|
+
|
|
1508
1906
|
// Ruby method definition hover provider.
|
|
1509
1907
|
// Calls the backend /definition endpoint (Ripper-based) and renders
|
|
1510
1908
|
// the method signature and any preceding # comments as hover markdown.
|
|
@@ -1943,6 +2341,14 @@
|
|
|
1943
2341
|
window.MbeditorEditorPlugins = {
|
|
1944
2342
|
registerGlobalExtensions: registerGlobalExtensions,
|
|
1945
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,
|
|
1946
2352
|
// Exposed so the ERB gating can be asserted directly in system tests.
|
|
1947
2353
|
isInsideErbTag: isInsideErbTag,
|
|
1948
2354
|
runRubyEnter: function runRubyEnter(editor) {
|