Skip to content

Commit 5eca96c

Browse files
committed
Completion list in the type expression should show types
1 parent dcac1df commit 5eca96c

7 files changed

+448
-28
lines changed

src/harness/fourslash.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -814,8 +814,8 @@ namespace FourSlash {
814814

815815
function filterByTextOrDocumentation(entry: ts.CompletionEntry) {
816816
const details = that.getCompletionEntryDetails(entry.name);
817-
const documentation = ts.displayPartsToString(details.documentation);
818-
const text = ts.displayPartsToString(details.displayParts);
817+
const documentation = details && ts.displayPartsToString(details.documentation);
818+
const text = details && ts.displayPartsToString(details.displayParts);
819819

820820
// If any of the expected values are undefined, assume that users don't
821821
// care about them.
@@ -852,6 +852,9 @@ namespace FourSlash {
852852
if (expectedKind) {
853853
error += "Expected kind: " + expectedKind + " to equal: " + filterCompletions[0].kind + ".";
854854
}
855+
else {
856+
error += "kind: " + filterCompletions[0].kind + ".";
857+
}
855858
if (replacementSpan) {
856859
const spanText = filterCompletions[0].replacementSpan ? stringify(filterCompletions[0].replacementSpan) : undefined;
857860
error += "Expected replacement span: " + stringify(replacementSpan) + " to equal: " + spanText + ".";

src/services/completions.ts

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -366,16 +366,16 @@ namespace ts.Completions {
366366
let request: Request | undefined;
367367

368368
let start = timestamp();
369-
const currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false);
369+
let currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false);
370370
// We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.)
371-
372371
log("getCompletionData: Get current token: " + (timestamp() - start));
373372

374373
start = timestamp();
375374
// Completion not allowed inside comments, bail out if this is the case
376375
const insideComment = isInComment(sourceFile, position, currentToken);
377376
log("getCompletionData: Is inside comment: " + (timestamp() - start));
378377

378+
let insideJsDocTagTypeExpression = false;
379379
if (insideComment) {
380380
if (hasDocComment(sourceFile, position)) {
381381
if (sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) {
@@ -410,33 +410,31 @@ namespace ts.Completions {
410410
// Completion should work inside certain JsDoc tags. For example:
411411
// /** @type {number | string} */
412412
// Completion should work in the brackets
413-
let insideJsDocTagExpression = false;
414413
const tag = getJsDocTagAtPosition(currentToken, position);
415414
if (tag) {
416415
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
417416
request = { kind: "JsDocTagName" };
418417
}
419-
420-
switch (tag.kind) {
421-
case SyntaxKind.JSDocTypeTag:
422-
case SyntaxKind.JSDocParameterTag:
423-
case SyntaxKind.JSDocReturnTag:
424-
const tagWithExpression = <JSDocTypeTag | JSDocParameterTag | JSDocReturnTag>tag;
425-
if (tagWithExpression.typeExpression && tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end) {
426-
insideJsDocTagExpression = true;
427-
}
428-
else if (isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
429-
request = { kind: "JsDocParameterName", tag };
430-
}
431-
break;
418+
if (isTagWithTypeExpression(tag) && tag.typeExpression) {
419+
currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ true);
420+
if (!currentToken ||
421+
(!isDeclarationName(currentToken) &&
422+
(currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag ||
423+
(<JSDocPropertyTag>currentToken.parent).name !== currentToken))) {
424+
// Use as type location if inside tag's type expression
425+
insideJsDocTagTypeExpression = isCurrentlyEditingNode(tag.typeExpression);
426+
}
427+
}
428+
if (isJSDocParameterTag(tag) && (nodeIsMissing(tag.name) || tag.name.pos <= position && position <= tag.name.end)) {
429+
request = { kind: "JsDocParameterName", tag };
432430
}
433431
}
434432

435433
if (request) {
436434
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, hasFilteredClassMemberKeywords: false };
437435
}
438436

439-
if (!insideJsDocTagExpression) {
437+
if (!insideJsDocTagTypeExpression) {
440438
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
441439
// comment or the plain text part of a jsDoc comment, so no completion should be available
442440
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");
@@ -445,7 +443,7 @@ namespace ts.Completions {
445443
}
446444

447445
start = timestamp();
448-
const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true);
446+
const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, insideJsDocTagTypeExpression);
449447
log("getCompletionData: Get previous token 1: " + (timestamp() - start));
450448

451449
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
@@ -456,7 +454,7 @@ namespace ts.Completions {
456454
// Skip this partial identifier and adjust the contextToken to the token that precedes it.
457455
if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) {
458456
const start = timestamp();
459-
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined, /*includeJsDoc*/ true);
457+
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined, insideJsDocTagTypeExpression);
460458
log("getCompletionData: Get previous token 2: " + (timestamp() - start));
461459
}
462460

@@ -468,7 +466,7 @@ namespace ts.Completions {
468466
let isRightOfOpenTag = false;
469467
let isStartingCloseTag = false;
470468

471-
let location = getTouchingPropertyName(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853
469+
let location = getTouchingPropertyName(sourceFile, position, insideJsDocTagTypeExpression); // TODO: GH#15853
472470
if (contextToken) {
473471
// Bail out if this is a known invalid completion location
474472
if (isCompletionListBlocker(contextToken)) {
@@ -572,14 +570,28 @@ namespace ts.Completions {
572570

573571
return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, hasFilteredClassMemberKeywords };
574572

573+
type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
574+
575+
function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression {
576+
switch (tag.kind) {
577+
case SyntaxKind.JSDocAugmentsTag:
578+
case SyntaxKind.JSDocParameterTag:
579+
case SyntaxKind.JSDocPropertyTag:
580+
case SyntaxKind.JSDocReturnTag:
581+
case SyntaxKind.JSDocTypeTag:
582+
case SyntaxKind.JSDocTypedefTag:
583+
return true;
584+
}
585+
}
586+
575587
function getTypeScriptMemberSymbols(): void {
576588
// Right of dot member completion list
577589
isGlobalCompletion = false;
578590
isMemberCompletion = true;
579591
isNewIdentifierLocation = false;
580592

581593
// Since this is qualified name check its a type node location
582-
const isTypeLocation = isPartOfTypeNode(node.parent);
594+
const isTypeLocation = isPartOfTypeNode(node.parent) || insideJsDocTagTypeExpression;
583595
const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node);
584596
if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName || node.kind === SyntaxKind.PropertyAccessExpression) {
585597
let symbol = typeChecker.getSymbolAtLocation(node);
@@ -741,8 +753,9 @@ namespace ts.Completions {
741753
return !!(symbol.flags & SymbolFlags.Namespace);
742754
}
743755

744-
if (!isContextTokenValueLocation(contextToken) &&
745-
(isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken))) {
756+
if (insideJsDocTagTypeExpression ||
757+
(!isContextTokenValueLocation(contextToken) &&
758+
(isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)))) {
746759
// Its a type, but you can reach it by namespace.type as well
747760
return symbolCanbeReferencedAtTypeLocation(symbol);
748761
}
@@ -789,14 +802,16 @@ namespace ts.Completions {
789802
symbol = typeChecker.getAliasedSymbol(symbol);
790803
}
791804

805+
if (symbol.flags & SymbolFlags.Type) {
806+
return true;
807+
}
808+
792809
if (symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule)) {
793810
const exportedSymbols = typeChecker.getExportsOfModule(symbol);
794811
// If the exported symbols contains type,
795812
// symbol can be referenced at locations where type is allowed
796813
return forEach(exportedSymbols, symbolCanbeReferencedAtTypeLocation);
797814
}
798-
799-
return !!(symbol.flags & (SymbolFlags.NamespaceModule | SymbolFlags.Type));
800815
}
801816

802817
/**

src/services/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,7 @@ namespace ts {
745745
// NOTE: JsxText is a weird kind of node that can contain only whitespaces (since they are not counted as trivia).
746746
// if this is the case - then we should assume that token in question is located in previous child.
747747
if (position < child.end && (nodeHasTokens(child) || child.kind === SyntaxKind.JsxText)) {
748-
const start = (includeJsDoc && child.jsDoc ? child.jsDoc[0] : child).getStart(sourceFile);
748+
const start = child.getStart(sourceFile, includeJsDoc);
749749
const lookInPreviousChild =
750750
(start >= position) || // cursor in the leading trivia
751751
(child.kind === SyntaxKind.JsxText && start === child.end); // whitespace only JsxText
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
// @allowNonTsExtensions: true
4+
// @Filename: jsFileJsdocTypedefTagTypeExpressionCompletion_typedef.js
5+
6+
//// /**
7+
//// * @typedef {/*1*/string | /*2*/number} T.NumberLike
8+
//// * @typedef {{/*propertyName*/age: /*3*/number}} T.People
9+
//// * @typedef {string | number} T.O.Q.NumberLike
10+
//// * @type {/*4*/T./*1TypeMember*/NumberLike}
11+
//// */
12+
//// var x;
13+
//// /** @type {/*5*/T./*2TypeMember*/O.Q.NumberLike} */
14+
//// var x1;
15+
//// /** @type {/*6*/T./*3TypeMember*/People} */
16+
//// var x1;
17+
//// /*globalValue*/
18+
19+
interface VeriferCompletionsInJsDoc {
20+
verifyType(symbol: string, kind: string): void;
21+
verifyValue(symbol: string, kind: string): void;
22+
verifyTypeMember(symbol: string, kind: string): void;
23+
}
24+
25+
function verifyCompletionsInJsDocType(marker: string, { verifyType, verifyValue, verifyTypeMember }: VeriferCompletionsInJsDoc) {
26+
goTo.marker(marker);
27+
28+
verifyType("T", "module");
29+
30+
// TODO: May be filter keywords based on context
31+
//verifyType("string", "keyword");
32+
//verifyType("number", "keyword");
33+
34+
verifyValue("x", "var");
35+
verifyValue("x1", "var");
36+
37+
verifyTypeMember("NumberLike", "type");
38+
verifyTypeMember("People", "type");
39+
verifyTypeMember("O", "module");
40+
}
41+
42+
function verifySymbolPresentWithKind(symbol: string, kind: string) {
43+
return verify.completionListContains(symbol, /*text*/ undefined, /*documentation*/ undefined, kind);
44+
}
45+
46+
function verifySymbolPresentWithWarning(symbol: string) {
47+
return verifySymbolPresentWithKind(symbol, "warning");
48+
}
49+
50+
for (let i = 1; i <= 6; i++) {
51+
verifyCompletionsInJsDocType(i.toString(), {
52+
verifyType: verifySymbolPresentWithKind,
53+
verifyValue: verifySymbolPresentWithWarning,
54+
verifyTypeMember: verifySymbolPresentWithWarning,
55+
});
56+
}
57+
verifyCompletionsInJsDocType("globalValue", {
58+
verifyType: verifySymbolPresentWithWarning,
59+
verifyValue: verifySymbolPresentWithKind,
60+
verifyTypeMember: verifySymbolPresentWithWarning,
61+
});
62+
for (let i = 1; i <= 3; i++) {
63+
verifyCompletionsInJsDocType(i.toString() + "TypeMember", {
64+
verifyType: verifySymbolPresentWithWarning,
65+
verifyValue: verifySymbolPresentWithWarning,
66+
verifyTypeMember: verifySymbolPresentWithKind,
67+
});
68+
}
69+
goTo.marker("propertyName");
70+
verify.completionListIsEmpty();
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
// @allowNonTsExtensions: true
4+
// @Filename: jsFileJsdocTypedefTagTypeExpressionCompletion2_typedef.js
5+
6+
//// class Foo {
7+
//// constructor(value: number) { this.property1 = "hello"; }
8+
//// static method1() {}
9+
//// method3() { return 3; }
10+
//// /**
11+
//// * @param {string} foo A value.
12+
//// * @returns {number} Another value
13+
//// * @mytag
14+
//// */
15+
//// method4(foo) { return 3; }
16+
//// }
17+
//// /**
18+
//// * @type { /*type*/Foo }
19+
//// */
20+
////var x;
21+
/////*globalValue*/
22+
////x./*valueMember*/
23+
24+
interface VeriferCompletionsInJsDoc {
25+
verifyValueOrType(symbol: string, kind: string): void;
26+
verifyValue(symbol: string, kind: string): void;
27+
verifyValueMember(symbol: string, kind: string): void;
28+
}
29+
30+
function verifyCompletionsInJsDocType(marker: string, { verifyValueOrType, verifyValue, verifyValueMember }: VeriferCompletionsInJsDoc) {
31+
goTo.marker(marker);
32+
33+
verifyValueOrType("Foo", "class");
34+
35+
verifyValue("x", "var");
36+
37+
verifyValueMember("property1", "property");
38+
verifyValueMember("method3", "method");
39+
verifyValueMember("method4", "method");
40+
verifyValueMember("foo", "warning");
41+
}
42+
43+
function verifySymbolPresentWithKind(symbol: string, kind: string) {
44+
return verify.completionListContains(symbol, /*text*/ undefined, /*documentation*/ undefined, kind);
45+
}
46+
47+
function verifySymbolPresentWithWarning(symbol: string) {
48+
return verifySymbolPresentWithKind(symbol, "warning");
49+
}
50+
51+
verifyCompletionsInJsDocType("type", {
52+
verifyValueOrType: verifySymbolPresentWithKind,
53+
verifyValue: verifySymbolPresentWithWarning,
54+
verifyValueMember: verifySymbolPresentWithWarning,
55+
});
56+
verifyCompletionsInJsDocType("globalValue", {
57+
verifyValueOrType: verifySymbolPresentWithKind,
58+
verifyValue: verifySymbolPresentWithKind,
59+
verifyValueMember: verifySymbolPresentWithWarning,
60+
});
61+
verifyCompletionsInJsDocType("valueMember", {
62+
verifyValueOrType: verifySymbolPresentWithWarning,
63+
verifyValue: verifySymbolPresentWithWarning,
64+
verifyValueMember: verifySymbolPresentWithKind,
65+
});

0 commit comments

Comments
 (0)