Repository navigation
Expand file tree
/
Copy pathDefaultProviders.js
More file actions
1716 lines (1561 loc) · 80.2 KB
/
Copy pathDefaultProviders.js
File metadata and controls
1716 lines (1561 loc) · 80.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2019 - present Adobe. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*global Map*/
/* eslint-disable indent */
/* eslint max-len: ["error", { "code": 200 }], no-invalid-this: 0*/
define(function (require, exports, module) {
var _ = require("thirdparty/lodash");
var EditorManager = require("editor/EditorManager"),
DocumentManager = require("document/DocumentManager"),
PreferencesManager = require("preferences/PreferencesManager"),
ProjectManager = require("project/ProjectManager"),
CommandManager = require("command/CommandManager"),
Commands = require("command/Commands"),
StringMatch = require("utils/StringMatch"),
CodeInspection = require("language/CodeInspection"),
LanguageManager = require("language/LanguageManager"),
QuickViewManager = require("features/QuickViewManager"),
FileSystem = require("filesystem/FileSystem"),
PathConverters = require("languageTools/PathConverters"),
TabstopManager = require("editor/TabstopManager"),
Strings = require("strings"),
StringUtils = require("utils/StringUtils"),
Metrics = require("utils/Metrics"),
FileUtils = require("file/FileUtils"),
InlineMenu = require("widgets/InlineMenu").InlineMenu,
marked = require("thirdparty/marked.min"),
matcher = new StringMatch.StringMatcher({
preferPrefixMatches: true
});
// Provider styles live in src/styles/brackets.less (core stylesheet) now that languageTools is a
// core module - no per-extension stylesheet to load.
// ----- LSP code-hint documentation popup ---------------------------------------------------
// The signature (type) is shown inline in the hint list; the (potentially long) documentation
// is shown in a separate popup beside the hint list so the list itself never reflows while
// navigating with the arrow keys.
var $lspDocPopup = null;
// The hint list reflows after _showDocPopup runs - it flips above/below the caret near a screen
// edge, shifts as the cursor moves, and moves on scroll. To keep the doc popup glued beside it
// (and never overlapping it), we re-derive its position from the list's *current* rect on every
// animation frame while it is visible, instead of positioning it once.
var _docTrackRAF = null, // active requestAnimationFrame handle, or null when not tracking
_docTrackList = null, // the ul.dropdown-menu the popup is anchored to
_docLastPos = ""; // last applied "left,top" - skip the css write when unchanged
function _hideDocPopup() {
if (_docTrackRAF) {
cancelAnimationFrame(_docTrackRAF);
_docTrackRAF = null;
}
_docTrackList = null;
_docLastPos = "";
if ($lspDocPopup) {
$lspDocPopup.hide().empty();
}
}
/**
* Place the doc popup flush beside the hint list's current position: to the right, flipping to
* the left when there isn't room, and clamped vertically to stay on screen. Reads the list's
* live rect so it stays correct no matter how the list has since moved.
*/
function _positionDocPopup() {
if (!$lspDocPopup || !_docTrackList || !_docTrackList.length) {
return;
}
var listEl = _docTrackList[0];
if (!listEl.isConnected) {
// The hint menu (and this popup, its child) was torn down - stop tracking.
_hideDocPopup();
return;
}
var anchor = listEl.getBoundingClientRect();
if (anchor.width === 0 && anchor.height === 0) {
return; // list not laid out yet (mid-reflow) - try again next frame
}
var GAP = 0,
winW = $(window).width(),
winH = $(window).height(),
pw = $lspDocPopup.outerWidth(),
ph = $lspDocPopup.outerHeight(),
left = anchor.right + GAP;
if (left + pw > winW - 8) {
left = anchor.left - pw - GAP; // not enough room on the right - flip to the left
}
left = Math.max(8, left);
var top = Math.max(8, Math.min(anchor.top, winH - ph - 8));
var pos = Math.round(left) + "," + Math.round(top);
if (pos !== _docLastPos) {
_docLastPos = pos;
$lspDocPopup.css({ left: left, top: top });
}
}
function _trackDocPopup() {
_positionDocPopup();
_docTrackRAF = requestAnimationFrame(_trackDocPopup);
}
// Syntax-highlight fenced code blocks (the signature/examples in completion docs) with the
// globally available highlight.js, so they read like code instead of flat monospace. Theme-aware
// token colours live in src/styles/brackets.less (.lsp-hint-doc-popup, shared with the hover).
function _highlightCode(html) {
var hljs = (typeof Phoenix !== "undefined") && Phoenix.libs && Phoenix.libs.hljs;
if (!hljs) {
return html;
}
var $wrap = $("<div>").html(html);
$wrap.find("pre > code").each(function () {
var $code = $(this);
var match = ($code.attr("class") || "").match(/language-([\w-]+)/);
var lang = match && match[1];
if (lang && hljs.getLanguage(lang)) {
try {
$code.html(hljs.highlight($code.text(), { language: lang }).value).addClass("hljs");
} catch (e) {
// leave the block unhighlighted on any hljs error
}
}
});
return $wrap.html();
}
function _docToHtml(documentation) {
if (!documentation) {
return "";
}
var md = (typeof documentation === "string") ? documentation : (documentation.value || "");
if (!md.trim()) {
return "";
}
try {
return _highlightCode(marked.parse(md));
} catch (e) {
return _.escape(md);
}
}
// Highlight.js language for the signature block, derived from the file being edited so it isn't
// hard-wired to one language. The LSP `detail` string carries no language tag, so we use the
// editor's. vtsls emits TypeScript-syntax signatures even in .js/.jsx (TS is a superset), so map
// the JS family to ts/tsx; every other language uses its own id - hljs resolves many aliases, and
// _highlightCode no-ops gracefully when the id is unknown (the block just renders as monospace).
var SIGNATURE_HLJS_LANG = {
javascript: "typescript",
jsx: "tsx",
typescript: "typescript",
tsx: "tsx"
};
function _signatureLang() {
var editor = EditorManager.getActiveEditor();
var id = editor && editor.getLanguageForSelection && editor.getLanguageForSelection().getId();
if (!id) {
return "typescript";
}
return SIGNATURE_HLJS_LANG[id] || id;
}
// Build the side doc popup's content, like VS Code/WebStorm's details panel: the item's
// signature (token.detail) as a highlighted code block - the focal point - followed by the
// prose documentation. The signature already carries the "Add import from <module>" line for
// auto-imports (tsserver puts it there), so we don't add our own. Shown whenever there's a
// signature OR docs, so a doc-less item still gets its signature shown instead of nothing.
function _docPopupHtml(token) {
var parts = [];
// The signature is worth a popup only when it adds something beyond the item's own name.
// vtsls returns the label itself as `detail` for keywords/plain identifiers (`this` -> "this",
// `throw` -> "throw"), which would make the popup a verbatim echo of the hint row - so a detail
// that merely restates the label counts as "no signature".
var label = (token.label || "").trim();
var detail = (token.detail || "").trim();
// Some servers (intelephense) split the signature across fields: labelDetails.detail holds
// the parameter list ("($search, $replace, ...)") and `detail` only the return type. Shown
// alone the return type reads like noise - compose the full signature instead. Only when
// labelDetails.detail is a parameter list and `detail` isn't already a full signature
// (vtsls puts the whole signature in `detail`, which must stay untouched).
var paramList = (token.labelDetails && token.labelDetails.detail || "").trim();
var signature = detail;
if (paramList.charAt(0) === "(" && detail.indexOf("(") === -1) {
signature = label + paramList + (detail ? ": " + detail : "");
}
if (signature && signature !== label) {
try {
parts.push(_highlightCode(marked.parse("```" + _signatureLang() + "\n" + signature + "\n```")));
} catch (e) {
// skip the signature block on any parse/highlight error
}
}
var docHtml = _docToHtml(token.documentation);
if (docHtml) {
parts.push(docHtml);
}
// Empty -> "" so _showDocPopup hides the popup entirely instead of echoing the hint row.
return parts.join("");
}
// Minimal copy glyph (stroked, inherits currentColor) for the per-code-block copy button.
const COPY_ICON = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
'stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<rect x="9" y="9" width="13" height="13" rx="2"/>' +
'<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
function _copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
return new Promise(function (resolve, reject) {
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.cssText = "position:fixed;opacity:0;pointer-events:none;";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
resolve();
} catch (e) {
reject(e);
}
});
}
// Give every code block in the doc popup a hover-revealed copy button.
function _addCopyButtons($popup) {
$popup.find("pre").each(function () {
const $pre = $(this);
if ($pre.children(".lsp-copy-btn").length) {
return;
}
$("<button>")
.addClass("lsp-copy-btn")
.attr({ type: "button", title: Strings.CMD_COPY, "aria-label": Strings.CMD_COPY })
.html(COPY_ICON)
.appendTo($pre);
});
}
function _showDocPopup($hint, docHtml) {
// Single choke point for the docs popup beside the hint list - the preference gates
// every caller (LSP hints here, npm hints, ...).
if (PreferencesManager.get("showCodeHintDocs") === false) {
_hideDocPopup();
return;
}
// .inlinemenu-menu too: the jump-to-definition multi-target picker (see
// JumpToDefProvider.doJumpToDef -> showJumpTargetPicker) reuses this same side-popup
// mechanism, via InlineMenu, to show a code excerpt for the hovered/selected target.
var $menu = $hint.closest(".codehint-menu, .inlinemenu-menu");
if (!docHtml || !$menu.length) {
_hideDocPopup();
return;
}
// Make the popup a child of the hint menu so it is removed automatically when the menu is
// removed (CodeHintList.close()/InlineMenu.close() always remove $hintMenu). This ties its
// lifecycle strictly to the list, regardless of which teardown path fires. It uses
// position:fixed, so it is still placed in viewport coordinates and is never clipped.
if (!$lspDocPopup || !$lspDocPopup.parent().is($menu)) {
$lspDocPopup = $("<div>").addClass("lsp-hint-doc-popup").appendTo($menu);
// Keep mouse interaction inside the popup from reaching the document-level handlers that
// dismiss the completion (Bootstrap's "close dropdown on outside click"), so the user can
// select and copy text/code without the popup vanishing. Crucially we do NOT preventDefault
// here - the browser's native text selection must keep working.
$lspDocPopup
.on("mousedown click", function (e) {
e.stopPropagation();
})
.on("click", ".lsp-copy-btn", function (e) {
e.preventDefault();
const $btn = $(this);
const text = $btn.closest("pre").find("code").text();
if (!text) {
return;
}
_copyText(text).then(function () {
$btn.addClass("copied");
setTimeout(function () { $btn.removeClass("copied"); }, 1200);
}).catch(function () { /* clipboard unavailable - ignore */ });
});
}
$lspDocPopup.empty().html(docHtml);
_addCopyButtons($lspDocPopup);
// Anchor to the actual visible list (ul.dropdown-menu), not the zero-width .codehint-menu
// positioning element, so the popup sits flush beside the list without overlapping it.
var $list = $hint.closest("ul.dropdown-menu");
if (!$list.length) {
$list = $menu;
}
// Position now, then keep re-positioning every frame so the popup follows the list wherever
// it reflows to (and never ends up overlapping it).
_docTrackList = $list;
_docLastPos = "";
$lspDocPopup.css({ display: "block" });
_positionDocPopup();
if (!_docTrackRAF) {
_trackDocPopup();
}
}
// Inline row details longer than this go to the side docs popup instead of the row (some
// servers - intelephense - put whole function signatures in `detail`, which bloats the list).
const INLINE_DETAIL_MAX_CHARS = 32;
function _injectInlineSignature($labelSpan, detail) {
if (!detail || !detail.trim() || detail.trim() === "?") {
return;
}
// Append the signature as a sibling of the label (inside .codehint-item) so the flex row
// layout (see CSS) lays out label + signature side-by-side without overlap or width change.
var text = detail.split("->").join(":").replace(/\s+/g, " ").trim();
var $container = $labelSpan.parent(); // .codehint-item
if (!$container.length) {
$container = $labelSpan;
}
$container.find(".lsp-hint-sig").remove();
// title shows the full signature on hover when it's truncated.
$("<span>").addClass("lsp-hint-sig").text(text).attr("title", text).appendTo($container);
}
// Characters that, when typed, implicitly trigger completion in the generic default policy
// (identifiers across most languages). A language server config may fully replace this policy
// with its own `shouldAutoTrigger(implicitChar, editor)` callback. Server-declared
// `triggerCharacters` (e.g. ".", "$", "::", "->") are honored per-language in the default too.
var DEFAULT_IDENTIFIER_CHARS = /[A-Za-z0-9_$]/;
function setClient(client) {
if (client) {
this.client = client;
}
}
function CodeHintsProvider(client) {
this.client = client;
this.query = "";
this.ignoreQuery = ["-", "->", ">", ":", "::", "(", "()", ")", "[", "[]", "]", "{", "{}", "}"];
}
CodeHintsProvider.prototype.setClient = setClient;
function filterWithQueryAndMatcher(hints, query) {
var matchResults = $.map(hints, function (hint) {
var searchResult = matcher.match(hint.label, query);
if (searchResult) {
for (var key in hint) {
searchResult[key] = hint[key];
}
}
return searchResult;
});
return matchResults;
}
// True when a completion is an auto-import suggestion (it carries the source module in
// labelDetails.description), as opposed to an in-scope symbol or a keyword.
function _isAutoImport(item) {
return !!(item.labelDetails && item.labelDetails.description);
}
// WebStorm-style noise control: when several modules export the SAME name, the LSP returns one
// auto-import suggestion per module, flooding the list with near-identical rows. Collapse those
// into a single "N imports…" row; choosing it re-opens the list filtered to just that name's
// sources so the user picks the module (see insertHint + the _importLabel branch of getHints).
// In-scope symbols and single-source auto-imports are left exactly as they were.
function _clubAutoImports(items) {
var counts = new Map();
items.forEach(function (it) {
if (_isAutoImport(it)) {
counts.set(it.label, (counts.get(it.label) || 0) + 1);
}
});
var seen = new Set(), result = [];
items.forEach(function (it) {
if (!_isAutoImport(it) || counts.get(it.label) < 2) {
result.push(it); // in-scope, keyword, or the only import source for this name
return;
}
if (seen.has(it.label)) {
return; // already represented by the clubbed row at its best-sorted position
}
seen.add(it.label);
result.push(Object.assign({}, it, {
_clubbed: true,
_importCount: counts.get(it.label)
}));
});
return result;
}
// Render one completion row: the (matched-highlighted) label, plus a dimmed right-aligned tag -
// either the source module of a single auto-import, or "N imports…" for a clubbed group.
function _renderHint(element) {
var $fHint = $("<span>").addClass("brackets-hints");
var $label = $("<span>").addClass("lsp-hint-label");
if (element.stringRanges) {
element.stringRanges.forEach(function (item) {
if (item.matched) {
$label.append($("<span>").append(_.escape(item.text)).addClass("matched-hint"));
} else {
$label.append(_.escape(item.text));
}
});
} else {
$label.text(element.label);
}
$fHint.append($label);
if (element._clubbed) {
$("<span>")
.addClass("lsp-hint-source lsp-hint-import-group")
.text(StringUtils.format(Strings.CODE_HINT_IMPORT_FROM_N, element._importCount))
.appendTo($fHint);
} else if (element.labelDetails && element.labelDetails.description) {
// VS Code-style source annotation: shows e.g. the `inspector` in a `console` auto-import
// so same-named items from different modules read as distinct rows.
$("<span>")
.addClass("lsp-hint-source")
.text(element.labelDetails.description)
.attr("title", element.labelDetails.description)
.appendTo($fHint);
}
$fHint.data("token", element);
return $fHint;
}
CodeHintsProvider.prototype.hasHints = function (editor, implicitChar) {
// Stand down when the document isn't one the server syncs (providers are selected by the
// language at the CURSOR, so e.g. inside an HTML <script> this provider is asked even though
// the server never sees the .html) - lets lower-priority providers (Tern) serve those.
if (!this.client || !this.client.servesDocument(editor)) {
return false;
}
var serverCapabilities = this.client.getServerCapabilities();
if (!serverCapabilities || !serverCapabilities.completionProvider) {
return false;
}
// Explicit invocation (e.g. Ctrl-Space) always shows hints.
if (!implicitChar) {
return true;
}
// A language server may fully control where hints implicitly appear by supplying a
// `shouldAutoTrigger(implicitChar, editor)` callback (e.g. to suppress inside strings or
// trigger on language-specific sequences). When provided, it replaces the default policy.
var config = this.client.config || {};
if (typeof config.shouldAutoTrigger === "function") {
return !!config.shouldAutoTrigger(implicitChar, editor);
}
// Default policy: auto-trigger only on identifier characters, so the popup doesn't appear
// aggressively on every keystroke (operators, punctuation, etc.).
if (DEFAULT_IDENTIFIER_CHARS.test(implicitChar)) {
return true;
}
// ...or on a server-declared trigger character (e.g. "." for member access). Whitespace
// triggers (some servers list " ") are intentionally ignored - they are too aggressive.
var triggerChars = serverCapabilities.completionProvider.triggerCharacters || [];
if (implicitChar.trim() && triggerChars.indexOf(implicitChar) !== -1) {
return true;
}
return false;
};
CodeHintsProvider.prototype.getHints = function (implicitChar) {
if (!this.client) {
return null;
}
var editor = EditorManager.getActiveEditor(),
pos = editor.getCursorPos(),
docPath = editor.document.file._path,
$deferredHints = $.Deferred(),
self = this;
// The query is the identifier prefix already typed before the cursor (empty right after a
// trigger char such as "."). Deriving it from the raw token is wrong - after a "." the token
// is "." itself, which would filter out every member completion.
function computeQuery() {
var lineText = editor.document.getLine(pos.line),
queryStart = pos.ch;
while (queryStart > 0 && /[\w$]/.test(lineText.charAt(queryStart - 1))) {
queryStart--;
}
return lineText.substring(queryStart, pos.ch);
}
this.client.requestHints({
filePath: docPath,
cursorPos: pos
}).done(function (msgObj) {
var hints = [];
self.query = computeQuery();
if (msgObj) {
var filteredHints = filterWithQueryAndMatcher(msgObj.items, self.query);
StringMatch.basicMatchSort(filteredHints);
// Second step of a clubbed auto-import: the user picked the "N imports…" row, so show
// just that name's import sources (one row per module) instead of the whole list.
if (self._importLabel) {
var wanted = self._importLabel;
self._importLabel = null;
filteredHints = filteredHints.filter(function (it) {
return it.label === wanted && _isAutoImport(it);
});
} else {
filteredHints = _clubAutoImports(filteredHints);
}
hints = filteredHints.map(_renderHint);
}
$deferredHints.resolve({
"hints": hints,
"selectInitial": true
});
}).fail(function () {
// requestHints rejects only on a real request failure - empty results resolve.
Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "hint", "Fail." + self.client._metricLabel);
self._importLabel = null;
$deferredHints.reject();
});
return $deferredHints;
};
/**
* Called when a hint is highlighted. LSP completion lists are lightweight - the type detail
* and documentation usually arrive only via `completionItem/resolve`. Resolve the highlighted
* item lazily and render its type/docs inline (reusing formatTypeDataForToken so the look
* matches the rest of the hint UI).
*/
CodeHintsProvider.prototype.onHighlight = function ($hint) {
var self = this;
if (!self.client) {
_hideDocPopup();
return;
}
var $span = $hint.closest("li").data("hint"),
token = $span && $span.data && $span.data("token");
if (!token) {
_hideDocPopup();
return;
}
// Mark this menu as an LSP menu so the flex row layout + stable min-width apply (scoped so
// other code-hint providers are unaffected). Done synchronously so it takes effect before
// the list is shown.
$hint.closest(".codehint-menu").addClass("lsp-hints");
// Clubbed "N imports…" row: it stands in for several modules, so resolving its (first
// module's) docs would be misleading. Show a short hint about what selecting it does.
if (token._clubbed) {
_showDocPopup($hint, "<p>" + StringUtils.format(
Strings.CODE_HINT_IMPORT_CHOOSE, token._importCount, _.escape(token.label)) + "</p>");
return;
}
function present() {
// Inline: the signature for the highlighted row. Re-inject every time (it is
// idempotent) because the list DOM is rebuilt on each keystroke, which drops a
// previously-injected signature. resolveCompletion is cached separately (_lspResolved),
// so this does not cause extra LSP requests. Skip it for auto-imports - their detail is a
// long "Add import from <module>" string that just clips uselessly; the short source-module
// tag stays visible there instead (see CSS), and the doc popup carries the full import.
// Also skip LONG details (some servers - intelephense - put the whole function signature
// there, which bloats the rows): the side docs popup carries those instead.
if (token.detail && !_isAutoImport(token) && token.detail.length <= INLINE_DETAIL_MAX_CHARS) {
_injectInlineSignature($span, token.detail);
}
// Beside the list: the signature header + the (possibly long) documentation.
_showDocPopup($hint, _docPopupHtml(token));
}
if (token._lspResolved || !self.client.resolveCompletion) {
present();
return;
}
self.client.resolveCompletion(token).done(function (resolved) {
token._lspResolved = true;
token.detail = resolved.detail || token.detail;
token.documentation = resolved.documentation || token.documentation;
// Auto-import edits are commonly supplied only by completionItem/resolve. Carry them (and
// a resolve-provided textEdit, when the item lacked one) onto the token so insertHint can
// apply them.
if (resolved.additionalTextEdits) {
token.additionalTextEdits = resolved.additionalTextEdits;
}
if (!token.textEdit && resolved.textEdit) {
token.textEdit = resolved.textEdit;
}
present();
});
};
CodeHintsProvider.prototype.onClose = function () {
_hideDocPopup();
};
CodeHintsProvider.prototype.insertHint = function ($hint) {
var editor = EditorManager.getActiveEditor();
if (!editor) {
return false;
}
var token = $hint.data("token") || {};
// Clubbed "N imports…" row: don't insert anything yet. Re-open the list showing only this
// name's import sources so the user picks the module (WebStorm's "Class to Import" flow).
// getHints honors _importLabel on the next open; the symbol/import is inserted when a
// concrete source is chosen there.
if (token._clubbed) {
this._importLabel = token.label;
setTimeout(function () {
CommandManager.execute(Commands.SHOW_CODE_HINTS);
}, 0);
return false;
}
var cursor = editor.getCursorPos(),
lineText = editor.document.getLine(cursor.line),
textEditRange = token.textEdit && token.textEdit.range,
startCh,
endCh = cursor.ch;
// Anchor the replacement start on the server-provided textEdit.range.start when it is usable.
// That start is stable as the user types forward (word/member starts don't move) and, crucially,
// for member completions it points AT the trigger "." while newText itself includes the dot
// (e.g. "console." + item ".log" -> replace from the "." -> "console.log", not "console..log").
if (textEditRange && textEditRange.start.line === cursor.line &&
textEditRange.start.character <= cursor.ch) {
startCh = textEditRange.start.character;
// Honor a range.end BEYOND the cursor: the server wants existing text after the caret
// overwritten because newText includes it (e.g. JSON key completion inside autoclosed
// quotes `"|"` - newText `"author": {$1}` covers both quotes; keeping the closing quote
// would leave a dangling `"` behind the insert). A range.end at/before the cursor is
// clamped TO the cursor instead: that end is stale when completions are served from
// cache while typing continues, which would otherwise replace only part of the word
// (e.g. "conso"+enter -> "consolenso").
if (textEditRange.end.line === cursor.line && textEditRange.end.character > cursor.ch) {
endCh = textEditRange.end.character;
}
} else {
startCh = cursor.ch;
while (startCh > 0 && /[\w$]/.test(lineText.charAt(startCh - 1))) {
startCh--;
}
}
var rawText = (token.textEdit && token.textEdit.newText) || token.insertText || token.label || "",
startPos = { line: cursor.line, ch: startCh },
endPos = { line: cursor.line, ch: endCh };
if (token.insertTextFormat === 2) {
// Snippet completion: let TabstopManager expand $1 / ${1:x} / $0, place the caret at the
// first stop (selecting any default placeholder) and, for multi-stop snippets, start a
// Tab-navigable session. Its caret/markers follow the additionalTextEdits applied below.
TabstopManager.insertSnippet(editor, rawText, startPos, endPos);
} else {
editor.document.replaceRange(rawText, startPos, endPos);
}
// Apply additionalTextEdits (e.g. the auto-import line). Bottom-to-top keeps their own
// coordinates valid; the editor maps the caret and any snippet markers through these edits
// automatically, so an import inserted above does not strand the cursor.
(token.additionalTextEdits || []).slice().sort(function (a, b) {
return (b.range.start.line - a.range.start.line) ||
(b.range.start.character - a.range.start.character);
}).forEach(function (te) {
editor.document.replaceRange(te.newText,
{ line: te.range.start.line, ch: te.range.start.character },
{ line: te.range.end.line, ch: te.range.end.character });
});
Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "hint", "Acc." + this.client._metricLabel);
if (token.additionalTextEdits && token.additionalTextEdits.length) {
// An auto-import was applied with the completion - a high-value signal on its own.
Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "hint", "Import." + this.client._metricLabel);
}
// Return false to indicate that another hinting session is not needed
return false;
};
function ParameterHintsProvider(client) {
this.client = client;
}
ParameterHintsProvider.prototype.setClient = setClient;
ParameterHintsProvider.prototype.hasParameterHints = function (editor, implicitChar) {
// See CodeHintsProvider.hasHints - don't claim documents the server doesn't sync.
if (!this.client || !this.client.servesDocument(editor)) {
return false;
}
var serverCapabilities = this.client.getServerCapabilities();
if (!serverCapabilities || !serverCapabilities.signatureHelpProvider) {
return false;
}
return true;
};
ParameterHintsProvider.prototype.getParameterHints = function () {
if (!this.client) {
return null;
}
var editor = EditorManager.getActiveEditor(),
pos = editor.getCursorPos(),
docPath = editor.document.file._path,
metricLabel = this.client._metricLabel,
$deferredHints = $.Deferred();
// A language server may VETO signature help at specific cursor positions by supplying a
// `shouldShowParameterHints(editor)` callback (e.g. vtsls suppresses the parent call's
// hint inside callback bodies). Rejecting here (rather than declining in
// hasParameterHints) keeps this provider the owner of the request, so the manager
// dismisses the popup instead of falling through to a lower-priority provider - and the
// cursor-activity refresh then auto-dismisses an already-visible popup when the caret
// moves into a vetoed position.
var config = this.client.config || {};
if (typeof config.shouldShowParameterHints === "function" &&
!config.shouldShowParameterHints(editor)) {
return $deferredHints.reject(null);
}
this.client.requestParameterHints({
filePath: docPath,
cursorPos: pos
}).done(function (msgObj) {
let paramList = [];
if (msgObj) {
let res = msgObj.signatures;
let activeParameter = msgObj.activeParameter;
if (res && res.length) {
// Use the active signature (not all of them concatenated - overloads would
// otherwise merge their parameters into one bogus list).
let sig = res[msgObj.activeSignature || 0] || res[0];
(sig.parameters || []).forEach(function (ele) {
paramList.push({
label: ele.label,
documentation: ele.documentation
});
});
// The function name for the popup header, taken from the signature label
// (everything before the parameter list "("). Without this the manager falls back
// to the editor token at the caret - which is the just-typed "(" - producing an
// unbalanced "((tableName: any)".
let functionName = "";
if (typeof sig.label === "string") {
let paren = sig.label.indexOf("(");
functionName = (paren >= 0 ? sig.label.slice(0, paren) : sig.label).trim();
}
$deferredHints.resolve({
parameters: paramList,
currentIndex: activeParameter,
functionDocumentation: sig.documentation,
functionName: functionName
});
} else {
$deferredHints.reject();
}
} else {
$deferredHints.reject();
}
}).fail(function (err) {
// requestParameterHints rejects bare for "no signatures here" (normal) and with an
// error for a real request failure - only the latter is a health signal.
if (err) {
Metrics.countEvent(Metrics.EVENT_TYPE.LSP, "sig", "Fail." + metricLabel);
}
$deferredHints.reject();
});
return $deferredHints;
};
/**
* Utility function to make the jump
* @param {Object} curPos - target postion for the cursor after the jump
*/
function setJumpPosition(curPos) {
EditorManager.getCurrentFullEditor().setCursorPos(curPos.line, curPos.ch, true);
}
var DECLARATION_SCAN_LIMIT = 200; // lines to search upward/downward before giving up
var EXCERPT_BODY_MAX_LINES = 6; // cap on the target's own block (target line included)
function _indentOf(line) {
return /^[ \t]*/.exec(line)[0].length;
}
// Scans upward from just above lineIdx for the nearest non-blank line indented *less* than
// lineIdx's own line - i.e. the line that opens whatever block/scope lineIdx sits inside.
// Language-agnostic on purpose: rather than hand-listing every language's "class"/"def"/"fn"/
// "func"/... declaration keywords (which is both never-ending and TS-biased in practice), this
// works the same way for any brace- or indentation-scoped language, the same technique editors'
// "sticky scroll"/breadcrumb features use. A member a few lines below "class JohnClass extends
// MyBaseClass {" is more indented than that line, so this reliably lands there regardless of
// what keyword the language actually uses. Best effort only: inconsistent indentation (or a
// target already at column 0) just means the excerpt starts at the target line itself instead.
function findEnclosingDeclarationLine(lines, lineIdx) {
var from = Math.min(lineIdx, lines.length - 1),
targetIndent = _indentOf(lines[from]);
if (targetIndent === 0) {
return null;
}
var to = Math.max(0, from - DECLARATION_SCAN_LIMIT);
for (var i = from - 1; i >= to; i--) {
if (!lines[i].trim()) {
continue; // skip blank lines - they carry no indentation signal
}
if (_indentOf(lines[i]) < targetIndent) {
return i;
}
}
return null;
}
// Mirrors findEnclosingDeclarationLine, but forward: scans from just below lineIdx for the
// nearest non-blank line indented at or less than lineIdx's own - i.e. where the block lineIdx
// opens actually closes (its own closing brace, in most brace-style languages, or the start of
// whatever comes next at the same level). Used to show a truncated target's own block honestly
// (with a trailing "...") instead of just grabbing a fixed number of lines regardless of
// whether that cuts the block short or spills past its end into unrelated code that follows.
function findBlockEndLine(lines, lineIdx) {
var indent = _indentOf(lines[lineIdx]),
to = Math.min(lines.length - 1, lineIdx + DECLARATION_SCAN_LIMIT);
for (var i = lineIdx + 1; i <= to; i++) {
if (!lines[i].trim()) {
continue;
}
if (_indentOf(lines[i]) <= indent) {
return i;
}
}
return null;
}
// True if any line strictly between startExclusive and endExclusive has real content - used so
// a "..." marker is only ever shown when something is actually being hidden, never just
// because two things happen to be on non-adjacent line numbers (blank lines don't count as
// "something hidden").
function _hasNonBlankLineBetween(lines, startExclusive, endExclusive) {
for (var i = startExclusive + 1; i < endExclusive; i++) {
if (lines[i].trim()) {
return true;
}
}
return false;
}
// A short, fixed-shape code excerpt around a jump target: the enclosing declaration line (if
// found nearby, e.g. "class JohnClass extends MyBaseClass {"), then its own block - never the
// declaration's other members (a sibling method, however small, is still unrelated to why this
// particular target was picked), so the popup reads the same way every time instead of
// sometimes including siblings and sometimes not depending on how much fits. The block itself
// is capped independently the same way (see findBlockEndLine) - a long body gets its own "..."
// without needing the declaration side to also collapse. A "..." marker is only ever inserted
// where it's true - between the declaration and the target when a member actually sits between
// them, and after the block only when it was actually cut short - never as a fixed decoration.
// Lines are shown in full - the popup already scrolls horizontally for a long line (same as
// the signature/documentation code blocks it shares that behavior with).
function buildExcerpt(lines, targetLineIdx) {
var declLine = findEnclosingDeclarationLine(lines, targetLineIdx),
indent = /^[ \t]*/.exec(lines[targetLineIdx])[0],
blockEnd = findBlockEndLine(lines, targetLineIdx),
naturalEnd = blockEnd !== null ?
blockEnd : Math.min(lines.length - 1, targetLineIdx + EXCERPT_BODY_MAX_LINES - 1),
bodyEnd = Math.min(naturalEnd, targetLineIdx + EXCERPT_BODY_MAX_LINES - 1),
bodyLines = lines.slice(targetLineIdx, bodyEnd + 1);
if (bodyEnd < naturalEnd) {
bodyLines.push(indent + "...");
}
if (declLine === null) {
return bodyLines.join("\n");
}
var headerLines = _hasNonBlankLineBetween(lines, declLine, targetLineIdx) ?
[lines[declLine], indent + "..."] : [lines[declLine]];
return headerLines.concat(bodyLines).join("\n");
}
// Text of `path` split into lines, preferring an already-open (possibly unsaved) document
// over a disk read, since a jump target may already be open and edited.
function getFileLines(path) {
var openDoc = DocumentManager.getOpenDocumentForPath(path);
if (openDoc) {
return $.Deferred().resolve(openDoc.getText().split("\n")).promise();
}
var deferred = $.Deferred();
FileSystem.getFileForPath(path).read(function (err, content) {
if (err || typeof content !== "string") {
deferred.reject();
} else {
deferred.resolve(content.split("\n"));
}
});
return deferred.promise();
}
// highlight.js language id for a jump target's own file (not the currently active editor's -
// a target may be in a different language/file than the one you jumped from). Reuses the same
// js/jsx -> ts/tsx aliasing as signature blocks (see SIGNATURE_HLJS_LANG below _docPopupHtml)
// since hljs's TS grammar is a strict superset and highlights plain JS/JSX fine.
function excerptHljsLang(path) {
var language = LanguageManager.getLanguageForPath(path),
id = language && language.getId();
if (!id) {
return "javascript";
}
return SIGNATURE_HLJS_LANG[id] || id;
}
// A location's identity for deduplication - same file and same start *line*, deliberately not
// also the start character. Locations from different requests (definition vs. implementation)
// routinely name the same declaration while anchored at different columns on that declaration's
// line - e.g. gotoDefinition landing at column 0 (the very start of "export class JohnClass
// extends MyBaseClass") while gotoImplementation for the same class lands at the "JohnClass"
// identifier a few columns in. Keying on the exact character missed that case entirely (shown
// as two near-identical "JohnClass" entries instead of one) - a jump target is realistically
// always alone on its line, so same file + same line is a safe stand-in for "same target".
function _locationKey(location) {
return location.uri + ":" + location.range.start.line;
}
// Combines `primary` with `extras`, keeping first-seen order and dropping any of `extras` that
// point at the same place as `primary` or one another. Used so a location gotoDefinition
// already resolved is never silently thrown away in favor of gotoImplementation's answer - see
// fallBackToImplementations below - even though in practice the two usually end up naming the
// same target anyway (a single "definition" resolution for a polymorphic call almost always
// turns out to be one of the concrete implementations, not the base/interface declaration
// itself - LSP has no request that reliably returns that separately).
function mergeLocations(primary, extras) {
var seen = {},
merged = [];
[primary].concat(extras).forEach(function (location) {
var key = _locationKey(location);
if (!seen[key]) {
seen[key] = true;
merged.push(location);
}
});
return merged;
}
function JumpToDefProvider(client) {
this.client = client;
}
JumpToDefProvider.prototype.setClient = setClient;
JumpToDefProvider.prototype.canJumpToDef = function (editor, implicitChar) {
// See CodeHintsProvider.hasHints - don't claim documents the server doesn't sync.
if (!this.client || !this.client.servesDocument(editor)) {
return false;
}
var serverCapabilities = this.client.getServerCapabilities();