mbeditor 0.12.8 → 0.13.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 +207 -0
- data/app/assets/javascripts/mbeditor/application.js +7 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
- data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +135 -40
- data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +629 -137
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +218 -13
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
- data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +444 -116
- data/app/assets/javascripts/mbeditor/file_import.js +78 -0
- data/app/assets/javascripts/mbeditor/file_service.js +122 -18
- data/app/assets/javascripts/mbeditor/git_service.js +130 -8
- data/app/assets/javascripts/mbeditor/history_service.js +33 -38
- data/app/assets/javascripts/mbeditor/log_service.js +4 -0
- data/app/assets/javascripts/mbeditor/search_service.js +30 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
- data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
- data/app/assets/stylesheets/mbeditor/editor.css +302 -55
- data/app/channels/mbeditor/channel_authentication.rb +7 -0
- data/app/channels/mbeditor/editor_channel.rb +6 -3
- data/app/controllers/mbeditor/application_controller.rb +5 -0
- data/app/controllers/mbeditor/editors_controller.rb +173 -29
- data/app/controllers/mbeditor/git_controller.rb +3 -3
- data/app/controllers/mbeditor/logs_controller.rb +3 -1
- data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
- data/app/services/mbeditor/editor_state_service.rb +36 -26
- data/app/services/mbeditor/exclusion_matcher.rb +12 -10
- data/app/services/mbeditor/file_operation_service.rb +38 -3
- data/app/services/mbeditor/file_tree_service.rb +30 -2
- data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
- data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
- data/app/services/mbeditor/git_diff_service.rb +5 -1
- data/app/services/mbeditor/git_info_service.rb +20 -8
- data/app/services/mbeditor/git_line_diff_service.rb +6 -2
- data/app/services/mbeditor/git_service.rb +65 -15
- data/app/services/mbeditor/js_definition_service.rb +3 -1
- data/app/services/mbeditor/js_globals_service.rb +7 -4
- data/app/services/mbeditor/js_members_service.rb +3 -2
- data/app/services/mbeditor/js_program_service.rb +15 -5
- data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
- data/app/services/mbeditor/process_runner.rb +42 -12
- data/app/services/mbeditor/ri_definition_service.rb +8 -1
- data/app/services/mbeditor/route_service.rb +45 -8
- data/app/services/mbeditor/rubocop_run_service.rb +86 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -4
- data/app/services/mbeditor/schema_service.rb +65 -64
- data/app/services/mbeditor/search_replace_service.rb +75 -10
- data/app/services/mbeditor/test_runner_service.rb +98 -10
- data/lib/mbeditor/cable_log_filter.rb +8 -2
- data/lib/mbeditor/configuration.rb +12 -1
- data/lib/mbeditor/editor_bootstrap.rb +18 -12
- data/lib/mbeditor/engine.rb +22 -3
- data/lib/mbeditor/exception_log.rb +2 -2
- data/lib/mbeditor/pending_migrations.rb +33 -0
- data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
- data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
- data/lib/mbeditor/route_map.rb +2 -0
- data/lib/mbeditor/ruby_lsp_client.rb +71 -12
- data/lib/mbeditor/version.rb +1 -1
- metadata +7 -2
|
@@ -58,6 +58,35 @@
|
|
|
58
58
|
var jsHoverCache = {};
|
|
59
59
|
var jsMembersCache = {};
|
|
60
60
|
|
|
61
|
+
// Symbols the workspace lookup came back empty on — a component or variable
|
|
62
|
+
// that genuinely does not exist. Their TS2304 is graded Error rather than
|
|
63
|
+
// Warning: "Cannot find name" is only ever a guess until the lookup answers,
|
|
64
|
+
// because host-app globals are invisible to the language service, but once
|
|
65
|
+
// it has answered there is nothing tentative left about it.
|
|
66
|
+
//
|
|
67
|
+
// Held per page rather than per model: the answer is about the workspace,
|
|
68
|
+
// and every open file referencing the name is equally wrong.
|
|
69
|
+
var unknownJsSymbols = {};
|
|
70
|
+
// Assigned by registerGlobalExtensions, which owns the marker rules. A
|
|
71
|
+
// negative lookup changes no model and no extra lib, so nothing would
|
|
72
|
+
// re-fire onDidChangeMarkers on its own and the grade would not be applied
|
|
73
|
+
// until the next keystroke.
|
|
74
|
+
var repatchJsMarkerSeverities = null;
|
|
75
|
+
var _unknownRepatchTimer = null;
|
|
76
|
+
|
|
77
|
+
function noteUnknownJsSymbol(sym) {
|
|
78
|
+
if (unknownJsSymbols[sym]) return;
|
|
79
|
+
unknownJsSymbols[sym] = true;
|
|
80
|
+
// Coalesced for the same reason addExtraLib is: a file full of unknown
|
|
81
|
+
// names resolves them one after another, and re-grading every open model
|
|
82
|
+
// per answer is the same wasted pass repeated.
|
|
83
|
+
if (_unknownRepatchTimer) return;
|
|
84
|
+
_unknownRepatchTimer = setTimeout(function () {
|
|
85
|
+
_unknownRepatchTimer = null;
|
|
86
|
+
if (repatchJsMarkerSeverities) repatchJsMarkerSeverities();
|
|
87
|
+
}, 300);
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
// Enumerate window for user-defined globals and return a TypeScript declaration string.
|
|
62
91
|
// Sprockets exposes every top-level var/function as a window property before Monaco
|
|
63
92
|
// initialises, so scanning at registration time captures all components and helpers.
|
|
@@ -150,10 +179,22 @@
|
|
|
150
179
|
FileService.getJsDefinition(job.sym)
|
|
151
180
|
.then(function (data) {
|
|
152
181
|
var results = data && data.results;
|
|
153
|
-
|
|
182
|
+
var hit = results && results[0];
|
|
183
|
+
// `topLevel` matters, and its absence was a hole: any match anywhere
|
|
184
|
+
// counted, so a `var x` nested inside a function in an unrelated file
|
|
185
|
+
// earned `x` an ambient `declare var` and the reference here stopped
|
|
186
|
+
// being reported at all. A local in another file is not in scope in
|
|
187
|
+
// this one. The hover and goto-definition paths always required
|
|
188
|
+
// top-level; this one is now consistent with them.
|
|
189
|
+
if (hit && hit.topLevel && hit.file !== job.modelPath) {
|
|
154
190
|
addDiscoveredGlobal(job.sym);
|
|
155
|
-
} else if (
|
|
156
|
-
|
|
191
|
+
} else if (isRuntimeWindowGlobal(job.sym)) {
|
|
192
|
+
addDiscoveredGlobal(job.sym);
|
|
193
|
+
} else {
|
|
194
|
+
// Nothing declares this name: not the TypeScript program, not the
|
|
195
|
+
// workspace globals, not a top-level definition anywhere on disk,
|
|
196
|
+
// not a live window property. The answer is in — it does not exist.
|
|
197
|
+
noteUnknownJsSymbol(job.sym);
|
|
157
198
|
}
|
|
158
199
|
})
|
|
159
200
|
.then(done, done);
|
|
@@ -161,6 +202,11 @@
|
|
|
161
202
|
|
|
162
203
|
function queueJsGlobalLookup(sym, modelPath) {
|
|
163
204
|
if (jsLookupQueue.length >= JS_LOOKUP_QUEUE_MAX) return;
|
|
205
|
+
// Marked here rather than at the call site: marking before the cap check
|
|
206
|
+
// spent the symbol's one attempt on a job that was then dropped, so a name
|
|
207
|
+
// unlucky enough to arrive while the queue was full was never looked up at
|
|
208
|
+
// all — not on the next keystroke, not for the rest of the page's life.
|
|
209
|
+
attemptedJsGlobals[sym] = true;
|
|
164
210
|
jsLookupQueue.push({ sym: sym, modelPath: modelPath });
|
|
165
211
|
pumpJsLookupQueue();
|
|
166
212
|
}
|
|
@@ -233,6 +279,11 @@
|
|
|
233
279
|
data.symbols.forEach(function (s) {
|
|
234
280
|
var name = s && s.name;
|
|
235
281
|
if (!name || !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name)) return;
|
|
282
|
+
// This name exists now, whatever an earlier lookup concluded. Writing
|
|
283
|
+
// the component you were being told was missing is the normal way that
|
|
284
|
+
// happens, and the verdict has to expire with the evidence — this list
|
|
285
|
+
// is refreshed on every files_changed.
|
|
286
|
+
delete unknownJsSymbols[name];
|
|
236
287
|
if (REACT_MINI_UMD_GLOBALS[name]) return;
|
|
237
288
|
if (discoveredJsGlobals[name]) return; // already in discovered-globals.d.ts
|
|
238
289
|
// The program already declares this one, with a real type.
|
|
@@ -257,13 +308,23 @@
|
|
|
257
308
|
var mts = monaco && monaco.languages && monaco.languages.typescript;
|
|
258
309
|
if (!mts || !mts.javascriptDefaults) return;
|
|
259
310
|
if (typeof FileService === 'undefined' || !FileService.getJsProgramFile) return;
|
|
260
|
-
(paths || []).
|
|
261
|
-
|
|
262
|
-
|
|
311
|
+
var wanted = (paths || []).filter(function (path) {
|
|
312
|
+
return path && /\.(js|jsx|ts|tsx)$/i.test(path);
|
|
313
|
+
});
|
|
314
|
+
if (!wanted.length) return;
|
|
315
|
+
|
|
316
|
+
// Collected first, applied in one synchronous pass. addExtraLib invalidates
|
|
317
|
+
// the TypeScript worker and re-validates every open model, so applying a
|
|
318
|
+
// burst of saves one response at a time paid that cost per file — the same
|
|
319
|
+
// reason flushDiscoveredGlobals coalesces.
|
|
320
|
+
Promise.all(wanted.map(function (path) {
|
|
321
|
+
return FileService.getJsProgramFile(path).catch(function () { return null; });
|
|
322
|
+
})).then(function (responses) {
|
|
323
|
+
responses.forEach(function (data) {
|
|
263
324
|
if (!data || !data.ok || !data.file) return;
|
|
264
325
|
programPaths[data.file.path] = true;
|
|
265
326
|
mts.javascriptDefaults.addExtraLib(data.file.content, programUri(data.file.path));
|
|
266
|
-
})
|
|
327
|
+
});
|
|
267
328
|
});
|
|
268
329
|
}
|
|
269
330
|
|
|
@@ -344,21 +405,41 @@
|
|
|
344
405
|
|
|
345
406
|
// Send a raw-passthrough request and hand back result.result, or null when
|
|
346
407
|
// the legacy path should run instead.
|
|
347
|
-
function rawRubyLsp(lspMethod, model, position) {
|
|
348
|
-
return tryRubyLsp(lspMethod, model, position || { lineNumber: 1, column: 1 });
|
|
408
|
+
function rawRubyLsp(lspMethod, model, position, token) {
|
|
409
|
+
return tryRubyLsp(lspMethod, model, position || { lineNumber: 1, column: 1 }, null, token);
|
|
349
410
|
}
|
|
350
411
|
|
|
351
412
|
// Requests that go through RuboCop rather than just Prism need more than the
|
|
352
413
|
// 6s default: the server's own budget for them is 10s.
|
|
353
414
|
var LSP_SLOW_METHODS = { diagnostics: 15000, formatting: 15000 };
|
|
354
415
|
|
|
355
|
-
|
|
416
|
+
// `token` is Monaco's CancellationToken, when the calling provider has one.
|
|
417
|
+
// Every provider fires on a gesture the user can abandon — moving the cursor
|
|
418
|
+
// off a word, typing another character — and Monaco cancels the outstanding
|
|
419
|
+
// request when that happens. Without this the request ran to completion
|
|
420
|
+
// anyway: a subprocess on the server, and the answer thrown away here.
|
|
421
|
+
function tryRubyLsp(lspMethod, model, position, extraBody, token) {
|
|
356
422
|
try {
|
|
357
423
|
if (!window.MBEDITOR_RUBY_LSP_AVAILABLE || lspBackedOff()) return Promise.resolve(null);
|
|
358
424
|
if (typeof FileService === 'undefined' || !FileService.rubyLspRequest) return Promise.resolve(null);
|
|
359
425
|
if (!model || !model._mbeditorPath || !position) return Promise.resolve(null);
|
|
360
426
|
if (model.getValueLength() > 5 * 1024 * 1024) return Promise.resolve(null);
|
|
427
|
+
// Nothing in the buffer means nothing to fold, outline, highlight or
|
|
428
|
+
// resolve, so every one of these is a round trip that can only come back
|
|
429
|
+
// empty. A newly created file is opened while empty, so this is the
|
|
430
|
+
// common case rather than an edge one. null is already this function's
|
|
431
|
+
// "no result" answer, so callers need no change.
|
|
432
|
+
if (model.getValueLength() === 0) return Promise.resolve(null);
|
|
433
|
+
if (token && token.isCancellationRequested) return Promise.resolve(null);
|
|
434
|
+
|
|
361
435
|
var config = LSP_SLOW_METHODS[lspMethod] ? { timeout: LSP_SLOW_METHODS[lspMethod] } : null;
|
|
436
|
+
var controller = (token && typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
437
|
+
var cancelSub = null;
|
|
438
|
+
if (controller && token.onCancellationRequested) {
|
|
439
|
+
cancelSub = token.onCancellationRequested(function () { controller.abort(); });
|
|
440
|
+
config = Object.assign({}, config, { signal: controller.signal });
|
|
441
|
+
}
|
|
442
|
+
|
|
362
443
|
return FileService.rubyLspRequest(lspMethod, model._mbeditorPath, model.getValue(),
|
|
363
444
|
position.lineNumber, position.column, config, extraBody)
|
|
364
445
|
.then(function (data) {
|
|
@@ -380,6 +461,12 @@
|
|
|
380
461
|
.catch(function (err) {
|
|
381
462
|
noteLspFailure(err);
|
|
382
463
|
return null;
|
|
464
|
+
})
|
|
465
|
+
// Settled either way: drop the cancellation listener (Monaco's token
|
|
466
|
+
// outlives the request) and answer nothing to a cancelled caller.
|
|
467
|
+
.then(function (result) {
|
|
468
|
+
if (cancelSub && cancelSub.dispose) cancelSub.dispose();
|
|
469
|
+
return (token && token.isCancellationRequested) ? null : result;
|
|
383
470
|
});
|
|
384
471
|
} catch (e) {
|
|
385
472
|
return Promise.resolve(null);
|
|
@@ -459,6 +546,43 @@
|
|
|
459
546
|
return null;
|
|
460
547
|
}
|
|
461
548
|
|
|
549
|
+
// The definition in the file you are already in wins. Sprockets puts every
|
|
550
|
+
// top-level name in one global scope, so the same helper defined in two
|
|
551
|
+
// files is ordinary rather than exceptional — and the server returns them in
|
|
552
|
+
// scan order, which is alphabetical by path and has nothing to do with which
|
|
553
|
+
// one you meant. Taking the first sent you to whichever file happened to
|
|
554
|
+
// sort earliest: Monaco's own provider would reveal the local definition,
|
|
555
|
+
// then this lookup landed and navigated away from it.
|
|
556
|
+
function preferCurrentFile(results, currentPath) {
|
|
557
|
+
if (!currentPath) return null;
|
|
558
|
+
for (var i = 0; i < results.length; i++) {
|
|
559
|
+
if (results[i].file === currentPath) return results[i];
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Candidates for the peek list, one entry per file. Several hits in the same
|
|
565
|
+
// file are the same answer as far as "which file did you mean" goes, and a
|
|
566
|
+
// picker that lists the same path three times is a worse question.
|
|
567
|
+
function distinctByFile(results) {
|
|
568
|
+
var seen = Object.create(null);
|
|
569
|
+
var out = [];
|
|
570
|
+
results.forEach(function (r) {
|
|
571
|
+
if (seen[r.file]) return;
|
|
572
|
+
seen[r.file] = true;
|
|
573
|
+
out.push(r);
|
|
574
|
+
});
|
|
575
|
+
return out;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Monaco's peek list is fed by a DefinitionProvider, and it does NOT dedupe
|
|
579
|
+
// across providers — measured: a provider returning the same location the
|
|
580
|
+
// TypeScript worker resolves turns an ordinary local jump into a two-entry
|
|
581
|
+
// picker for one definition. So this provider stays silent except for the
|
|
582
|
+
// one case that actually needs it, which navigateToJsWord hands over here
|
|
583
|
+
// immediately before asking Monaco to reveal.
|
|
584
|
+
var pendingJsDefinitionPeek = null;
|
|
585
|
+
|
|
462
586
|
function navigateToJsWord(editor, word, parent) {
|
|
463
587
|
if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return Promise.resolve(false);
|
|
464
588
|
var currentPath = editor.getModel && editor.getModel() && editor.getModel()._mbeditorPath;
|
|
@@ -469,11 +593,40 @@
|
|
|
469
593
|
if (isRuntimeWindowGlobal(word)) addDiscoveredGlobal(word);
|
|
470
594
|
return false;
|
|
471
595
|
}
|
|
472
|
-
var
|
|
596
|
+
var here = preferCurrentFile(results, currentPath);
|
|
597
|
+
var elsewhere = distinctByFile(results.filter(function (x) { return x.file !== currentPath; }));
|
|
598
|
+
|
|
599
|
+
// Defined right here: that is the answer, and asking which one you
|
|
600
|
+
// meant would be noise.
|
|
601
|
+
var r = here || elsewhere[0];
|
|
473
602
|
// Only declare as a global when the definition is itself a top-level
|
|
474
603
|
// (Sprockets-global) declaration in a different file. Nested/member
|
|
475
604
|
// definitions must not get an ambient declare var.
|
|
476
605
|
if (r.topLevel && r.file !== currentPath) addDiscoveredGlobal(word);
|
|
606
|
+
|
|
607
|
+
// Genuinely ambiguous — the same name declared at top level in more
|
|
608
|
+
// than one other file — so choose rather than guess.
|
|
609
|
+
if (!here && elsewhere.length > 1) {
|
|
610
|
+
// Stamped with the model and cursor position the reveal below will
|
|
611
|
+
// ask about, so a candidate list whose reveal never fired cannot
|
|
612
|
+
// surface on an unrelated gesture later.
|
|
613
|
+
var gestureModel = editor.getModel && editor.getModel();
|
|
614
|
+
var gesturePos = editor.getPosition && editor.getPosition();
|
|
615
|
+
pendingJsDefinitionPeek = {
|
|
616
|
+
uri: gestureModel ? gestureModel.uri.toString() : '',
|
|
617
|
+
lineNumber: gesturePos ? gesturePos.lineNumber : 0,
|
|
618
|
+
column: gesturePos ? gesturePos.column : 0,
|
|
619
|
+
locations: elsewhere.map(function (x) {
|
|
620
|
+
return {
|
|
621
|
+
uri: lspUri(x.file),
|
|
622
|
+
range: new window.monaco.Range(x.line || 1, 1, x.line || 1, 1)
|
|
623
|
+
};
|
|
624
|
+
})
|
|
625
|
+
};
|
|
626
|
+
editor.trigger('mbeditor', 'editor.action.revealDefinition', null);
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
|
|
477
630
|
if (typeof TabManager !== 'undefined' && TabManager.openTab) {
|
|
478
631
|
TabManager.openTab(r.file, r.file.split('/').pop(), r.line);
|
|
479
632
|
}
|
|
@@ -816,58 +969,34 @@
|
|
|
816
969
|
return handled;
|
|
817
970
|
});
|
|
818
971
|
|
|
819
|
-
// ──
|
|
972
|
+
// ── Paste is left alone, deliberately ────────────────────────────────────
|
|
820
973
|
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
// pasted range — which is also what makes the two cases distinguishable:
|
|
974
|
+
// There was an indent-on-paste handler here that ran
|
|
975
|
+
// `editor.action.reindentselectedlines` over the pasted range. It did not
|
|
976
|
+
// adjust the paste — it recomputed every line's indent from the language's
|
|
977
|
+
// indentation rules, which model bracket depth and nothing else. Any
|
|
978
|
+
// structure those rules cannot derive was therefore destroyed on arrival:
|
|
827
979
|
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
// byte-identical
|
|
980
|
+
// <Row> pasted <Row>
|
|
981
|
+
// <Cell a={1} /> ────► <Cell a={1} />
|
|
982
|
+
// <Cell b={2} /> <Cell b={2} />
|
|
983
|
+
// </Row> </Row>
|
|
833
984
|
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
var full = pasteModel.getFullModelRange();
|
|
844
|
-
var wholeDocument = e.range.startLineNumber <= full.startLineNumber &&
|
|
845
|
-
e.range.endLineNumber >= full.endLineNumber;
|
|
846
|
-
|
|
847
|
-
var action = editor.getAction(wholeDocument ? 'editor.action.formatDocument' : 'editor.action.formatSelection');
|
|
848
|
-
if (!action) return;
|
|
849
|
-
|
|
850
|
-
if (wholeDocument) {
|
|
851
|
-
action.run()["catch"](function () { /* no formatter, or unparseable — leave it */ });
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// formatSelection works on the selection, so point it at the pasted range
|
|
856
|
-
// and put the cursor back where the paste left it.
|
|
857
|
-
var restore = editor.getSelections();
|
|
858
|
-
editor.setSelection(e.range);
|
|
859
|
-
action.run()["catch"](function () {})["finally"](function () {
|
|
860
|
-
if (restore && restore.length && !editor.getModel().isDisposed()) editor.setSelections(restore);
|
|
861
|
-
});
|
|
862
|
-
});
|
|
863
|
-
|
|
985
|
+
// JSX nesting flattened; the same happened to method chains, ternaries and
|
|
986
|
+
// wrapped arguments. It was right only for brace-structured code, which in
|
|
987
|
+
// a React codebase is the minority of what gets pasted.
|
|
988
|
+
//
|
|
989
|
+
// Preserving what was copied is what every other editor does by default,
|
|
990
|
+
// and it is never destructive: the worst case is a block at the wrong
|
|
991
|
+
// depth, which Tab / Shift+Tab on the still-selected paste fixes, and
|
|
992
|
+
// Format fixes properly. Monaco's own `formatOnPaste` stays off too
|
|
993
|
+
// (EditorPanel passes false) — its contribution declines to run here.
|
|
864
994
|
return {
|
|
865
995
|
dispose: function dispose() {
|
|
866
996
|
if (keydownDisposable) keydownDisposable.dispose();
|
|
867
997
|
if (emmetTabDisposable) emmetTabDisposable.dispose();
|
|
868
998
|
if (jsGotoMouseDisposable) jsGotoMouseDisposable.dispose();
|
|
869
999
|
if (jsGotoActionDisposable) jsGotoActionDisposable.dispose();
|
|
870
|
-
if (pasteDisposable) pasteDisposable.dispose();
|
|
871
1000
|
contentDisposable.dispose();
|
|
872
1001
|
}
|
|
873
1002
|
};
|
|
@@ -885,7 +1014,15 @@
|
|
|
885
1014
|
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
|
|
886
1015
|
noSemanticValidation: false,
|
|
887
1016
|
noSyntaxValidation: false,
|
|
888
|
-
|
|
1017
|
+
// Suggestion diagnostics are TypeScript's advice to a TypeScript
|
|
1018
|
+
// author: nearly all of them on plain JS/JSX are 7044, "Parameter 'x'
|
|
1019
|
+
// implicitly has an 'any' type, but a better type may be inferred from
|
|
1020
|
+
// usage" — one per untyped parameter in the file, in a language that
|
|
1021
|
+
// has no types to annotate. They arrive as Hint/Info, which the marker
|
|
1022
|
+
// patcher below never inspected (it only ever filtered Errors), so
|
|
1023
|
+
// every one of them reached the Problems panel. Semantic and syntax
|
|
1024
|
+
// checking, which is what actually catches mistakes here, stay on.
|
|
1025
|
+
noSuggestionDiagnostics: true
|
|
889
1026
|
// No diagnosticCodesToIgnore here on purpose: JS/JSX markers are
|
|
890
1027
|
// filtered by category in the marker patcher below (see JS_KEEP_CODES),
|
|
891
1028
|
// which is a closed rule rather than a list that grows by one code
|
|
@@ -1048,7 +1185,12 @@
|
|
|
1048
1185
|
}
|
|
1049
1186
|
if (typeof WebSocketService !== 'undefined' && WebSocketService.onFilesChanged) {
|
|
1050
1187
|
WebSocketService.onFilesChanged(function (payload) {
|
|
1051
|
-
if (payload && payload.paths)
|
|
1188
|
+
if (payload && payload.paths) {
|
|
1189
|
+
refreshProgramPaths(monaco, payload.paths);
|
|
1190
|
+
// An edited file's include list is wrong the moment it is saved,
|
|
1191
|
+
// and the TTL alone would keep serving it for another 30 s.
|
|
1192
|
+
payload.paths.forEach(function (p) { delete includesCache[p]; });
|
|
1193
|
+
}
|
|
1052
1194
|
refreshWorkspaceGlobals();
|
|
1053
1195
|
});
|
|
1054
1196
|
}
|
|
@@ -1091,11 +1233,51 @@
|
|
|
1091
1233
|
//
|
|
1092
1234
|
// .ts/.tsx keeps full checking: there the types are hand-written, so a
|
|
1093
1235
|
// type error is a statement about code the author actually wrote.
|
|
1094
|
-
|
|
1236
|
+
//
|
|
1237
|
+
// ── Call-shape diagnostics ───────────────────────────────────────────
|
|
1238
|
+
//
|
|
1239
|
+
// These are kept even though the blanket rule above drops type
|
|
1240
|
+
// diagnostics, because they are not inference guesses: they compare a
|
|
1241
|
+
// call against a signature the author wrote. 2554 counts declared
|
|
1242
|
+
// parameters. The JSX codes only have anything to say when the
|
|
1243
|
+
// component carries a JSDoc `@param {{...}} props` annotation — with a
|
|
1244
|
+
// bare `function Card(props)` the props type is `any` and nothing is
|
|
1245
|
+
// reported at all, so this can only ever fire against a contract
|
|
1246
|
+
// someone stated on purpose.
|
|
1247
|
+
//
|
|
1248
|
+
// Measured before enabling, over 49 real files (the gem's own 39 JS/JSX
|
|
1249
|
+
// sources plus the sample workspace): zero occurrences of any of them,
|
|
1250
|
+
// including in spread_props_test.jsx — the spread case that defeated
|
|
1251
|
+
// the old denylist. 2345 (argument type) is deliberately NOT here: it
|
|
1252
|
+
// fired 3 times on that corpus, every one of them on `isNaN(dateObj)`,
|
|
1253
|
+
// which is a working idiom. That is the arbitrary-inference category
|
|
1254
|
+
// this filter exists to keep out.
|
|
1255
|
+
var JS_CALL_CODES = { '2554': true, '2769': true, '2741': true, '2322': true };
|
|
1256
|
+
var JS_KEEP_CODES = { '2304': true, '6133': true, '2554': true, '2769': true, '2741': true, '2322': true };
|
|
1095
1257
|
var JS_SYNTAX_CODE = /^(?:1\d{3}|17\d{3})$/;
|
|
1096
|
-
var JS_WARN_CODES = { '2304': true, '6133': true };
|
|
1258
|
+
var JS_WARN_CODES = { '2304': true, '6133': true, '2554': true, '2769': true, '2741': true, '2322': true };
|
|
1097
1259
|
var TS_WARN_CODES = { '6133': true };
|
|
1098
1260
|
|
|
1261
|
+
// Graded by what the mistake does when the code runs, which is the only
|
|
1262
|
+
// distinction worth drawing here. An argument past the end of the
|
|
1263
|
+
// parameter list is dropped; a prop the component never reads is inert.
|
|
1264
|
+
// A required prop that was never passed arrives as undefined, and a
|
|
1265
|
+
// value of the wrong type flows straight into the component — both of
|
|
1266
|
+
// those break something, so they read as errors.
|
|
1267
|
+
//
|
|
1268
|
+
// TypeScript folds all three JSX cases into 2769 ("No overload matches
|
|
1269
|
+
// this call") once React's types are in play, so the distinction has to
|
|
1270
|
+
// come from the message chain, which Monaco flattens into the marker.
|
|
1271
|
+
var JS_UNKNOWN_PROPERTY = /does not exist on type/;
|
|
1272
|
+
var JS_MISSING_REQUIRED = /is missing in type .* but required in type/;
|
|
1273
|
+
function callDiagnosticSeverity(code, message) {
|
|
1274
|
+
if (code === '2554') return monaco.MarkerSeverity.Warning;
|
|
1275
|
+
var text = message || '';
|
|
1276
|
+
if (JS_MISSING_REQUIRED.test(text)) return monaco.MarkerSeverity.Error;
|
|
1277
|
+
if (JS_UNKNOWN_PROPERTY.test(text)) return monaco.MarkerSeverity.Warning;
|
|
1278
|
+
return monaco.MarkerSeverity.Error;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1099
1281
|
function keepJsMarker(marker) {
|
|
1100
1282
|
if (marker.severity !== monaco.MarkerSeverity.Error) return true;
|
|
1101
1283
|
var code = String(marker.code == null ? '' : marker.code);
|
|
@@ -1120,8 +1302,17 @@
|
|
|
1120
1302
|
return JS_ASSIGN_AFTER.test(rest);
|
|
1121
1303
|
}
|
|
1122
1304
|
|
|
1305
|
+
// "Cannot find name 'Foo'." → "Foo"
|
|
1306
|
+
function missingNameOf(message) {
|
|
1307
|
+
var match = message && message.match(/Cannot find name '([^']+)'/);
|
|
1308
|
+
return match ? match[1] : null;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1123
1311
|
var _severityPatchActive = false;
|
|
1124
|
-
|
|
1312
|
+
// jsMarkers, when given, is a uri-string -> javascript markers map the
|
|
1313
|
+
// caller has already fetched; getModelMarkers filters the whole marker
|
|
1314
|
+
// store per call and the TS2304 sweep below wants the same list.
|
|
1315
|
+
function patchSeverities(uris, jsMarkers) {
|
|
1125
1316
|
if (_severityPatchActive) return;
|
|
1126
1317
|
_severityPatchActive = true;
|
|
1127
1318
|
try {
|
|
@@ -1132,15 +1323,48 @@
|
|
|
1132
1323
|
{ owner: 'javascript', keep: keepJsMarker, warn: JS_WARN_CODES },
|
|
1133
1324
|
{ owner: 'typescript', keep: null, warn: TS_WARN_CODES }
|
|
1134
1325
|
].forEach(function(entry) {
|
|
1135
|
-
var markers =
|
|
1326
|
+
var markers = (entry.owner === 'javascript' && jsMarkers && jsMarkers[uri.toString()]) ||
|
|
1327
|
+
monaco.editor.getModelMarkers({ resource: uri, owner: entry.owner });
|
|
1136
1328
|
var patched = markers.filter(function(m) {
|
|
1137
1329
|
return entry.keep ? entry.keep(m) : true;
|
|
1138
1330
|
}).map(function(m) {
|
|
1139
|
-
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
|
|
1331
|
+
var code = String(m.code);
|
|
1332
|
+
if (!entry.warn[code]) return m;
|
|
1333
|
+
|
|
1334
|
+
// 2304 is graded from the evidence rather than from what the
|
|
1335
|
+
// marker currently says, because the verdict can move in both
|
|
1336
|
+
// directions: it starts as a Warning and becomes an Error once
|
|
1337
|
+
// the workspace lookup reports the name does not exist. A rule
|
|
1338
|
+
// that only ever fired on an incoming Error could never make
|
|
1339
|
+
// that second move — it had already downgraded the marker on
|
|
1340
|
+
// the first pass.
|
|
1341
|
+
if (code === '2304') {
|
|
1342
|
+
if (isUndeclaredAssignment(model, m)) {
|
|
1343
|
+
return m.message.indexOf(JS_IMPLICIT_GLOBAL_HINT) !== -1 ? m
|
|
1344
|
+
: Object.assign({}, m, {
|
|
1345
|
+
severity: monaco.MarkerSeverity.Error,
|
|
1346
|
+
message: m.message + JS_IMPLICIT_GLOBAL_HINT
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
var sym = missingNameOf(m.message);
|
|
1350
|
+
// Lenient only while the answer is outstanding: host-app
|
|
1351
|
+
// globals are invisible to the language service, so an
|
|
1352
|
+
// unresolved name is a question until the lookup answers it.
|
|
1353
|
+
var want = (sym && unknownJsSymbols[sym])
|
|
1354
|
+
? monaco.MarkerSeverity.Error
|
|
1355
|
+
: monaco.MarkerSeverity.Warning;
|
|
1356
|
+
return m.severity === want ? m : Object.assign({}, m, { severity: want });
|
|
1143
1357
|
}
|
|
1358
|
+
|
|
1359
|
+
// Graded from the evidence for the same reason 2304 is: the
|
|
1360
|
+
// verdict is a property of the message, not of whatever the
|
|
1361
|
+
// marker happens to say on this pass.
|
|
1362
|
+
if (JS_CALL_CODES[code] && entry.owner === 'javascript') {
|
|
1363
|
+
var callWant = callDiagnosticSeverity(code, m.message);
|
|
1364
|
+
return m.severity === callWant ? m : Object.assign({}, m, { severity: callWant });
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (m.severity !== monaco.MarkerSeverity.Error) return m;
|
|
1144
1368
|
return Object.assign({}, m, { severity: monaco.MarkerSeverity.Warning });
|
|
1145
1369
|
});
|
|
1146
1370
|
// Re-applying an unchanged set would re-enter this handler
|
|
@@ -1154,23 +1378,38 @@
|
|
|
1154
1378
|
} finally {
|
|
1155
1379
|
_severityPatchActive = false;
|
|
1156
1380
|
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// A lookup answering "no such symbol" changes no model, so it produces no
|
|
1384
|
+
// marker event of its own — the new grade has to be pushed over the open
|
|
1385
|
+
// models explicitly.
|
|
1386
|
+
repatchJsMarkerSeverities = function () {
|
|
1387
|
+
patchSeverities(monaco.editor.getModels().map(function (m) { return m.uri; }));
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
monaco.editor.onDidChangeMarkers(function(uris) {
|
|
1391
|
+
// One fetch per uri, shared by the severity patch and the TS2304 sweep.
|
|
1392
|
+
var jsMarkers = {};
|
|
1393
|
+
uris.forEach(function (uri) {
|
|
1394
|
+
jsMarkers[uri.toString()] = monaco.editor.getModelMarkers({ resource: uri, owner: 'javascript' });
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
patchSeverities(uris, jsMarkers);
|
|
1157
1398
|
|
|
1158
1399
|
// Auto-resolve TS2304 ("Cannot find name 'X'") for JS files by
|
|
1159
1400
|
// looking up the symbol in the workspace. If found, addDiscoveredGlobal
|
|
1160
|
-
// declares it via addExtraLib and Monaco re-validates, removing the
|
|
1401
|
+
// declares it via addExtraLib and Monaco re-validates, removing the
|
|
1402
|
+
// warning; if not found, the name is recorded as unknown and its
|
|
1403
|
+
// marker is re-graded to an Error.
|
|
1161
1404
|
if (typeof FileService !== 'undefined' && FileService.getJsDefinition) {
|
|
1162
1405
|
uris.forEach(function(uri) {
|
|
1163
1406
|
var model = monaco.editor.getModel(uri);
|
|
1164
1407
|
if (!model) return;
|
|
1165
|
-
|
|
1166
|
-
markers.forEach(function(m) {
|
|
1408
|
+
(jsMarkers[uri.toString()] || []).forEach(function(m) {
|
|
1167
1409
|
if (String(m.code) !== '2304') return;
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
if (!match) return;
|
|
1171
|
-
var sym = match[1];
|
|
1410
|
+
var sym = missingNameOf(m.message);
|
|
1411
|
+
if (!sym) return;
|
|
1172
1412
|
if (attemptedJsGlobals[sym]) return;
|
|
1173
|
-
attemptedJsGlobals[sym] = true;
|
|
1174
1413
|
queueJsGlobalLookup(sym, model._mbeditorPath);
|
|
1175
1414
|
});
|
|
1176
1415
|
});
|
|
@@ -1188,6 +1427,23 @@
|
|
|
1188
1427
|
});
|
|
1189
1428
|
}
|
|
1190
1429
|
|
|
1430
|
+
// These drive `editor.action.reindentLines`, which the Format command
|
|
1431
|
+
// falls back to for .ts/.tsx — the only open languages with no Prettier
|
|
1432
|
+
// parser. They do NOT affect typing: Monaco's built-in bracket handling
|
|
1433
|
+
// already produces the same indent on Enter with or without them, verified
|
|
1434
|
+
// case by case. Nothing else should be routed through them, because
|
|
1435
|
+
// bracket depth is all they model — JSX nesting and continuation lines
|
|
1436
|
+
// (method chains, ternaries, wrapped arguments) are invisible to them, and
|
|
1437
|
+
// re-indenting from these rules alone flattens both.
|
|
1438
|
+
['javascript', 'typescript'].forEach(function (langId) {
|
|
1439
|
+
monaco.languages.setLanguageConfiguration(langId, {
|
|
1440
|
+
indentationRules: {
|
|
1441
|
+
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/,
|
|
1442
|
+
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[})\]].*$/
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1191
1447
|
monaco.languages.setLanguageConfiguration('ruby', {
|
|
1192
1448
|
comments: { lineComment: '#', blockComment: ['=begin', '=end'] },
|
|
1193
1449
|
brackets: [['(', ')'], ['{', '}'], ['[', ']']],
|
|
@@ -1567,7 +1823,7 @@
|
|
|
1567
1823
|
command: {
|
|
1568
1824
|
id: 'mbeditor.applyRubocopFix',
|
|
1569
1825
|
title: 'Apply RuboCop fix for ' + cop,
|
|
1570
|
-
arguments: [model, cop,
|
|
1826
|
+
arguments: [model, cop, modelPath]
|
|
1571
1827
|
}
|
|
1572
1828
|
});
|
|
1573
1829
|
});
|
|
@@ -1588,9 +1844,14 @@
|
|
|
1588
1844
|
});
|
|
1589
1845
|
|
|
1590
1846
|
// Command handler that fetches the fix from the backend and applies it.
|
|
1591
|
-
|
|
1847
|
+
// The buffer is read here, at click time, rather than captured in the
|
|
1848
|
+
// action's arguments: a whole copy of the document per marker per lightbulb
|
|
1849
|
+
// pass is wasteful, and the copy went stale — the server autocorrected text
|
|
1850
|
+
// the buffer had already moved past, and the returned edit landed on the
|
|
1851
|
+
// new one.
|
|
1852
|
+
monaco.editor.registerCommand('mbeditor.applyRubocopFix', function(_accessor, model, copName, modelPath) {
|
|
1592
1853
|
if (typeof FileService === 'undefined' || !FileService.quickFixOffense) return;
|
|
1593
|
-
FileService.quickFixOffense(modelPath,
|
|
1854
|
+
FileService.quickFixOffense(modelPath, model.getValue(), copName).then(function(data) {
|
|
1594
1855
|
if (!data || !data.fix) return;
|
|
1595
1856
|
var fix = data.fix;
|
|
1596
1857
|
model.pushEditOperations([], [{
|
|
@@ -1610,12 +1871,39 @@
|
|
|
1610
1871
|
|
|
1611
1872
|
// ── ruby-lsp navigation ──────────────────────────────────────────────────
|
|
1612
1873
|
|
|
1874
|
+
// Serves exactly one thing: the candidate list navigateToJsWord hands over
|
|
1875
|
+
// when a name is declared at top level in several other files. It is empty
|
|
1876
|
+
// the rest of the time, so the TypeScript worker remains the only voice on
|
|
1877
|
+
// an ordinary jump and a local definition still goes straight there —
|
|
1878
|
+
// Monaco merges providers without deduping, so a second opinion here would
|
|
1879
|
+
// show a picker listing one definition twice.
|
|
1880
|
+
monaco.languages.registerDefinitionProvider('javascript', {
|
|
1881
|
+
provideDefinition: function (model, position) {
|
|
1882
|
+
var pending = pendingJsDefinitionPeek;
|
|
1883
|
+
pendingJsDefinitionPeek = null;
|
|
1884
|
+
if (!pending) return null;
|
|
1885
|
+
if (pending.uri !== model.uri.toString() ||
|
|
1886
|
+
pending.lineNumber !== position.lineNumber ||
|
|
1887
|
+
pending.column !== position.column) return null;
|
|
1888
|
+
return pending.locations;
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1613
1892
|
// Teaches Monaco how to open a file:// resource in this editor. Without it
|
|
1614
1893
|
// every provider below can find a location but nothing can go there:
|
|
1615
1894
|
// peek-definition, the references widget and Ctrl+hover previews all route
|
|
1616
1895
|
// their "open this" through here.
|
|
1617
1896
|
monaco.editor.registerEditorOpener({
|
|
1618
1897
|
openCodeEditor: function (_source, resource, selectionOrPosition) {
|
|
1898
|
+
// Only file:// resources name a workspace file. Models are created
|
|
1899
|
+
// without an explicit URI, so Monaco gives each one an
|
|
1900
|
+
// `inmemory://model/N` identity — and the TS worker returns exactly
|
|
1901
|
+
// that when a JS definition resolves inside the file you are already
|
|
1902
|
+
// in. Stripping it to a path opened a phantom tab called "57".
|
|
1903
|
+
// Handing those back to Monaco lets it reveal the position in the
|
|
1904
|
+
// current editor, which is what the gesture meant.
|
|
1905
|
+
if (String(resource.scheme || '') !== 'file') return false;
|
|
1906
|
+
|
|
1619
1907
|
var path = String(resource.path || '').replace(/^\/+/, '');
|
|
1620
1908
|
if (!path || typeof TabManager === 'undefined' || !TabManager.openTab) return false;
|
|
1621
1909
|
|
|
@@ -1695,10 +1983,10 @@
|
|
|
1695
1983
|
// text grep, not a reference index — so an empty list is the honest answer
|
|
1696
1984
|
// when ruby-lsp can't help.
|
|
1697
1985
|
monaco.languages.registerReferenceProvider('ruby', {
|
|
1698
|
-
provideReferences: function provideReferences(model, position) {
|
|
1986
|
+
provideReferences: function provideReferences(model, position, _context, token) {
|
|
1699
1987
|
if (!rubyNavigableWord(model, position, false)) return null;
|
|
1700
1988
|
|
|
1701
|
-
return rawRubyLsp('references', model, position).then(function (locations) {
|
|
1989
|
+
return rawRubyLsp('references', model, position, token).then(function (locations) {
|
|
1702
1990
|
if (!Array.isArray(locations)) return null;
|
|
1703
1991
|
return locations.filter(function (loc) { return loc && loc.uri; }).map(function (loc) {
|
|
1704
1992
|
return { uri: lspUri(loc.uri), range: lspRange(loc.range) };
|
|
@@ -1710,13 +1998,28 @@
|
|
|
1710
1998
|
// Occurrences of the symbol under the cursor. Monaco has a built-in
|
|
1711
1999
|
// word-match highlighter, but it cannot tell a local named `id` from a
|
|
1712
2000
|
// method named `id`; ruby-lsp resolves the actual symbol.
|
|
2001
|
+
//
|
|
2002
|
+
// This one fires on every cursor move, so a caret walking through a word
|
|
2003
|
+
// would otherwise cost a request per keypress. One memo entry is enough:
|
|
2004
|
+
// the repeat is always the immediately preceding position. Keyed on the
|
|
2005
|
+
// exact occurrence rather than the word text — the same name in two scopes
|
|
2006
|
+
// is two different symbols with two different highlight sets — and on the
|
|
2007
|
+
// model version, so an edit invalidates it.
|
|
2008
|
+
var _highlightMemo = null;
|
|
1713
2009
|
monaco.languages.registerDocumentHighlightProvider('ruby', {
|
|
1714
|
-
provideDocumentHighlights: function provideDocumentHighlights(model, position) {
|
|
1715
|
-
|
|
2010
|
+
provideDocumentHighlights: function provideDocumentHighlights(model, position, token) {
|
|
2011
|
+
var wordInfo = model.getWordAtPosition(position);
|
|
2012
|
+
var key = wordInfo && (model.uri.toString() + '@' + model.getVersionId() + ':' +
|
|
2013
|
+
position.lineNumber + ':' + wordInfo.startColumn + ':' + wordInfo.word);
|
|
2014
|
+
if (key && _highlightMemo && _highlightMemo.key === key) return _highlightMemo.result;
|
|
2015
|
+
|
|
2016
|
+
return rawRubyLsp('document_highlight', model, position, token).then(function (highlights) {
|
|
1716
2017
|
if (!Array.isArray(highlights)) return null;
|
|
1717
|
-
|
|
2018
|
+
var result = highlights.filter(Boolean).map(function (h) {
|
|
1718
2019
|
return { range: lspRange(h.range), kind: h.kind };
|
|
1719
2020
|
});
|
|
2021
|
+
if (key) _highlightMemo = { key: key, result: result };
|
|
2022
|
+
return result;
|
|
1720
2023
|
});
|
|
1721
2024
|
}
|
|
1722
2025
|
});
|
|
@@ -1756,8 +2059,8 @@
|
|
|
1756
2059
|
|
|
1757
2060
|
monaco.languages.registerDocumentSymbolProvider('ruby', {
|
|
1758
2061
|
displayName: 'Ruby',
|
|
1759
|
-
provideDocumentSymbols: function provideDocumentSymbols(model) {
|
|
1760
|
-
return rawRubyLsp('document_symbol', model).then(function (symbols) {
|
|
2062
|
+
provideDocumentSymbols: function provideDocumentSymbols(model, token) {
|
|
2063
|
+
return rawRubyLsp('document_symbol', model, null, token).then(function (symbols) {
|
|
1761
2064
|
if (!Array.isArray(symbols)) return null;
|
|
1762
2065
|
return toMonacoSymbols(symbols, 0);
|
|
1763
2066
|
});
|
|
@@ -1770,11 +2073,11 @@
|
|
|
1770
2073
|
// for when ruby-lsp is unavailable.
|
|
1771
2074
|
monaco.languages.registerDocumentFormattingEditProvider('ruby', {
|
|
1772
2075
|
displayName: 'RuboCop',
|
|
1773
|
-
provideDocumentFormattingEdits: function provideDocumentFormattingEdits(model, options) {
|
|
2076
|
+
provideDocumentFormattingEdits: function provideDocumentFormattingEdits(model, options, token) {
|
|
1774
2077
|
var opts = { tab_size: (options && options.tabSize) || 2,
|
|
1775
2078
|
insert_spaces: !options || options.insertSpaces !== false };
|
|
1776
2079
|
|
|
1777
|
-
return tryRubyLsp('formatting', model, { lineNumber: 1, column: 1 }, opts)
|
|
2080
|
+
return tryRubyLsp('formatting', model, { lineNumber: 1, column: 1 }, opts, token)
|
|
1778
2081
|
.then(function (edits) {
|
|
1779
2082
|
if (Array.isArray(edits)) {
|
|
1780
2083
|
return edits.map(function (e) {
|
|
@@ -1878,8 +2181,8 @@
|
|
|
1878
2181
|
// Parameter hints while typing a call's arguments.
|
|
1879
2182
|
monaco.languages.registerSignatureHelpProvider('ruby', {
|
|
1880
2183
|
signatureHelpTriggerCharacters: ['(', ','],
|
|
1881
|
-
provideSignatureHelp: function provideSignatureHelp(model, position) {
|
|
1882
|
-
return rawRubyLsp('signature_help', model, position).then(function (help) {
|
|
2184
|
+
provideSignatureHelp: function provideSignatureHelp(model, position, token) {
|
|
2185
|
+
return rawRubyLsp('signature_help', model, position, token).then(function (help) {
|
|
1883
2186
|
if (!help || !Array.isArray(help.signatures) || !help.signatures.length) return null;
|
|
1884
2187
|
// LSP's SignatureHelp is structurally identical to Monaco's, and
|
|
1885
2188
|
// MarkupContent {kind, value} is accepted where an IMarkdownString is.
|
|
@@ -1890,12 +2193,12 @@
|
|
|
1890
2193
|
|
|
1891
2194
|
// Smart expand/shrink selection (Shift+Alt+Right / Left).
|
|
1892
2195
|
monaco.languages.registerSelectionRangeProvider('ruby', {
|
|
1893
|
-
provideSelectionRanges: function provideSelectionRanges(model, positions) {
|
|
2196
|
+
provideSelectionRanges: function provideSelectionRanges(model, positions, token) {
|
|
1894
2197
|
// LSP takes a list of positions and answers one linked list per
|
|
1895
2198
|
// position; the bridge sends a single position, so ask per position and
|
|
1896
2199
|
// flatten each chain into the array Monaco wants.
|
|
1897
2200
|
return Promise.all(positions.map(function (position) {
|
|
1898
|
-
return rawRubyLsp('selection_range', model, position).then(function (result) {
|
|
2201
|
+
return rawRubyLsp('selection_range', model, position, token).then(function (result) {
|
|
1899
2202
|
var node = Array.isArray(result) ? result[0] : null;
|
|
1900
2203
|
var ranges = [];
|
|
1901
2204
|
// Bounded: the chain comes from a subprocess and a cycle would spin.
|
|
@@ -1915,8 +2218,8 @@
|
|
|
1915
2218
|
// declines anything else up front — otherwise F2 on a method name would
|
|
1916
2219
|
// open the input box and then fail after you'd typed a new name.
|
|
1917
2220
|
monaco.languages.registerRenameProvider('ruby', {
|
|
1918
|
-
resolveRenameLocation: function resolveRenameLocation(model, position) {
|
|
1919
|
-
return rawRubyLsp('prepare_rename', model, position).then(function (result) {
|
|
2221
|
+
resolveRenameLocation: function resolveRenameLocation(model, position, token) {
|
|
2222
|
+
return rawRubyLsp('prepare_rename', model, position, token).then(function (result) {
|
|
1920
2223
|
if (!result) {
|
|
1921
2224
|
return { rejectReason: 'Only Ruby constants can be renamed here.' };
|
|
1922
2225
|
}
|
|
@@ -1994,8 +2297,8 @@
|
|
|
1994
2297
|
// folding provider, so the vim-marker provider registered further down
|
|
1995
2298
|
// keeps working alongside this one.
|
|
1996
2299
|
monaco.languages.registerFoldingRangeProvider('ruby', {
|
|
1997
|
-
provideFoldingRanges: function provideFoldingRanges(model) {
|
|
1998
|
-
return rawRubyLsp('folding_range', model).then(function (ranges) {
|
|
2300
|
+
provideFoldingRanges: function provideFoldingRanges(model, _context, token) {
|
|
2301
|
+
return rawRubyLsp('folding_range', model, null, token).then(function (ranges) {
|
|
1999
2302
|
if (!Array.isArray(ranges)) return null;
|
|
2000
2303
|
return ranges.filter(Boolean).map(function (r) {
|
|
2001
2304
|
return {
|
|
@@ -2019,6 +2322,20 @@
|
|
|
2019
2322
|
var HOVER_CACHE_TTL_MS = 60000;
|
|
2020
2323
|
var HOVER_MEMBER_LIMIT = 20;
|
|
2021
2324
|
|
|
2325
|
+
// Store an entry, stamping it and sweeping expired keys as it goes. The TTL
|
|
2326
|
+
// was only ever consulted on read, and nothing else ever removed a key, so
|
|
2327
|
+
// every symbol hovered in a session stayed resident for the life of the
|
|
2328
|
+
// page. The sweep is O(entries) on a miss, which at these sizes is free.
|
|
2329
|
+
function cacheWrite(cache, ttlMs, key, entry) {
|
|
2330
|
+
var now = Date.now();
|
|
2331
|
+
Object.keys(cache).forEach(function (k) {
|
|
2332
|
+
if (now - cache[k].ts >= ttlMs) delete cache[k];
|
|
2333
|
+
});
|
|
2334
|
+
entry.ts = now;
|
|
2335
|
+
cache[key] = entry;
|
|
2336
|
+
return entry;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2022
2339
|
// ruby-lsp's hover for a constant is a title, a Definitions link and any
|
|
2023
2340
|
// doc comments — it never lists what the class/module defines. Keep the
|
|
2024
2341
|
// /module_members breakdown that the legacy hover showed, appended below.
|
|
@@ -2044,7 +2361,7 @@
|
|
|
2044
2361
|
}
|
|
2045
2362
|
markdown = '\n\n' + lines.join('\n');
|
|
2046
2363
|
}
|
|
2047
|
-
hoverCache
|
|
2364
|
+
cacheWrite(hoverCache, HOVER_CACHE_TTL_MS, key, { markdown: markdown });
|
|
2048
2365
|
return markdown;
|
|
2049
2366
|
}).catch(function() { return ''; });
|
|
2050
2367
|
}
|
|
@@ -2087,11 +2404,11 @@
|
|
|
2087
2404
|
range: new monaco.Range(position.lineNumber, wordInfo.startColumn, position.lineNumber, wordInfo.endColumn),
|
|
2088
2405
|
contents: [{ value: lsp.markdown + members, isTrusted: true }]
|
|
2089
2406
|
};
|
|
2090
|
-
hoverCache
|
|
2407
|
+
cacheWrite(hoverCache, HOVER_CACHE_TTL_MS, lspKey, { result: lspResult });
|
|
2091
2408
|
return lspResult;
|
|
2092
2409
|
});
|
|
2093
2410
|
}
|
|
2094
|
-
hoverCache
|
|
2411
|
+
cacheWrite(hoverCache, HOVER_CACHE_TTL_MS, lspKey, { result: null });
|
|
2095
2412
|
return legacyRubyHover(model, position, token, wordInfo);
|
|
2096
2413
|
});
|
|
2097
2414
|
}
|
|
@@ -2120,7 +2437,7 @@
|
|
|
2120
2437
|
return FileService.getModuleMembers(word, modController ? { signal: modController.signal } : {}).then(function(data) {
|
|
2121
2438
|
if (token && token.isCancellationRequested) return null;
|
|
2122
2439
|
if (!data || !data.methods || data.methods.length === 0) {
|
|
2123
|
-
hoverCache
|
|
2440
|
+
cacheWrite(hoverCache, HOVER_CACHE_TTL_MS, modKey, { result: null });
|
|
2124
2441
|
return null;
|
|
2125
2442
|
}
|
|
2126
2443
|
var lines = ['**' + data.name + '** `' + (data.file || '') + '`\n'];
|
|
@@ -2128,7 +2445,7 @@
|
|
|
2128
2445
|
lines.push('- `' + (m.signature || m.name) + '`');
|
|
2129
2446
|
});
|
|
2130
2447
|
var result = { contents: [{ value: lines.join('\n') }] };
|
|
2131
|
-
hoverCache
|
|
2448
|
+
cacheWrite(hoverCache, HOVER_CACHE_TTL_MS, modKey, { result: result });
|
|
2132
2449
|
return result;
|
|
2133
2450
|
}).catch(function() { return null; });
|
|
2134
2451
|
}
|
|
@@ -2161,7 +2478,7 @@
|
|
|
2161
2478
|
|
|
2162
2479
|
var results = data && Array.isArray(data.results) ? data.results : [];
|
|
2163
2480
|
// Cache the raw results (before current-file filter).
|
|
2164
|
-
hoverCache
|
|
2481
|
+
cacheWrite(hoverCache, HOVER_CACHE_TTL_MS, word, { results: results });
|
|
2165
2482
|
|
|
2166
2483
|
if (currentFile) {
|
|
2167
2484
|
results = results.filter(function(r) { return r.file !== currentFile; });
|
|
@@ -2306,7 +2623,7 @@
|
|
|
2306
2623
|
}
|
|
2307
2624
|
|
|
2308
2625
|
return FileService.getFileIncludes(path).then(function(result) {
|
|
2309
|
-
includesCache
|
|
2626
|
+
cacheWrite(includesCache, INCLUDES_CACHE_TTL_MS, path, { data: result });
|
|
2310
2627
|
return buildSuggestions(result);
|
|
2311
2628
|
}).catch(function() { return { suggestions: [] }; });
|
|
2312
2629
|
}
|
|
@@ -2359,7 +2676,9 @@
|
|
|
2359
2676
|
jsHoverCache[cacheKey] = { ts: Date.now(), contents: null };
|
|
2360
2677
|
return null;
|
|
2361
2678
|
}
|
|
2362
|
-
|
|
2679
|
+
// Same preference as Ctrl+click, so the file the hover names is
|
|
2680
|
+
// the file the jump would take you to.
|
|
2681
|
+
var r = preferCurrentFile(results, model._mbeditorPath) || results[0];
|
|
2363
2682
|
// Only declare as global when the definition is a top-level
|
|
2364
2683
|
// (Sprockets-global) declaration in a different file — nested and
|
|
2365
2684
|
// member definitions must not get a duplicate declare var.
|
|
@@ -2390,29 +2709,38 @@
|
|
|
2390
2709
|
if (!discoveredJsGlobals[symbol] && !/^[A-Z]/.test(symbol)) return { suggestions: [] };
|
|
2391
2710
|
if (typeof FileService === 'undefined' || !FileService.getJsMembers) return { suggestions: [] };
|
|
2392
2711
|
|
|
2712
|
+
// Only the raw member list is cached. Suggestions carry the insert
|
|
2713
|
+
// `range`, which is where the completion is about to be written — the
|
|
2714
|
+
// same trap the hover provider's makeHoverRange avoids. Cached
|
|
2715
|
+
// suggestions inserted at the position of the invocation that filled
|
|
2716
|
+
// the cache, so a hit anywhere else in the file was misapplied or
|
|
2717
|
+
// silently dropped by Monaco.
|
|
2718
|
+
function buildSuggestions(members) {
|
|
2719
|
+
return members.map(function(m) {
|
|
2720
|
+
return {
|
|
2721
|
+
label: m.name,
|
|
2722
|
+
kind: monaco.languages.CompletionItemKind.Method,
|
|
2723
|
+
detail: symbol,
|
|
2724
|
+
documentation: m.snippet,
|
|
2725
|
+
insertText: m.name,
|
|
2726
|
+
range: {
|
|
2727
|
+
startLineNumber: position.lineNumber, endLineNumber: position.lineNumber,
|
|
2728
|
+
startColumn: position.column, endColumn: position.column
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2393
2734
|
var cached = jsMembersCache[symbol];
|
|
2394
2735
|
if (cached && (Date.now() - cached.ts) < JS_MEMBERS_CACHE_TTL_MS) {
|
|
2395
|
-
return { suggestions: cached.
|
|
2736
|
+
return { suggestions: buildSuggestions(cached.members) };
|
|
2396
2737
|
}
|
|
2397
2738
|
|
|
2398
2739
|
return FileService.getJsMembers(symbol)
|
|
2399
2740
|
.then(function(data) {
|
|
2400
2741
|
var members = (data && data.members) || [];
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
label: m.name,
|
|
2404
|
-
kind: monaco.languages.CompletionItemKind.Method,
|
|
2405
|
-
detail: symbol,
|
|
2406
|
-
documentation: m.snippet,
|
|
2407
|
-
insertText: m.name,
|
|
2408
|
-
range: {
|
|
2409
|
-
startLineNumber: position.lineNumber, endLineNumber: position.lineNumber,
|
|
2410
|
-
startColumn: position.column, endColumn: position.column
|
|
2411
|
-
}
|
|
2412
|
-
};
|
|
2413
|
-
});
|
|
2414
|
-
jsMembersCache[symbol] = { ts: Date.now(), suggestions: suggestions };
|
|
2415
|
-
return { suggestions: suggestions };
|
|
2742
|
+
jsMembersCache[symbol] = { ts: Date.now(), members: members };
|
|
2743
|
+
return { suggestions: buildSuggestions(members) };
|
|
2416
2744
|
}).catch(function() { return { suggestions: [] }; });
|
|
2417
2745
|
}
|
|
2418
2746
|
});
|