mbeditor 0.13.1 → 0.14.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 +91 -0
- data/app/assets/javascripts/mbeditor/application.js +3 -0
- data/app/assets/javascripts/mbeditor/audit_log.js +165 -0
- data/app/assets/javascripts/mbeditor/collaboration_service.js +248 -26
- data/app/assets/javascripts/mbeditor/components/ChangelogView.js +89 -94
- data/app/assets/javascripts/mbeditor/components/CodeReviewPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +12 -7
- data/app/assets/javascripts/mbeditor/components/CombinedDiffViewer.js +20 -0
- data/app/assets/javascripts/mbeditor/components/DiffViewer.js +1 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +429 -247
- data/app/assets/javascripts/mbeditor/components/FileHistoryPanel.js +6 -9
- data/app/assets/javascripts/mbeditor/components/FileTree.js +26 -12
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +3 -0
- data/app/assets/javascripts/mbeditor/components/Gutter.js +51 -0
- data/app/assets/javascripts/mbeditor/components/LogPanel.js +3 -44
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +1168 -1243
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +94 -37
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +47 -65
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +26 -8
- data/app/assets/javascripts/mbeditor/components/SettingsModal.js +342 -0
- data/app/assets/javascripts/mbeditor/components/ShortcutHelp.js +2 -1
- data/app/assets/javascripts/mbeditor/components/TabBar.js +67 -98
- data/app/assets/javascripts/mbeditor/editor_plugins.js +687 -305
- data/app/assets/javascripts/mbeditor/file_import.js +13 -15
- data/app/assets/javascripts/mbeditor/file_service.js +38 -39
- data/app/assets/javascripts/mbeditor/git_service.js +15 -1
- data/app/assets/javascripts/mbeditor/history_service.js +5 -13
- data/app/assets/javascripts/mbeditor/search_service.js +9 -0
- data/app/assets/javascripts/mbeditor/tab_manager.js +127 -49
- data/app/assets/javascripts/mbeditor/websocket_service.js +13 -5
- data/app/assets/stylesheets/mbeditor/application.css +6 -1
- data/app/assets/stylesheets/mbeditor/editor.css +642 -248
- data/app/assets/stylesheets/mbeditor/glass.css +163 -0
- data/app/assets/stylesheets/mbeditor/themes.css +90 -30
- data/app/channels/mbeditor/collaboration_channel.rb +17 -4
- data/app/controllers/mbeditor/application_controller.rb +26 -3
- data/app/controllers/mbeditor/editors_controller.rb +117 -439
- data/app/services/mbeditor/archive_service.rb +137 -0
- data/app/services/mbeditor/collaboration_doc_store.rb +143 -14
- data/app/services/mbeditor/editor_state_service.rb +14 -51
- data/app/services/mbeditor/file_history_service.rb +222 -0
- data/app/services/mbeditor/git_info_service.rb +6 -0
- data/app/services/mbeditor/js_syntax_check_service.rb +42 -16
- data/app/services/mbeditor/lint_service.rb +137 -0
- data/app/services/mbeditor/locked_json_file.rb +67 -0
- data/app/services/mbeditor/process_runner.rb +32 -0
- data/app/services/mbeditor/ruby_lsp_result_translator.rb +226 -0
- data/app/services/mbeditor/search_replace_service.rb +8 -0
- data/app/views/layouts/mbeditor/application.html.erb +1 -1
- data/lib/mbeditor/audit_log.rb +203 -0
- data/lib/mbeditor/configuration.rb +6 -1
- data/lib/mbeditor/rack/pending_migration_bypass.rb +15 -9
- data/lib/mbeditor/route_map.rb +4 -0
- data/lib/mbeditor/ruby_lsp_client.rb +82 -14
- data/lib/mbeditor/version.rb +1 -1
- data/lib/mbeditor.rb +1 -0
- metadata +12 -2
|
@@ -239,12 +239,43 @@
|
|
|
239
239
|
// Same-URI addExtraLib replaces content in place; that is how both layers
|
|
240
240
|
// refresh.
|
|
241
241
|
var PROGRAM_VISIBLE_KINDS = { 'var': 1, 'let': 1, 'const': 1, 'function': 1, 'class': 1 };
|
|
242
|
-
|
|
242
|
+
// workspace-relative path -> { content, lib }, where lib is the addExtraLib
|
|
243
|
+
// disposable or null while the file is open. Presence is what the filter
|
|
244
|
+
// above reads: an open file is still in the program, supplied by its model.
|
|
245
|
+
var programPaths = {};
|
|
243
246
|
|
|
244
247
|
function programUri(path) {
|
|
245
248
|
return 'file:///' + String(path).replace(/^\/+/, '');
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
// An open file reaches the program twice — as its live Monaco model and as
|
|
252
|
+
// this extraLib — and TypeScript reads that as two declarations of everything
|
|
253
|
+
// at its top level, so a lone `const x` in an open .jsx reports TS2451
|
|
254
|
+
// against itself. The model is the edited copy, so it wins and the extraLib
|
|
255
|
+
// stands down until the model is disposed.
|
|
256
|
+
function syncProgramLib(monaco, path) {
|
|
257
|
+
var entry = programPaths[path];
|
|
258
|
+
if (!entry) return;
|
|
259
|
+
var open = monaco.editor.getModels().some(function (m) {
|
|
260
|
+
return m._mbeditorPath === path && !m.isDisposed();
|
|
261
|
+
});
|
|
262
|
+
if (open && entry.lib) {
|
|
263
|
+
entry.lib.dispose();
|
|
264
|
+
entry.lib = null;
|
|
265
|
+
} else if (!open && !entry.lib) {
|
|
266
|
+
entry.lib = monaco.languages.typescript.javascriptDefaults
|
|
267
|
+
.addExtraLib(entry.content, programUri(path));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function setProgramFile(monaco, path, content) {
|
|
272
|
+
var entry = programPaths[path];
|
|
273
|
+
if (!entry) entry = programPaths[path] = { content: content, lib: null };
|
|
274
|
+
entry.content = content;
|
|
275
|
+
if (entry.lib) { entry.lib.dispose(); entry.lib = null; }
|
|
276
|
+
syncProgramLib(monaco, path);
|
|
277
|
+
}
|
|
278
|
+
|
|
248
279
|
function loadWorkspaceProgram(monaco) {
|
|
249
280
|
if (typeof FileService === 'undefined') return;
|
|
250
281
|
var mts = monaco && monaco.languages && monaco.languages.typescript;
|
|
@@ -255,8 +286,7 @@
|
|
|
255
286
|
if (!data || !data.ok || !data.files) return;
|
|
256
287
|
data.files.forEach(function (f) {
|
|
257
288
|
if (!f || typeof f.content !== 'string' || !f.path) return;
|
|
258
|
-
|
|
259
|
-
mts.javascriptDefaults.addExtraLib(f.content, programUri(f.path));
|
|
289
|
+
setProgramFile(monaco, f.path, f.content);
|
|
260
290
|
});
|
|
261
291
|
if (data.skipped && data.skipped.length && window.console) {
|
|
262
292
|
console.info('[mbeditor] ' + data.fileCount + ' source files (' +
|
|
@@ -322,8 +352,7 @@
|
|
|
322
352
|
})).then(function (responses) {
|
|
323
353
|
responses.forEach(function (data) {
|
|
324
354
|
if (!data || !data.ok || !data.file) return;
|
|
325
|
-
|
|
326
|
-
mts.javascriptDefaults.addExtraLib(data.file.content, programUri(data.file.path));
|
|
355
|
+
setProgramFile(monaco, data.file.path, data.file.content);
|
|
327
356
|
});
|
|
328
357
|
});
|
|
329
358
|
}
|
|
@@ -413,6 +442,22 @@
|
|
|
413
442
|
// 6s default: the server's own budget for them is 10s.
|
|
414
443
|
var LSP_SLOW_METHODS = { diagnostics: 15000, formatting: 15000 };
|
|
415
444
|
|
|
445
|
+
// ruby-lsp's wire names are snake_case, the audit legend's are camelCase, and
|
|
446
|
+
// these two differ outright. code() answers 0 ("other") for anything the
|
|
447
|
+
// legend does not list, so a method added later still records.
|
|
448
|
+
function auditLspName(lspMethod) {
|
|
449
|
+
if (lspMethod === 'formatting') return 'format';
|
|
450
|
+
if (lspMethod === 'prepare_rename') return 'rename';
|
|
451
|
+
return lspMethod.replace(/_(\w)/g, function (m, c) { return c.toUpperCase(); });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function recLsp(lspMethod, startedAt, ok) {
|
|
455
|
+
var audit = window.MbeditorAudit;
|
|
456
|
+
if (!audit) return;
|
|
457
|
+
audit.rec(audit.EV.LSP, audit.code('lspMethod', auditLspName(lspMethod)),
|
|
458
|
+
Date.now() - startedAt, ok ? 1 : 0);
|
|
459
|
+
}
|
|
460
|
+
|
|
416
461
|
// `token` is Monaco's CancellationToken, when the calling provider has one.
|
|
417
462
|
// Every provider fires on a gesture the user can abandon — moving the cursor
|
|
418
463
|
// off a word, typing another character — and Monaco cancels the outstanding
|
|
@@ -432,6 +477,7 @@
|
|
|
432
477
|
if (model.getValueLength() === 0) return Promise.resolve(null);
|
|
433
478
|
if (token && token.isCancellationRequested) return Promise.resolve(null);
|
|
434
479
|
|
|
480
|
+
var startedAt = Date.now();
|
|
435
481
|
var config = LSP_SLOW_METHODS[lspMethod] ? { timeout: LSP_SLOW_METHODS[lspMethod] } : null;
|
|
436
482
|
var controller = (token && typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
437
483
|
var cancelSub = null;
|
|
@@ -443,6 +489,7 @@
|
|
|
443
489
|
return FileService.rubyLspRequest(lspMethod, model._mbeditorPath, model.getValue(),
|
|
444
490
|
position.lineNumber, position.column, config, extraBody)
|
|
445
491
|
.then(function (data) {
|
|
492
|
+
recLsp(lspMethod, startedAt, (!data || data.fallback || data.error) ? 0 : 1);
|
|
446
493
|
if (!data || data.fallback || data.error) {
|
|
447
494
|
// A 200 can still carry lspState: 'failed' — the server answered,
|
|
448
495
|
// the language server did not.
|
|
@@ -459,6 +506,9 @@
|
|
|
459
506
|
return null;
|
|
460
507
|
})
|
|
461
508
|
.catch(function (err) {
|
|
509
|
+
// A cancelled request is Monaco changing its mind, not a failure —
|
|
510
|
+
// documentHighlight alone would otherwise log one per cursor move.
|
|
511
|
+
if (!(token && token.isCancellationRequested)) recLsp(lspMethod, startedAt, 0);
|
|
462
512
|
noteLspFailure(err);
|
|
463
513
|
return null;
|
|
464
514
|
})
|
|
@@ -498,6 +548,37 @@
|
|
|
498
548
|
}
|
|
499
549
|
}
|
|
500
550
|
|
|
551
|
+
// True when a hover provider should actually run here: real buffer text,
|
|
552
|
+
// not a `#` comment. Route-hint decorations (EditorPanel.js) render their
|
|
553
|
+
// label via a zero-width decoration's `after.content` — text that is never
|
|
554
|
+
// part of the model — anchored at a column past the real end of the line.
|
|
555
|
+
// Monaco still resolves a mouse position over that rendered text, but the
|
|
556
|
+
// column it reports exceeds getLineMaxColumn, which a position over real
|
|
557
|
+
// content can never do. getWordAtPosition doesn't reject that
|
|
558
|
+
// out-of-range column — it just snaps to the nearest real word — so
|
|
559
|
+
// without this check hovering the decoration silently hovers whatever word
|
|
560
|
+
// precedes it. Comment membership comes from Monaco's own tokenizer
|
|
561
|
+
// (getLineTokens/StandardTokenType) rather than a `#` regex, so a `#`
|
|
562
|
+
// inside a string isn't misread as a comment opener. Exposed so both
|
|
563
|
+
// cases can be asserted directly in system tests.
|
|
564
|
+
function isRealHoverPosition(model, position) {
|
|
565
|
+
if (!model || !position) return false;
|
|
566
|
+
if (position.column > model.getLineMaxColumn(position.lineNumber)) return false;
|
|
567
|
+
// Standard token types: Other 0, Comment 1, String 2, RegEx 3. The enum
|
|
568
|
+
// object is not exported by this Monaco build, and getLineTokens lives on
|
|
569
|
+
// model.tokenization, not the model — both misses were swallowed by the
|
|
570
|
+
// catch below and made the guard a no-op.
|
|
571
|
+
var COMMENT = 1;
|
|
572
|
+
try {
|
|
573
|
+
var tk = model.tokenization || model;
|
|
574
|
+
if (tk.forceTokenization) tk.forceTokenization(position.lineNumber);
|
|
575
|
+
var lineTokens = tk.getLineTokens(position.lineNumber);
|
|
576
|
+
var idx = lineTokens.findTokenIndexAtOffset(position.column - 1);
|
|
577
|
+
if (idx >= 0 && lineTokens.getStandardTokenType(idx) === COMMENT) return false;
|
|
578
|
+
} catch (e) { /* tokenizer not ready — treat as real text */ }
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
501
582
|
// Rails view helpers are defined inside the framework, not the workspace, so
|
|
502
583
|
// looking them up from ERB only ever produces empty round-trips.
|
|
503
584
|
var RAILS_VIEW_HELPERS = {
|
|
@@ -756,14 +837,16 @@
|
|
|
756
837
|
var lineNumber = change.range.startLineNumber;
|
|
757
838
|
var columnBeforeInsert = change.range.startColumn;
|
|
758
839
|
var lineContent = model.getLineContent(lineNumber);
|
|
759
|
-
var textBefore =
|
|
840
|
+
var textBefore = textBackTo(model, lineNumber, columnBeforeInsert - 1);
|
|
760
841
|
|
|
761
842
|
if (/\/$/.test(textBefore)) return false;
|
|
762
843
|
|
|
763
|
-
var
|
|
764
|
-
|
|
844
|
+
var tags = scanTags(textBefore);
|
|
845
|
+
var opener = tags[tags.length - 1];
|
|
846
|
+
if (!opener || opener.closing || opener.closed || opener.depth !== 0) return false;
|
|
847
|
+
if (opener.end !== textBefore.length) return false;
|
|
765
848
|
|
|
766
|
-
var tagName =
|
|
849
|
+
var tagName = opener.name;
|
|
767
850
|
if (VOID_HTML_ELEMENTS[tagName.toLowerCase()]) return false;
|
|
768
851
|
|
|
769
852
|
var closingTag = '</' + tagName + '>';
|
|
@@ -793,6 +876,164 @@
|
|
|
793
876
|
return true;
|
|
794
877
|
}
|
|
795
878
|
|
|
879
|
+
var TAG_SCAN_LINES = 200;
|
|
880
|
+
var TAG_SCAN_LINES_AFTER = 500;
|
|
881
|
+
|
|
882
|
+
// Tags in `text`, brace-aware so JSX attribute expressions like
|
|
883
|
+
// onClick={() => x > y} do not end a tag early.
|
|
884
|
+
function scanTags(text) {
|
|
885
|
+
var tags = [];
|
|
886
|
+
var re = /<(\/?)([A-Za-z][\w:.\-]*)/g;
|
|
887
|
+
var match;
|
|
888
|
+
while ((match = re.exec(text))) {
|
|
889
|
+
var i = re.lastIndex;
|
|
890
|
+
var depth = 0;
|
|
891
|
+
var quote = null;
|
|
892
|
+
var closed = false;
|
|
893
|
+
while (i < text.length) {
|
|
894
|
+
var c = text.charAt(i);
|
|
895
|
+
if (quote) {
|
|
896
|
+
if (c === '\\') { i += 2; continue; }
|
|
897
|
+
if (c === quote) quote = null;
|
|
898
|
+
} else if (c === '"' || c === "'" || c === '`') {
|
|
899
|
+
quote = c;
|
|
900
|
+
} else if (c === '{') {
|
|
901
|
+
depth++;
|
|
902
|
+
} else if (c === '}') {
|
|
903
|
+
if (depth > 0) depth--;
|
|
904
|
+
} else if (depth === 0) {
|
|
905
|
+
if (c === '>') { closed = true; break; }
|
|
906
|
+
if (c === '<') break;
|
|
907
|
+
}
|
|
908
|
+
i++;
|
|
909
|
+
}
|
|
910
|
+
tags.push({
|
|
911
|
+
name: match[2],
|
|
912
|
+
closing: !!match[1],
|
|
913
|
+
closed: closed,
|
|
914
|
+
selfClosing: closed && text.charAt(i - 1) === '/',
|
|
915
|
+
start: match.index,
|
|
916
|
+
end: i,
|
|
917
|
+
depth: depth
|
|
918
|
+
});
|
|
919
|
+
re.lastIndex = closed ? i + 1 : i;
|
|
920
|
+
}
|
|
921
|
+
return tags;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Text from at most TAG_SCAN_LINES lines back up to `endIndex` on `lineNumber`.
|
|
925
|
+
function textBackTo(model, lineNumber, endIndex) {
|
|
926
|
+
var startLine = Math.max(1, lineNumber - TAG_SCAN_LINES);
|
|
927
|
+
return model.getValueInRange(new window.monaco.Range(startLine, 1, lineNumber, endIndex + 1));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Nearest still-open tag before `endIndex` on `lineNumber`, scanning back at
|
|
931
|
+
// most TAG_SCAN_LINES lines. Void and self-closing tags never go on the stack.
|
|
932
|
+
function nearestUnclosedTag(model, lineNumber, endIndex) {
|
|
933
|
+
var stack = [];
|
|
934
|
+
scanTags(textBackTo(model, lineNumber, endIndex)).forEach(function (tag) {
|
|
935
|
+
if (tag.closing) {
|
|
936
|
+
for (var i = stack.length - 1; i >= 0; i--) {
|
|
937
|
+
if (stack[i] === tag.name) { stack.length = i; break; }
|
|
938
|
+
}
|
|
939
|
+
} else if (tag.closed && !tag.selfClosing && !VOID_HTML_ELEMENTS[tag.name.toLowerCase()]) {
|
|
940
|
+
stack.push(tag.name);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
return stack.length ? stack[stack.length - 1] : null;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function tagNameRange(model, base, tag) {
|
|
947
|
+
var offset = base + tag.start + (tag.closing ? 2 : 1);
|
|
948
|
+
var start = model.getPositionAt(offset);
|
|
949
|
+
var end = model.getPositionAt(offset + tag.name.length);
|
|
950
|
+
return new window.monaco.Range(start.lineNumber, start.column, end.lineNumber, end.column);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function linkedEditingRanges(model, position) {
|
|
954
|
+
if (!model || !position) return null;
|
|
955
|
+
if (model.getLanguageId && model.getLanguageId() === 'erb' && isInsideErbTag(model, position)) return null;
|
|
956
|
+
|
|
957
|
+
var startLine = Math.max(1, position.lineNumber - TAG_SCAN_LINES);
|
|
958
|
+
var endLine = Math.min(model.getLineCount(), position.lineNumber + TAG_SCAN_LINES_AFTER);
|
|
959
|
+
var base = model.getOffsetAt({ lineNumber: startLine, column: 1 });
|
|
960
|
+
var text = model.getValueInRange(new window.monaco.Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)));
|
|
961
|
+
var tags = scanTags(text);
|
|
962
|
+
var cursor = model.getOffsetAt(position) - base;
|
|
963
|
+
|
|
964
|
+
var index = -1;
|
|
965
|
+
for (var i = 0; i < tags.length; i++) {
|
|
966
|
+
var nameStart = tags[i].start + (tags[i].closing ? 2 : 1);
|
|
967
|
+
if (cursor >= nameStart && cursor <= nameStart + tags[i].name.length) { index = i; break; }
|
|
968
|
+
}
|
|
969
|
+
if (index < 0) return null;
|
|
970
|
+
|
|
971
|
+
var tag = tags[index];
|
|
972
|
+
if (VOID_HTML_ELEMENTS[tag.name.toLowerCase()]) return null;
|
|
973
|
+
|
|
974
|
+
var match = null;
|
|
975
|
+
var depth = 0;
|
|
976
|
+
var j;
|
|
977
|
+
if (tag.closing) {
|
|
978
|
+
for (j = index - 1; j >= 0; j--) {
|
|
979
|
+
if (tags[j].name !== tag.name) continue;
|
|
980
|
+
if (tags[j].closing) depth++;
|
|
981
|
+
else if (tags[j].closed && !tags[j].selfClosing) {
|
|
982
|
+
if (depth === 0) { match = tags[j]; break; }
|
|
983
|
+
depth--;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
} else {
|
|
987
|
+
if (!tag.closed || tag.selfClosing) return null;
|
|
988
|
+
for (j = index + 1; j < tags.length; j++) {
|
|
989
|
+
if (tags[j].name !== tag.name) continue;
|
|
990
|
+
if (tags[j].closing) {
|
|
991
|
+
if (depth === 0) { match = tags[j]; break; }
|
|
992
|
+
depth--;
|
|
993
|
+
} else if (tags[j].closed && !tags[j].selfClosing) depth++;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (!match) return null;
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
ranges: [
|
|
1000
|
+
tagNameRange(model, base, tag.closing ? match : tag),
|
|
1001
|
+
tagNameRange(model, base, tag.closing ? tag : match)
|
|
1002
|
+
],
|
|
1003
|
+
wordPattern: /[\w:.\-]+/
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function handleClosingTagAutoClose(editor, model, change) {
|
|
1008
|
+
if (change.rangeLength !== 0 || change.text !== '/') return false;
|
|
1009
|
+
|
|
1010
|
+
var lineNumber = change.range.startLineNumber;
|
|
1011
|
+
var slashColumn = change.range.startColumn;
|
|
1012
|
+
var lineContent = model.getLineContent(lineNumber);
|
|
1013
|
+
if (lineContent.charAt(slashColumn - 2) !== '<') return false;
|
|
1014
|
+
|
|
1015
|
+
var tagName = nearestUnclosedTag(model, lineNumber, slashColumn - 2);
|
|
1016
|
+
if (!tagName) return false;
|
|
1017
|
+
|
|
1018
|
+
var insertAt = slashColumn + 1;
|
|
1019
|
+
window.setTimeout(function () {
|
|
1020
|
+
var activeModel = editor.getModel();
|
|
1021
|
+
if (!activeModel || activeModel !== model) return;
|
|
1022
|
+
|
|
1023
|
+
editor.executeEdits('html-auto-close-end', [{
|
|
1024
|
+
range: new window.monaco.Range(lineNumber, insertAt, lineNumber, insertAt),
|
|
1025
|
+
text: tagName + '>'
|
|
1026
|
+
}]);
|
|
1027
|
+
|
|
1028
|
+
window.setTimeout(function () {
|
|
1029
|
+
editor.setPosition({ lineNumber: lineNumber, column: insertAt + tagName.length + 1 });
|
|
1030
|
+
editor.focus();
|
|
1031
|
+
}, 0);
|
|
1032
|
+
}, 0);
|
|
1033
|
+
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
796
1037
|
function attachEditorFeatures(editor, language) {
|
|
797
1038
|
var model = editor && editor.getModel ? editor.getModel() : null;
|
|
798
1039
|
if (!model) {
|
|
@@ -955,12 +1196,15 @@
|
|
|
955
1196
|
|
|
956
1197
|
suppressInternalEdit = true;
|
|
957
1198
|
try {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1199
|
+
var isMarkup = language === 'html' || language === 'javascript' || language === 'typescript' ||
|
|
1200
|
+
(language === 'erb' && !isInsideErbTag(model, {
|
|
1201
|
+
lineNumber: change.range.startLineNumber,
|
|
1202
|
+
column: change.range.startColumn
|
|
1203
|
+
}));
|
|
961
1204
|
|
|
962
|
-
if (
|
|
1205
|
+
if (isMarkup) {
|
|
963
1206
|
handled = handleMarkupAutoClose(editor, model, change) || handled;
|
|
1207
|
+
handled = handleClosingTagAutoClose(editor, model, change) || handled;
|
|
964
1208
|
}
|
|
965
1209
|
} finally {
|
|
966
1210
|
suppressInternalEdit = false;
|
|
@@ -1002,12 +1246,11 @@
|
|
|
1002
1246
|
};
|
|
1003
1247
|
}
|
|
1004
1248
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1249
|
+
// One-time Monaco registration, grouped by provider affinity rather than by
|
|
1250
|
+
// scroll position. Each group is independent — no group's providers read
|
|
1251
|
+
// state another group sets up — so the split only adds seams; nothing here
|
|
1252
|
+
// was a pass-through.
|
|
1253
|
+
function registerJsProviders(monaco) {
|
|
1011
1254
|
// JavaScript: enable semantic checking (off by default in Monaco) and JSX support.
|
|
1012
1255
|
// checkJs catches undefined variables, noUnusedLocals catches dead assignments.
|
|
1013
1256
|
if (monaco.languages.typescript && monaco.languages.typescript.javascriptDefaults) {
|
|
@@ -1176,6 +1419,22 @@
|
|
|
1176
1419
|
// supply, loaded once now.
|
|
1177
1420
|
loadWorkspaceProgram(monaco);
|
|
1178
1421
|
|
|
1422
|
+
// Opening or closing a file moves it between the two halves of the
|
|
1423
|
+
// program — see syncProgramLib. Deferred a tick because _mbeditorPath is
|
|
1424
|
+
// stamped on the model after createModel returns, and because a disposing
|
|
1425
|
+
// model is still live when the event fires.
|
|
1426
|
+
var _syncPending = false;
|
|
1427
|
+
var syncProgram = function () {
|
|
1428
|
+
if (_syncPending) return;
|
|
1429
|
+
_syncPending = true;
|
|
1430
|
+
setTimeout(function () {
|
|
1431
|
+
_syncPending = false;
|
|
1432
|
+
Object.keys(programPaths).forEach(function (p) { syncProgramLib(monaco, p); });
|
|
1433
|
+
}, 0);
|
|
1434
|
+
};
|
|
1435
|
+
monaco.editor.onDidCreateModel(syncProgram);
|
|
1436
|
+
monaco.editor.onWillDisposeModel(syncProgram);
|
|
1437
|
+
|
|
1179
1438
|
// On a change: refresh just the touched files' program entries, and
|
|
1180
1439
|
// re-run the (cheap, cached) globals scan. The whole tree is never
|
|
1181
1440
|
// re-sent — see refreshProgramPaths.
|
|
@@ -1220,16 +1479,29 @@
|
|
|
1220
1479
|
// Downgraded to Warning below and auto-resolved against the workspace,
|
|
1221
1480
|
// since host-app globals are invisible to the language service.
|
|
1222
1481
|
// • 6133 "declared but never read" — a lint. Downgraded to Warning.
|
|
1482
|
+
// • 2451 "Cannot redeclare block-scoped variable" and 2300 "Duplicate
|
|
1483
|
+
// identifier", but only when the file collides with itself — see
|
|
1484
|
+
// isSelfRedeclaration. Two `const`s or two `class`es of one name in
|
|
1485
|
+
// one file is a SyntaxError under every module system, so this is not
|
|
1486
|
+
// a guess about types at all.
|
|
1223
1487
|
// • anything below Error severity — hints and suggestions render faint
|
|
1224
1488
|
// and cost nothing, so they pass through untouched.
|
|
1225
1489
|
//
|
|
1226
|
-
// Deliberately dropped along with the type errors:
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1230
|
-
//
|
|
1231
|
-
// `declare var Foo: any`
|
|
1232
|
-
//
|
|
1490
|
+
// Deliberately dropped along with the type errors: 2403 "Subsequent
|
|
1491
|
+
// variable declarations…", the code TypeScript reaches for when a
|
|
1492
|
+
// declaration collides with a `var`. Every ambient name this module
|
|
1493
|
+
// synthesizes is a `declare var`, so it fires against our own `.d.ts`
|
|
1494
|
+
// files: typing `function Foo()` into an open file collides with the
|
|
1495
|
+
// `declare var Foo: any` that workspace-globals.d.ts still holds until
|
|
1496
|
+
// the next save. That ambient collision reaches 2300 too, which is what
|
|
1497
|
+
// the two-site rule below exists to tell apart — measured on the sample
|
|
1498
|
+
// workspace after the extraLib suppression above, a real duplicate emits
|
|
1499
|
+
// 2300 at both sites and the ambient one emits it at one.
|
|
1500
|
+
//
|
|
1501
|
+
// 2393 "Duplicate function implementation" is kept under the same rule.
|
|
1502
|
+
// Redeclaring a plain `function` is legal JS and TypeScript rightly says
|
|
1503
|
+
// nothing, but it does emit 2393 for a class that declares one method
|
|
1504
|
+
// twice — where the second silently wins and the first is dead code.
|
|
1233
1505
|
//
|
|
1234
1506
|
// .ts/.tsx keeps full checking: there the types are hand-written, so a
|
|
1235
1507
|
// type error is a statement about code the author actually wrote.
|
|
@@ -1253,7 +1525,8 @@
|
|
|
1253
1525
|
// which is a working idiom. That is the arbitrary-inference category
|
|
1254
1526
|
// this filter exists to keep out.
|
|
1255
1527
|
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 };
|
|
1528
|
+
var JS_KEEP_CODES = { '2304': true, '6133': true, '2554': true, '2769': true, '2741': true, '2322': true, '2451': true, '2300': true, '2393': true };
|
|
1529
|
+
var JS_DUP_CODES = { '2451': true, '2300': true, '2393': true };
|
|
1257
1530
|
var JS_SYNTAX_CODE = /^(?:1\d{3}|17\d{3})$/;
|
|
1258
1531
|
var JS_WARN_CODES = { '2304': true, '6133': true, '2554': true, '2769': true, '2741': true, '2322': true };
|
|
1259
1532
|
var TS_WARN_CODES = { '6133': true };
|
|
@@ -1265,9 +1538,12 @@
|
|
|
1265
1538
|
// value of the wrong type flows straight into the component — both of
|
|
1266
1539
|
// those break something, so they read as errors.
|
|
1267
1540
|
//
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1541
|
+
// The code is not a reliable signal, so the distinction comes from the
|
|
1542
|
+
// message chain, which Monaco flattens into the marker. TypeScript
|
|
1543
|
+
// reports the precise 2322/2741 when the component is declared once, but
|
|
1544
|
+
// folds both JSX cases into 2769 ("No overload matches this call") as
|
|
1545
|
+
// soon as it sees two declarations of it — which is what an open file
|
|
1546
|
+
// duplicated into the program as an extraLib used to produce.
|
|
1271
1547
|
var JS_UNKNOWN_PROPERTY = /does not exist on type/;
|
|
1272
1548
|
var JS_MISSING_REQUIRED = /is missing in type .* but required in type/;
|
|
1273
1549
|
function callDiagnosticSeverity(code, message) {
|
|
@@ -1278,10 +1554,41 @@
|
|
|
1278
1554
|
return monaco.MarkerSeverity.Error;
|
|
1279
1555
|
}
|
|
1280
1556
|
|
|
1281
|
-
|
|
1557
|
+
// TypeScript flags every colliding declaration site, so a file that really
|
|
1558
|
+
// does declare a name twice gets one marker per site — identical message,
|
|
1559
|
+
// different position. A lone one means the other declaration is elsewhere:
|
|
1560
|
+
// an ambient `.d.ts` this module synthesizes, or a workspace file that
|
|
1561
|
+
// shares a top-level name and is never loaded alongside this one. Neither
|
|
1562
|
+
// is the author's mistake, and neither is visible any other way — Monaco
|
|
1563
|
+
// drops the relatedInformation naming the other file, because it resolves
|
|
1564
|
+
// to no open model.
|
|
1565
|
+
//
|
|
1566
|
+
// The rule is under-inclusive for `function`, never wrong: redeclaring one
|
|
1567
|
+
// is legal JS, so TypeScript says nothing unless the name ALSO has an
|
|
1568
|
+
// ambient declaration, at which point it reports 2300 at both sites and
|
|
1569
|
+
// this keeps them. `function Foo(){}` twice therefore underlines when Foo
|
|
1570
|
+
// is a workspace global and stays quiet otherwise. Both are real
|
|
1571
|
+
// duplicates; only the quiet half is missed.
|
|
1572
|
+
//
|
|
1573
|
+
// This is also the only thing keeping diff tabs quiet. DiffViewer builds
|
|
1574
|
+
// two more javascript models per diff, and every javascript model joins
|
|
1575
|
+
// the one program, so opening a diff of a file that is also open makes
|
|
1576
|
+
// each side collide with the live model — measured, one raw 2451 on the
|
|
1577
|
+
// live model per diff pane. One occurrence, so the rule drops it. Relaxing
|
|
1578
|
+
// the two-site test, or adding a duplicate code to JS_KEEP_CODES without
|
|
1579
|
+
// adding it to JS_DUP_CODES, lights every diff tab up with false errors.
|
|
1580
|
+
function isSelfRedeclaration(marker, markers) {
|
|
1581
|
+
var code = String(marker.code);
|
|
1582
|
+
return markers.filter(function (m) {
|
|
1583
|
+
return String(m.code) === code && m.message === marker.message;
|
|
1584
|
+
}).length > 1;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function keepJsMarker(marker, markers) {
|
|
1282
1588
|
if (marker.severity !== monaco.MarkerSeverity.Error) return true;
|
|
1283
1589
|
var code = String(marker.code == null ? '' : marker.code);
|
|
1284
|
-
|
|
1590
|
+
if (JS_KEEP_CODES[code] !== true && !JS_SYNTAX_CODE.test(code)) return false;
|
|
1591
|
+
return !JS_DUP_CODES[code] || isSelfRedeclaration(marker, markers);
|
|
1285
1592
|
}
|
|
1286
1593
|
|
|
1287
1594
|
// A 2304 whose span is an assignment TARGET (`foo = 1` with no
|
|
@@ -1326,7 +1633,7 @@
|
|
|
1326
1633
|
var markers = (entry.owner === 'javascript' && jsMarkers && jsMarkers[uri.toString()]) ||
|
|
1327
1634
|
monaco.editor.getModelMarkers({ resource: uri, owner: entry.owner });
|
|
1328
1635
|
var patched = markers.filter(function(m) {
|
|
1329
|
-
return entry.keep ? entry.keep(m) : true;
|
|
1636
|
+
return entry.keep ? entry.keep(m, markers) : true;
|
|
1330
1637
|
}).map(function(m) {
|
|
1331
1638
|
var code = String(m.code);
|
|
1332
1639
|
if (!entry.warn[code]) return m;
|
|
@@ -1444,6 +1751,218 @@
|
|
|
1444
1751
|
});
|
|
1445
1752
|
});
|
|
1446
1753
|
|
|
1754
|
+
// ── ruby-lsp navigation ──────────────────────────────────────────────────
|
|
1755
|
+
|
|
1756
|
+
// Serves exactly one thing: the candidate list navigateToJsWord hands over
|
|
1757
|
+
// when a name is declared at top level in several other files. It is empty
|
|
1758
|
+
// the rest of the time, so the TypeScript worker remains the only voice on
|
|
1759
|
+
// an ordinary jump and a local definition still goes straight there —
|
|
1760
|
+
// Monaco merges providers without deduping, so a second opinion here would
|
|
1761
|
+
// show a picker listing one definition twice.
|
|
1762
|
+
monaco.languages.registerDefinitionProvider('javascript', {
|
|
1763
|
+
provideDefinition: function (model, position) {
|
|
1764
|
+
var pending = pendingJsDefinitionPeek;
|
|
1765
|
+
pendingJsDefinitionPeek = null;
|
|
1766
|
+
if (!pending) return null;
|
|
1767
|
+
if (pending.uri !== model.uri.toString() ||
|
|
1768
|
+
pending.lineNumber !== position.lineNumber ||
|
|
1769
|
+
pending.column !== position.column) return null;
|
|
1770
|
+
return pending.locations;
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
// JS/JSX hover provider: looks up workspace definitions for window globals.
|
|
1775
|
+
// Fires for mixed-case identifiers and for any symbol already in discoveredJsGlobals.
|
|
1776
|
+
var JS_HOVER_CACHE_TTL_MS = 60000;
|
|
1777
|
+
monaco.languages.registerHoverProvider('javascript', {
|
|
1778
|
+
provideHover: function(model, position, token) {
|
|
1779
|
+
var wordInfo = model.getWordAtPosition(position);
|
|
1780
|
+
if (!wordInfo || !wordInfo.word || wordInfo.word.length < 2) return null;
|
|
1781
|
+
var word = wordInfo.word;
|
|
1782
|
+
if (!discoveredJsGlobals[word] && !/[A-Z]/.test(word)) return null;
|
|
1783
|
+
if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return null;
|
|
1784
|
+
|
|
1785
|
+
// Build a position-specific range for this hover instance.
|
|
1786
|
+
// The range must NOT be cached because the same symbol can appear on
|
|
1787
|
+
// different lines; a stale cached lineNumber would highlight the wrong place.
|
|
1788
|
+
function makeHoverRange() {
|
|
1789
|
+
return new monaco.Range(position.lineNumber, wordInfo.startColumn, position.lineNumber, wordInfo.endColumn);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
var parentCtx = extractJsParentContext(model, position.lineNumber, wordInfo);
|
|
1793
|
+
// Parent-qualified hovers cache separately: Parent.myFunction and a
|
|
1794
|
+
// bare myFunction can resolve to different definitions.
|
|
1795
|
+
var cacheKey = (parentCtx ? parentCtx + '.' : '') + word;
|
|
1796
|
+
var cached = jsHoverCache[cacheKey];
|
|
1797
|
+
if (cached && (Date.now() - cached.ts) < JS_HOVER_CACHE_TTL_MS) {
|
|
1798
|
+
if (!cached.contents) return null;
|
|
1799
|
+
return { range: makeHoverRange(), contents: cached.contents };
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1803
|
+
if (controller && token && token.onCancellationRequested) {
|
|
1804
|
+
token.onCancellationRequested(function() { controller.abort(); });
|
|
1805
|
+
}
|
|
1806
|
+
return FileService.getJsDefinition(word, controller ? { signal: controller.signal } : {}, parentCtx)
|
|
1807
|
+
.then(function(data) {
|
|
1808
|
+
if (token && token.isCancellationRequested) return null;
|
|
1809
|
+
var results = data && data.results;
|
|
1810
|
+
if (!results || !results.length) {
|
|
1811
|
+
if (isRuntimeWindowGlobal(word)) {
|
|
1812
|
+
addDiscoveredGlobal(word);
|
|
1813
|
+
var kind = typeof window[word];
|
|
1814
|
+
var rtContents = [{ value: '**' + word + '** — runtime global (`' + kind + '`)' }];
|
|
1815
|
+
jsHoverCache[cacheKey] = { ts: Date.now(), contents: rtContents };
|
|
1816
|
+
return { range: makeHoverRange(), contents: rtContents };
|
|
1817
|
+
}
|
|
1818
|
+
jsHoverCache[cacheKey] = { ts: Date.now(), contents: null };
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
// Same preference as Ctrl+click, so the file the hover names is
|
|
1822
|
+
// the file the jump would take you to.
|
|
1823
|
+
var r = preferCurrentFile(results, model._mbeditorPath) || results[0];
|
|
1824
|
+
// Only declare as global when the definition is a top-level
|
|
1825
|
+
// (Sprockets-global) declaration in a different file — nested and
|
|
1826
|
+
// member definitions must not get a duplicate declare var.
|
|
1827
|
+
if (r.topLevel && r.file !== model._mbeditorPath) addDiscoveredGlobal(word);
|
|
1828
|
+
var fileRef = r.file + ':' + r.line;
|
|
1829
|
+
var contents = [
|
|
1830
|
+
{ value: '```javascript\n' + r.snippet + '\n```', isTrusted: true },
|
|
1831
|
+
{ value: '<span style="opacity:0.55;font-size:0.9em;">' + fileRef + '</span>', isTrusted: true, supportHtml: true }
|
|
1832
|
+
];
|
|
1833
|
+
jsHoverCache[cacheKey] = { ts: Date.now(), contents: contents };
|
|
1834
|
+
return { range: makeHoverRange(), contents: contents };
|
|
1835
|
+
}).catch(function() { return null; });
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
// JS/JSX member completion provider: suggests properties/methods of workspace globals after '.'.
|
|
1840
|
+
// Only looks up PascalCase/mixed-case identifiers or previously discovered globals.
|
|
1841
|
+
var JS_MEMBERS_CACHE_TTL_MS = 60000;
|
|
1842
|
+
monaco.languages.registerCompletionItemProvider('javascript', {
|
|
1843
|
+
triggerCharacters: ['.'],
|
|
1844
|
+
provideCompletionItems: function(model, position) {
|
|
1845
|
+
var line = model.getLineContent(position.lineNumber);
|
|
1846
|
+
var col = position.column - 2; // index of character just before the '.'
|
|
1847
|
+
var end = col;
|
|
1848
|
+
while (col >= 0 && /[a-zA-Z0-9_$]/.test(line[col])) col--;
|
|
1849
|
+
var symbol = line.slice(col + 1, end + 1);
|
|
1850
|
+
if (!symbol || symbol.length < 2) return { suggestions: [] };
|
|
1851
|
+
if (!discoveredJsGlobals[symbol] && !/^[A-Z]/.test(symbol)) return { suggestions: [] };
|
|
1852
|
+
if (typeof FileService === 'undefined' || !FileService.getJsMembers) return { suggestions: [] };
|
|
1853
|
+
|
|
1854
|
+
// Only the raw member list is cached. Suggestions carry the insert
|
|
1855
|
+
// `range`, which is where the completion is about to be written — the
|
|
1856
|
+
// same trap the hover provider's makeHoverRange avoids. Cached
|
|
1857
|
+
// suggestions inserted at the position of the invocation that filled
|
|
1858
|
+
// the cache, so a hit anywhere else in the file was misapplied or
|
|
1859
|
+
// silently dropped by Monaco.
|
|
1860
|
+
function buildSuggestions(members) {
|
|
1861
|
+
return members.map(function(m) {
|
|
1862
|
+
return {
|
|
1863
|
+
label: m.name,
|
|
1864
|
+
kind: monaco.languages.CompletionItemKind.Method,
|
|
1865
|
+
detail: symbol,
|
|
1866
|
+
documentation: m.snippet,
|
|
1867
|
+
insertText: m.name,
|
|
1868
|
+
range: {
|
|
1869
|
+
startLineNumber: position.lineNumber, endLineNumber: position.lineNumber,
|
|
1870
|
+
startColumn: position.column, endColumn: position.column
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
var cached = jsMembersCache[symbol];
|
|
1877
|
+
if (cached && (Date.now() - cached.ts) < JS_MEMBERS_CACHE_TTL_MS) {
|
|
1878
|
+
return { suggestions: buildSuggestions(cached.members) };
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
return FileService.getJsMembers(symbol)
|
|
1882
|
+
.then(function(data) {
|
|
1883
|
+
var members = (data && data.members) || [];
|
|
1884
|
+
jsMembersCache[symbol] = { ts: Date.now(), members: members };
|
|
1885
|
+
return { suggestions: buildSuggestions(members) };
|
|
1886
|
+
}).catch(function() { return { suggestions: [] }; });
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
|
|
1890
|
+
monaco.languages.registerCompletionItemProvider('javascript', jsxPropsProvider);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// JSX attribute completion for components whose props are read as `props.x` /
|
|
1894
|
+
// `this.props.x`, which the TS worker types as `any` and so completes nothing
|
|
1895
|
+
// for. Destructured params are deliberately not scanned: the TS worker already
|
|
1896
|
+
// completes those, and a second copy would double every row.
|
|
1897
|
+
// Component name → prop-name list.
|
|
1898
|
+
var jsxPropsProvider = {
|
|
1899
|
+
triggerCharacters: [' '],
|
|
1900
|
+
provideCompletionItems: function(model, position) {
|
|
1901
|
+
var monaco = window.monaco;
|
|
1902
|
+
var before = model.getValueInRange({
|
|
1903
|
+
startLineNumber: position.lineNumber, startColumn: 1,
|
|
1904
|
+
endLineNumber: position.lineNumber, endColumn: position.column
|
|
1905
|
+
});
|
|
1906
|
+
var tag = before.match(/<([A-Z][\w$]*)(?:\s+[^<>]*)?\s+[\w-]*$/);
|
|
1907
|
+
if (!tag) return { suggestions: [] };
|
|
1908
|
+
|
|
1909
|
+
var used = {};
|
|
1910
|
+
(before.slice(tag.index).match(/[\w-]+(?=\s*=)/g) || []).forEach(function(a) { used[a] = 1; });
|
|
1911
|
+
|
|
1912
|
+
var word = model.getWordUntilPosition(position);
|
|
1913
|
+
var range = {
|
|
1914
|
+
startLineNumber: position.lineNumber, endLineNumber: position.lineNumber,
|
|
1915
|
+
startColumn: word.startColumn, endColumn: word.endColumn
|
|
1916
|
+
};
|
|
1917
|
+
return {
|
|
1918
|
+
suggestions: collectComponentProps(monaco, model, tag[1])
|
|
1919
|
+
.filter(function(p) { return !used[p]; })
|
|
1920
|
+
.map(function(p) {
|
|
1921
|
+
return {
|
|
1922
|
+
label: p,
|
|
1923
|
+
kind: monaco.languages.CompletionItemKind.Property,
|
|
1924
|
+
insertText: p,
|
|
1925
|
+
range: range
|
|
1926
|
+
};
|
|
1927
|
+
})
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
|
|
1932
|
+
function collectComponentProps(monaco, model, name) {
|
|
1933
|
+
var sources = [model.getValue()];
|
|
1934
|
+
try {
|
|
1935
|
+
var libs = monaco.languages.typescript.javascriptDefaults.getExtraLibs();
|
|
1936
|
+
Object.keys(libs).forEach(function(uri) {
|
|
1937
|
+
if (libs[uri] && libs[uri].content) sources.push(libs[uri].content);
|
|
1938
|
+
});
|
|
1939
|
+
} catch (e) { /* extraLibs unavailable */ }
|
|
1940
|
+
monaco.editor.getModels().forEach(function(m) { if (m !== model) sources.push(m.getValue()); });
|
|
1941
|
+
|
|
1942
|
+
var decl = new RegExp('(?:function\\s+' + name + '\\s*\\(|(?:const|let|var)\\s+' + name + '\\s*=|class\\s+' + name + '\\b)');
|
|
1943
|
+
var names = [];
|
|
1944
|
+
var seen = {};
|
|
1945
|
+
function add(candidate) {
|
|
1946
|
+
if (/^[A-Za-z_$][\w$]*$/.test(candidate) && !seen[candidate]) { seen[candidate] = 1; names.push(candidate); }
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
for (var i = 0; i < sources.length; i++) {
|
|
1950
|
+
var found = decl.exec(sources[i]);
|
|
1951
|
+
if (!found) continue;
|
|
1952
|
+
|
|
1953
|
+
var body = sources[i].slice(found.index);
|
|
1954
|
+
var end = body.slice(1).search(/\n(?:function |class |const |let |var )/);
|
|
1955
|
+
if (end > 0) body = body.slice(0, end + 1);
|
|
1956
|
+
|
|
1957
|
+
var re = /\b(?:this\.)?props\.([A-Za-z_$][\w$]*)/g;
|
|
1958
|
+
var read;
|
|
1959
|
+
while ((read = re.exec(body))) add(read[1]);
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
return names;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function registerRubyProviders(monaco) {
|
|
1447
1966
|
monaco.languages.setLanguageConfiguration('ruby', {
|
|
1448
1967
|
comments: { lineComment: '#', blockComment: ['=begin', '=end'] },
|
|
1449
1968
|
brackets: [['(', ')'], ['{', '}'], ['[', ']']],
|
|
@@ -1727,49 +2246,6 @@
|
|
|
1727
2246
|
}
|
|
1728
2247
|
});
|
|
1729
2248
|
|
|
1730
|
-
var genericLinkedProvider = {
|
|
1731
|
-
provideLinkedEditingRanges: function provideLinkedEditingRanges(model, position) {
|
|
1732
|
-
var line = model.getLineContent(position.lineNumber);
|
|
1733
|
-
var wordInfo = model.getWordAtPosition(position);
|
|
1734
|
-
if (!wordInfo) return null;
|
|
1735
|
-
|
|
1736
|
-
var word = wordInfo.word;
|
|
1737
|
-
var startCol = wordInfo.startColumn;
|
|
1738
|
-
var endCol = wordInfo.endColumn;
|
|
1739
|
-
|
|
1740
|
-
if (line[startCol - 2] === '<') {
|
|
1741
|
-
var closeTagStr = '</' + word + '>';
|
|
1742
|
-
var closeIdx = line.indexOf(closeTagStr, endCol - 1);
|
|
1743
|
-
if (closeIdx !== -1) {
|
|
1744
|
-
return {
|
|
1745
|
-
ranges: [new monaco.Range(position.lineNumber, startCol, position.lineNumber, endCol), new monaco.Range(position.lineNumber, closeIdx + 3, position.lineNumber, closeIdx + 3 + word.length)],
|
|
1746
|
-
wordPattern: /[a-zA-Z0-9:\-_]+/
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
if (line[startCol - 3] === '<' && line[startCol - 2] === '/') {
|
|
1752
|
-
var openTagRegex = new RegExp('<' + word + '(?:\\s|>)');
|
|
1753
|
-
var match = line.match(openTagRegex);
|
|
1754
|
-
if (match) {
|
|
1755
|
-
var openStart = match.index + 2;
|
|
1756
|
-
if (openStart < startCol) {
|
|
1757
|
-
return {
|
|
1758
|
-
ranges: [new monaco.Range(position.lineNumber, openStart, position.lineNumber, openStart + word.length), new monaco.Range(position.lineNumber, startCol, position.lineNumber, endCol)],
|
|
1759
|
-
wordPattern: /[a-zA-Z0-9:\-_]+/
|
|
1760
|
-
};
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
return null;
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
1768
|
-
|
|
1769
|
-
monaco.languages.registerLinkedEditingRangeProvider('javascript', genericLinkedProvider);
|
|
1770
|
-
monaco.languages.registerLinkedEditingRangeProvider('typescript', genericLinkedProvider);
|
|
1771
|
-
monaco.languages.registerLinkedEditingRangeProvider('ruby', genericLinkedProvider);
|
|
1772
|
-
|
|
1773
2249
|
// RuboCop quick-fix code-action provider for Ruby files.
|
|
1774
2250
|
// Only registers when RuboCop is available in the workspace.
|
|
1775
2251
|
//
|
|
@@ -1875,52 +2351,6 @@
|
|
|
1875
2351
|
TabManager.openTab(path, String(path).split('/').pop(), line || 1);
|
|
1876
2352
|
});
|
|
1877
2353
|
|
|
1878
|
-
// ── ruby-lsp navigation ──────────────────────────────────────────────────
|
|
1879
|
-
|
|
1880
|
-
// Serves exactly one thing: the candidate list navigateToJsWord hands over
|
|
1881
|
-
// when a name is declared at top level in several other files. It is empty
|
|
1882
|
-
// the rest of the time, so the TypeScript worker remains the only voice on
|
|
1883
|
-
// an ordinary jump and a local definition still goes straight there —
|
|
1884
|
-
// Monaco merges providers without deduping, so a second opinion here would
|
|
1885
|
-
// show a picker listing one definition twice.
|
|
1886
|
-
monaco.languages.registerDefinitionProvider('javascript', {
|
|
1887
|
-
provideDefinition: function (model, position) {
|
|
1888
|
-
var pending = pendingJsDefinitionPeek;
|
|
1889
|
-
pendingJsDefinitionPeek = null;
|
|
1890
|
-
if (!pending) return null;
|
|
1891
|
-
if (pending.uri !== model.uri.toString() ||
|
|
1892
|
-
pending.lineNumber !== position.lineNumber ||
|
|
1893
|
-
pending.column !== position.column) return null;
|
|
1894
|
-
return pending.locations;
|
|
1895
|
-
}
|
|
1896
|
-
});
|
|
1897
|
-
|
|
1898
|
-
// Teaches Monaco how to open a file:// resource in this editor. Without it
|
|
1899
|
-
// every provider below can find a location but nothing can go there:
|
|
1900
|
-
// peek-definition, the references widget and Ctrl+hover previews all route
|
|
1901
|
-
// their "open this" through here.
|
|
1902
|
-
monaco.editor.registerEditorOpener({
|
|
1903
|
-
openCodeEditor: function (_source, resource, selectionOrPosition) {
|
|
1904
|
-
// Only file:// resources name a workspace file. Models are created
|
|
1905
|
-
// without an explicit URI, so Monaco gives each one an
|
|
1906
|
-
// `inmemory://model/N` identity — and the TS worker returns exactly
|
|
1907
|
-
// that when a JS definition resolves inside the file you are already
|
|
1908
|
-
// in. Stripping it to a path opened a phantom tab called "57".
|
|
1909
|
-
// Handing those back to Monaco lets it reveal the position in the
|
|
1910
|
-
// current editor, which is what the gesture meant.
|
|
1911
|
-
if (String(resource.scheme || '') !== 'file') return false;
|
|
1912
|
-
|
|
1913
|
-
var path = String(resource.path || '').replace(/^\/+/, '');
|
|
1914
|
-
if (!path || typeof TabManager === 'undefined' || !TabManager.openTab) return false;
|
|
1915
|
-
|
|
1916
|
-
var pos = selectionOrPosition || {};
|
|
1917
|
-
var line = pos.startLineNumber || pos.lineNumber || 1;
|
|
1918
|
-
var col = pos.startColumn || pos.column || 1;
|
|
1919
|
-
TabManager.openTab(path, path.split('/').pop(), line, null, false, col);
|
|
1920
|
-
return true;
|
|
1921
|
-
}
|
|
1922
|
-
});
|
|
1923
|
-
|
|
1924
2354
|
// Words never worth a definition lookup: language keywords, core methods,
|
|
1925
2355
|
// and (in ERB) Rails view helpers that live in the framework rather than
|
|
1926
2356
|
// the workspace. Guarding here rather than in the request means Ctrl+hover
|
|
@@ -1942,11 +2372,24 @@
|
|
|
1942
2372
|
var isErb = languageId === 'erb';
|
|
1943
2373
|
|
|
1944
2374
|
monaco.languages.registerDefinitionProvider(languageId, {
|
|
1945
|
-
provideDefinition: function provideDefinition(model, position) {
|
|
2375
|
+
provideDefinition: function provideDefinition(model, position, token) {
|
|
1946
2376
|
var word = rubyNavigableWord(model, position, isErb);
|
|
1947
2377
|
if (!word) return null;
|
|
1948
2378
|
|
|
1949
|
-
|
|
2379
|
+
// Passing token lets tryRubyLsp abort the outstanding request (see
|
|
2380
|
+
// its own comment) when this ctrl-click is abandoned — a second
|
|
2381
|
+
// click before the first answers, most obviously — instead of
|
|
2382
|
+
// leaving an abandoned axios request to run to completion unread.
|
|
2383
|
+
var lsp = isErb ? Promise.resolve(null) : tryRubyLsp('definition', model, position, null, token);
|
|
2384
|
+
// Started alongside ruby-lsp, not inside its .then(): when ruby-lsp
|
|
2385
|
+
// is slow or times out (server-side budget is 3s by default — see
|
|
2386
|
+
// Mbeditor.configuration.ruby_lsp_timeout), waiting for it to
|
|
2387
|
+
// settle before even starting the Ripper-backed fallback tacked a
|
|
2388
|
+
// full extra round trip onto an already-slow answer. The result is
|
|
2389
|
+
// only used when ruby-lsp comes back empty, so a fast ruby-lsp
|
|
2390
|
+
// answer is unaffected — this promise is simply left to resolve
|
|
2391
|
+
// unused.
|
|
2392
|
+
var legacy = legacyRubyDefinition(word);
|
|
1950
2393
|
return lsp.then(function (data) {
|
|
1951
2394
|
if (data && data.results && data.results.length) {
|
|
1952
2395
|
return data.results.map(function (r) {
|
|
@@ -1957,7 +2400,7 @@
|
|
|
1957
2400
|
};
|
|
1958
2401
|
});
|
|
1959
2402
|
}
|
|
1960
|
-
return
|
|
2403
|
+
return legacy;
|
|
1961
2404
|
});
|
|
1962
2405
|
}
|
|
1963
2406
|
});
|
|
@@ -2108,82 +2551,6 @@
|
|
|
2108
2551
|
}).catch(function () { return []; });
|
|
2109
2552
|
}
|
|
2110
2553
|
|
|
2111
|
-
// ── Prettier formatting providers ────────────────────────────────────────
|
|
2112
|
-
//
|
|
2113
|
-
// `formatOnPaste` has been on by default for a long time and did nothing
|
|
2114
|
-
// outside Ruby: Monaco acts on a paste only through a *range* formatting
|
|
2115
|
-
// provider, and none was registered. Registering one here is what makes
|
|
2116
|
-
// pasted blocks land correctly indented, and what formats the whole thing
|
|
2117
|
-
// when the paste fills an empty file (the pasted range is then the file).
|
|
2118
|
-
//
|
|
2119
|
-
// Prettier's own rangeStart/rangeEnd does the narrowing — it reprints the
|
|
2120
|
-
// smallest enclosing statement and leaves the rest byte-identical, so a
|
|
2121
|
-
// paste in the middle of a file does not reformat the file.
|
|
2122
|
-
var PRETTIER_LANGUAGE_PARSERS = {
|
|
2123
|
-
javascript: 'babel', json: 'json', css: 'css',
|
|
2124
|
-
scss: 'scss', less: 'less', html: 'html', markdown: 'markdown'
|
|
2125
|
-
};
|
|
2126
|
-
|
|
2127
|
-
// One edit spanning only what actually changed. A whole-document
|
|
2128
|
-
// replacement would work but throws away the cursor position and collapses
|
|
2129
|
-
// undo, which on every paste is very noticeable.
|
|
2130
|
-
function minimalEdit(model, oldText, newText) {
|
|
2131
|
-
if (oldText === newText) return [];
|
|
2132
|
-
var start = 0;
|
|
2133
|
-
var maxStart = Math.min(oldText.length, newText.length);
|
|
2134
|
-
while (start < maxStart && oldText.charCodeAt(start) === newText.charCodeAt(start)) start++;
|
|
2135
|
-
var oldEnd = oldText.length;
|
|
2136
|
-
var newEnd = newText.length;
|
|
2137
|
-
while (oldEnd > start && newEnd > start && oldText.charCodeAt(oldEnd - 1) === newText.charCodeAt(newEnd - 1)) {
|
|
2138
|
-
oldEnd--;
|
|
2139
|
-
newEnd--;
|
|
2140
|
-
}
|
|
2141
|
-
return [{
|
|
2142
|
-
range: monaco.Range.fromPositions(model.getPositionAt(start), model.getPositionAt(oldEnd)),
|
|
2143
|
-
text: newText.slice(start, newEnd)
|
|
2144
|
-
}];
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
function prettierEdits(model, range) {
|
|
2148
|
-
var parser = PRETTIER_LANGUAGE_PARSERS[model.getLanguageId()];
|
|
2149
|
-
if (!parser || typeof runPrettier !== 'function') return Promise.resolve([]);
|
|
2150
|
-
|
|
2151
|
-
var text = model.getValue();
|
|
2152
|
-
var prefs = (typeof EditorStore !== 'undefined' && EditorStore.getState().editorPrefs) || {};
|
|
2153
|
-
var extra = null;
|
|
2154
|
-
if (range) {
|
|
2155
|
-
extra = {
|
|
2156
|
-
rangeStart: model.getOffsetAt({ lineNumber: range.startLineNumber, column: range.startColumn }),
|
|
2157
|
-
rangeEnd: model.getOffsetAt({ lineNumber: range.endLineNumber, column: range.endColumn })
|
|
2158
|
-
};
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
return runPrettier(text, prefs, parser, extra)
|
|
2162
|
-
.then(function (formatted) {
|
|
2163
|
-
// The model can have moved on while Prettier was working.
|
|
2164
|
-
if (model.isDisposed() || model.getValue() !== text) return [];
|
|
2165
|
-
return minimalEdit(model, text, formatted);
|
|
2166
|
-
})
|
|
2167
|
-
// A paste of half an expression will not parse. Leaving it alone is the
|
|
2168
|
-
// right answer — the diagnostics path reports the syntax error.
|
|
2169
|
-
["catch"](function () { return []; });
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
Object.keys(PRETTIER_LANGUAGE_PARSERS).forEach(function (languageId) {
|
|
2173
|
-
monaco.languages.registerDocumentRangeFormattingEditProvider(languageId, {
|
|
2174
|
-
displayName: 'Prettier',
|
|
2175
|
-
provideDocumentRangeFormattingEdits: function (model, range) {
|
|
2176
|
-
return prettierEdits(model, range);
|
|
2177
|
-
}
|
|
2178
|
-
});
|
|
2179
|
-
monaco.languages.registerDocumentFormattingEditProvider(languageId, {
|
|
2180
|
-
displayName: 'Prettier',
|
|
2181
|
-
provideDocumentFormattingEdits: function (model) {
|
|
2182
|
-
return prettierEdits(model, null);
|
|
2183
|
-
}
|
|
2184
|
-
});
|
|
2185
|
-
});
|
|
2186
|
-
|
|
2187
2554
|
// Parameter hints while typing a call's arguments.
|
|
2188
2555
|
monaco.languages.registerSignatureHelpProvider('ruby', {
|
|
2189
2556
|
signatureHelpTriggerCharacters: ['(', ','],
|
|
@@ -2380,6 +2747,7 @@
|
|
|
2380
2747
|
provideHover: function provideHover(model, position, token) {
|
|
2381
2748
|
var isErb = lang === 'erb';
|
|
2382
2749
|
if (isErb && !isInsideErbTag(model, position)) return null;
|
|
2750
|
+
if (!isRealHoverPosition(model, position)) return null;
|
|
2383
2751
|
|
|
2384
2752
|
var wordInfo = model.getWordAtPosition(position);
|
|
2385
2753
|
if (!wordInfo) return null;
|
|
@@ -2634,121 +3002,117 @@
|
|
|
2634
3002
|
}).catch(function() { return { suggestions: [] }; });
|
|
2635
3003
|
}
|
|
2636
3004
|
}
|
|
3005
|
+
}
|
|
2637
3006
|
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
if (!discoveredJsGlobals[word] && !/[A-Z]/.test(word)) return null;
|
|
2647
|
-
if (typeof FileService === 'undefined' || !FileService.getJsDefinition) return null;
|
|
3007
|
+
// Cross-language providers: features registered once for several languages
|
|
3008
|
+
// at once (linked editing, the file:// opener, Prettier formatting, vim fold
|
|
3009
|
+
// markers) rather than owned by Ruby or JS specifically.
|
|
3010
|
+
function registerGenericProviders(monaco) {
|
|
3011
|
+
var linkedProvider = { provideLinkedEditingRanges: linkedEditingRanges };
|
|
3012
|
+
['html', 'erb', 'javascript', 'typescript', 'ruby'].forEach(function (lang) {
|
|
3013
|
+
monaco.languages.registerLinkedEditingRangeProvider(lang, linkedProvider);
|
|
3014
|
+
});
|
|
2648
3015
|
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
3016
|
+
// Teaches Monaco how to open a file:// resource in this editor. Without it
|
|
3017
|
+
// every provider below can find a location but nothing can go there:
|
|
3018
|
+
// peek-definition, the references widget and Ctrl+hover previews all route
|
|
3019
|
+
// their "open this" through here.
|
|
3020
|
+
monaco.editor.registerEditorOpener({
|
|
3021
|
+
openCodeEditor: function (_source, resource, selectionOrPosition) {
|
|
3022
|
+
// Only file:// resources name a workspace file. Models are created
|
|
3023
|
+
// without an explicit URI, so Monaco gives each one an
|
|
3024
|
+
// `inmemory://model/N` identity — and the TS worker returns exactly
|
|
3025
|
+
// that when a JS definition resolves inside the file you are already
|
|
3026
|
+
// in. Stripping it to a path opened a phantom tab called "57".
|
|
3027
|
+
// Handing those back to Monaco lets it reveal the position in the
|
|
3028
|
+
// current editor, which is what the gesture meant.
|
|
3029
|
+
if (String(resource.scheme || '') !== 'file') return false;
|
|
2655
3030
|
|
|
2656
|
-
var
|
|
2657
|
-
|
|
2658
|
-
// bare myFunction can resolve to different definitions.
|
|
2659
|
-
var cacheKey = (parentCtx ? parentCtx + '.' : '') + word;
|
|
2660
|
-
var cached = jsHoverCache[cacheKey];
|
|
2661
|
-
if (cached && (Date.now() - cached.ts) < JS_HOVER_CACHE_TTL_MS) {
|
|
2662
|
-
if (!cached.contents) return null;
|
|
2663
|
-
return { range: makeHoverRange(), contents: cached.contents };
|
|
2664
|
-
}
|
|
3031
|
+
var path = String(resource.path || '').replace(/^\/+/, '');
|
|
3032
|
+
if (!path || typeof TabManager === 'undefined' || !TabManager.openTab) return false;
|
|
2665
3033
|
|
|
2666
|
-
var
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
return
|
|
2671
|
-
.then(function(data) {
|
|
2672
|
-
if (token && token.isCancellationRequested) return null;
|
|
2673
|
-
var results = data && data.results;
|
|
2674
|
-
if (!results || !results.length) {
|
|
2675
|
-
if (isRuntimeWindowGlobal(word)) {
|
|
2676
|
-
addDiscoveredGlobal(word);
|
|
2677
|
-
var kind = typeof window[word];
|
|
2678
|
-
var rtContents = [{ value: '**' + word + '** — runtime global (`' + kind + '`)' }];
|
|
2679
|
-
jsHoverCache[cacheKey] = { ts: Date.now(), contents: rtContents };
|
|
2680
|
-
return { range: makeHoverRange(), contents: rtContents };
|
|
2681
|
-
}
|
|
2682
|
-
jsHoverCache[cacheKey] = { ts: Date.now(), contents: null };
|
|
2683
|
-
return null;
|
|
2684
|
-
}
|
|
2685
|
-
// Same preference as Ctrl+click, so the file the hover names is
|
|
2686
|
-
// the file the jump would take you to.
|
|
2687
|
-
var r = preferCurrentFile(results, model._mbeditorPath) || results[0];
|
|
2688
|
-
// Only declare as global when the definition is a top-level
|
|
2689
|
-
// (Sprockets-global) declaration in a different file — nested and
|
|
2690
|
-
// member definitions must not get a duplicate declare var.
|
|
2691
|
-
if (r.topLevel && r.file !== model._mbeditorPath) addDiscoveredGlobal(word);
|
|
2692
|
-
var fileRef = r.file + ':' + r.line;
|
|
2693
|
-
var contents = [
|
|
2694
|
-
{ value: '```javascript\n' + r.snippet + '\n```', isTrusted: true },
|
|
2695
|
-
{ value: '<span style="opacity:0.55;font-size:0.9em;">' + fileRef + '</span>', isTrusted: true, supportHtml: true }
|
|
2696
|
-
];
|
|
2697
|
-
jsHoverCache[cacheKey] = { ts: Date.now(), contents: contents };
|
|
2698
|
-
return { range: makeHoverRange(), contents: contents };
|
|
2699
|
-
}).catch(function() { return null; });
|
|
3034
|
+
var pos = selectionOrPosition || {};
|
|
3035
|
+
var line = pos.startLineNumber || pos.lineNumber || 1;
|
|
3036
|
+
var col = pos.startColumn || pos.column || 1;
|
|
3037
|
+
TabManager.openTab(path, path.split('/').pop(), line, null, false, col);
|
|
3038
|
+
return true;
|
|
2700
3039
|
}
|
|
2701
3040
|
});
|
|
2702
3041
|
|
|
2703
|
-
//
|
|
2704
|
-
//
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
3042
|
+
// ── Prettier formatting providers ────────────────────────────────────────
|
|
3043
|
+
//
|
|
3044
|
+
// `formatOnPaste` has been on by default for a long time and did nothing
|
|
3045
|
+
// outside Ruby: Monaco acts on a paste only through a *range* formatting
|
|
3046
|
+
// provider, and none was registered. Registering one here is what makes
|
|
3047
|
+
// pasted blocks land correctly indented, and what formats the whole thing
|
|
3048
|
+
// when the paste fills an empty file (the pasted range is then the file).
|
|
3049
|
+
//
|
|
3050
|
+
// Prettier's own rangeStart/rangeEnd does the narrowing — it reprints the
|
|
3051
|
+
// smallest enclosing statement and leaves the rest byte-identical, so a
|
|
3052
|
+
// paste in the middle of a file does not reformat the file.
|
|
3053
|
+
var PRETTIER_LANGUAGE_PARSERS = {
|
|
3054
|
+
javascript: 'babel', json: 'json', css: 'css',
|
|
3055
|
+
scss: 'scss', less: 'less', html: 'html', markdown: 'markdown'
|
|
3056
|
+
};
|
|
2717
3057
|
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
});
|
|
2738
|
-
}
|
|
3058
|
+
// One edit spanning only what actually changed. A whole-document
|
|
3059
|
+
// replacement would work but throws away the cursor position and collapses
|
|
3060
|
+
// undo, which on every paste is very noticeable.
|
|
3061
|
+
function minimalEdit(model, oldText, newText) {
|
|
3062
|
+
if (oldText === newText) return [];
|
|
3063
|
+
var start = 0;
|
|
3064
|
+
var maxStart = Math.min(oldText.length, newText.length);
|
|
3065
|
+
while (start < maxStart && oldText.charCodeAt(start) === newText.charCodeAt(start)) start++;
|
|
3066
|
+
var oldEnd = oldText.length;
|
|
3067
|
+
var newEnd = newText.length;
|
|
3068
|
+
while (oldEnd > start && newEnd > start && oldText.charCodeAt(oldEnd - 1) === newText.charCodeAt(newEnd - 1)) {
|
|
3069
|
+
oldEnd--;
|
|
3070
|
+
newEnd--;
|
|
3071
|
+
}
|
|
3072
|
+
return [{
|
|
3073
|
+
range: monaco.Range.fromPositions(model.getPositionAt(start), model.getPositionAt(oldEnd)),
|
|
3074
|
+
text: newText.slice(start, newEnd)
|
|
3075
|
+
}];
|
|
3076
|
+
}
|
|
2739
3077
|
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
}
|
|
3078
|
+
function prettierEdits(model, range) {
|
|
3079
|
+
var parser = PRETTIER_LANGUAGE_PARSERS[model.getLanguageId()];
|
|
3080
|
+
if (!parser || typeof runPrettier !== 'function') return Promise.resolve([]);
|
|
2744
3081
|
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
3082
|
+
var text = model.getValue();
|
|
3083
|
+
var prefs = (typeof EditorStore !== 'undefined' && EditorStore.getState().editorPrefs) || {};
|
|
3084
|
+
var extra = null;
|
|
3085
|
+
if (range) {
|
|
3086
|
+
extra = {
|
|
3087
|
+
rangeStart: model.getOffsetAt({ lineNumber: range.startLineNumber, column: range.startColumn }),
|
|
3088
|
+
rangeEnd: model.getOffsetAt({ lineNumber: range.endLineNumber, column: range.endColumn })
|
|
3089
|
+
};
|
|
2751
3090
|
}
|
|
3091
|
+
|
|
3092
|
+
return runPrettier(text, prefs, parser, extra)
|
|
3093
|
+
.then(function (formatted) {
|
|
3094
|
+
// The model can have moved on while Prettier was working.
|
|
3095
|
+
if (model.isDisposed() || model.getValue() !== text) return [];
|
|
3096
|
+
return minimalEdit(model, text, formatted);
|
|
3097
|
+
})
|
|
3098
|
+
// A paste of half an expression will not parse. Leaving it alone is the
|
|
3099
|
+
// right answer — the diagnostics path reports the syntax error.
|
|
3100
|
+
["catch"](function () { return []; });
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
Object.keys(PRETTIER_LANGUAGE_PARSERS).forEach(function (languageId) {
|
|
3104
|
+
monaco.languages.registerDocumentRangeFormattingEditProvider(languageId, {
|
|
3105
|
+
displayName: 'Prettier',
|
|
3106
|
+
provideDocumentRangeFormattingEdits: function (model, range) {
|
|
3107
|
+
return prettierEdits(model, range);
|
|
3108
|
+
}
|
|
3109
|
+
});
|
|
3110
|
+
monaco.languages.registerDocumentFormattingEditProvider(languageId, {
|
|
3111
|
+
displayName: 'Prettier',
|
|
3112
|
+
provideDocumentFormattingEdits: function (model) {
|
|
3113
|
+
return prettierEdits(model, null);
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
2752
3116
|
});
|
|
2753
3117
|
|
|
2754
3118
|
// Vim-style fold-marker folding provider.
|
|
@@ -2780,6 +3144,17 @@
|
|
|
2780
3144
|
});
|
|
2781
3145
|
}
|
|
2782
3146
|
|
|
3147
|
+
function registerGlobalExtensions(monaco) {
|
|
3148
|
+
if (globalsRegistered) return;
|
|
3149
|
+
if (!monaco || !monaco.languages) return;
|
|
3150
|
+
|
|
3151
|
+
globalsRegistered = true;
|
|
3152
|
+
|
|
3153
|
+
registerJsProviders(monaco);
|
|
3154
|
+
registerRubyProviders(monaco);
|
|
3155
|
+
registerGenericProviders(monaco);
|
|
3156
|
+
}
|
|
3157
|
+
|
|
2783
3158
|
window.MbeditorEditorPlugins = {
|
|
2784
3159
|
registerGlobalExtensions: registerGlobalExtensions,
|
|
2785
3160
|
attachEditorFeatures: attachEditorFeatures,
|
|
@@ -2793,6 +3168,13 @@
|
|
|
2793
3168
|
markerFixKey: markerFixKey,
|
|
2794
3169
|
// Exposed so the ERB gating can be asserted directly in system tests.
|
|
2795
3170
|
isInsideErbTag: isInsideErbTag,
|
|
3171
|
+
// Exposed so the hover-provider comment/decoration guard can be asserted
|
|
3172
|
+
// directly in system tests.
|
|
3173
|
+
isRealHoverPosition: isRealHoverPosition,
|
|
3174
|
+
// Exposed so JSX prop completion can be asserted without the suggest widget.
|
|
3175
|
+
jsxPropsProvider: jsxPropsProvider,
|
|
3176
|
+
// Exposed so tag-pair linking can be asserted without driving a rename.
|
|
3177
|
+
linkedEditingRanges: linkedEditingRanges,
|
|
2796
3178
|
runRubyEnter: function runRubyEnter(editor) {
|
|
2797
3179
|
if (!editor || !editor.getModel) return false;
|
|
2798
3180
|
return handleRubyEnter(editor, editor.getModel());
|