Skip to content

Commit ee5d1d3

Browse files
committed
Completion list in the type expression should show types
1 parent 893ba1d commit ee5d1d3

7 files changed

+446
-25
lines changed

src/harness/fourslash.ts

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

823823
function filterByTextOrDocumentation(entry: ts.CompletionEntry) {
824824
const details = that.getCompletionEntryDetails(entry.name);
825-
const documentation = ts.displayPartsToString(details.documentation);
826-
const text = ts.displayPartsToString(details.displayParts);
825+
const documentation = details && ts.displayPartsToString(details.documentation);
826+
const text = details && ts.displayPartsToString(details.displayParts);
827827

828828
// If any of the expected values are undefined, assume that users don't
829829
// care about them.
@@ -860,6 +860,9 @@ namespace FourSlash {
860860
if (expectedKind) {
861861
error += "Expected kind: " + expectedKind + " to equal: " + filterCompletions[0].kind + ".";
862862
}
863+
else {
864+
error += "kind: " + filterCompletions[0].kind + ".";
865+
}
863866
if (replacementSpan) {
864867
const spanText = filterCompletions[0].replacementSpan ? stringify(filterCompletions[0].replacementSpan) : undefined;
865868
error += "Expected replacement span: " + stringify(replacementSpan) + " to equal: " + spanText + ".";

src/services/completions.ts

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -354,14 +354,15 @@ namespace ts.Completions {
354354
let requestJsDocTag = false;
355355

356356
let start = timestamp();
357-
const currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853
357+
let currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853
358358
log("getCompletionData: Get current token: " + (timestamp() - start));
359359

360360
start = timestamp();
361361
// Completion not allowed inside comments, bail out if this is the case
362362
const insideComment = isInComment(sourceFile, position, currentToken);
363363
log("getCompletionData: Is inside comment: " + (timestamp() - start));
364364

365+
let insideJsDocTagTypeExpression = false;
365366
if (insideComment) {
366367
if (hasDocComment(sourceFile, position)) {
367368
// The current position is next to the '@' sign, when no tag name being provided yet.
@@ -394,30 +395,28 @@ namespace ts.Completions {
394395
// Completion should work inside certain JsDoc tags. For example:
395396
// /** @type {number | string} */
396397
// Completion should work in the brackets
397-
let insideJsDocTagExpression = false;
398398
const tag = getJsDocTagAtPosition(sourceFile, position);
399399
if (tag) {
400400
if (tag.tagName.pos <= position && position <= tag.tagName.end) {
401401
requestJsDocTagName = true;
402402
}
403-
404-
switch (tag.kind) {
405-
case SyntaxKind.JSDocTypeTag:
406-
case SyntaxKind.JSDocParameterTag:
407-
case SyntaxKind.JSDocReturnTag:
408-
const tagWithExpression = <JSDocTypeTag | JSDocParameterTag | JSDocReturnTag>tag;
409-
if (tagWithExpression.typeExpression) {
410-
insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end;
411-
}
412-
break;
403+
if (isTagWithTypeExpression(tag) && tag.typeExpression) {
404+
currentToken = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ true);
405+
if (!currentToken ||
406+
(!isDeclarationName(currentToken) &&
407+
(currentToken.parent.kind !== SyntaxKind.JSDocPropertyTag ||
408+
(<JSDocPropertyTag>currentToken.parent).name !== currentToken))) {
409+
// Use as type location if inside tag's type expression
410+
insideJsDocTagTypeExpression = isCurrentlyEditingNode(tag.typeExpression);
411+
}
413412
}
414413
}
415414

416415
if (requestJsDocTagName || requestJsDocTag) {
417416
return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords: false };
418417
}
419418

420-
if (!insideJsDocTagExpression) {
419+
if (!insideJsDocTagTypeExpression) {
421420
// Proceed if the current position is in jsDoc tag expression; otherwise it is a normal
422421
// comment or the plain text part of a jsDoc comment, so no completion should be available
423422
log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment.");
@@ -426,7 +425,7 @@ namespace ts.Completions {
426425
}
427426

428427
start = timestamp();
429-
const previousToken = findPrecedingToken(position, sourceFile);
428+
const previousToken = findPrecedingToken(position, sourceFile, /*startNode*/ undefined, insideJsDocTagTypeExpression);
430429
log("getCompletionData: Get previous token 1: " + (timestamp() - start));
431430

432431
// The decision to provide completion depends on the contextToken, which is determined through the previousToken.
@@ -437,7 +436,7 @@ namespace ts.Completions {
437436
// Skip this partial identifier and adjust the contextToken to the token that precedes it.
438437
if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) {
439438
const start = timestamp();
440-
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile);
439+
contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile, /*startNode*/ undefined, insideJsDocTagTypeExpression);
441440
log("getCompletionData: Get previous token 2: " + (timestamp() - start));
442441
}
443442

@@ -449,7 +448,7 @@ namespace ts.Completions {
449448
let isRightOfOpenTag = false;
450449
let isStartingCloseTag = false;
451450

452-
let location = getTouchingPropertyName(sourceFile, position, /*includeJsDocComment*/ false); // TODO: GH#15853
451+
let location = getTouchingPropertyName(sourceFile, position, insideJsDocTagTypeExpression); // TODO: GH#15853
453452
if (contextToken) {
454453
// Bail out if this is a known invalid completion location
455454
if (isCompletionListBlocker(contextToken)) {
@@ -553,14 +552,28 @@ namespace ts.Completions {
553552

554553
return { symbols, isGlobalCompletion, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), requestJsDocTagName, requestJsDocTag, hasFilteredClassMemberKeywords };
555554

555+
type JSDocTagWithTypeExpression = JSDocAugmentsTag | JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
556+
557+
function isTagWithTypeExpression(tag: JSDocTag): tag is JSDocTagWithTypeExpression {
558+
switch (tag.kind) {
559+
case SyntaxKind.JSDocAugmentsTag:
560+
case SyntaxKind.JSDocParameterTag:
561+
case SyntaxKind.JSDocPropertyTag:
562+
case SyntaxKind.JSDocReturnTag:
563+
case SyntaxKind.JSDocTypeTag:
564+
case SyntaxKind.JSDocTypedefTag:
565+
return true;
566+
}
567+
}
568+
556569
function getTypeScriptMemberSymbols(): void {
557570
// Right of dot member completion list
558571
isGlobalCompletion = false;
559572
isMemberCompletion = true;
560573
isNewIdentifierLocation = false;
561574

562575
// Since this is qualified name check its a type node location
563-
const isTypeLocation = isPartOfTypeNode(node.parent);
576+
const isTypeLocation = isPartOfTypeNode(node.parent) || insideJsDocTagTypeExpression;
564577
const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node);
565578
if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName || node.kind === SyntaxKind.PropertyAccessExpression) {
566579
let symbol = typeChecker.getSymbolAtLocation(node);
@@ -722,8 +735,9 @@ namespace ts.Completions {
722735
return !!(symbol.flags & SymbolFlags.Namespace);
723736
}
724737

725-
if (!isContextTokenValueLocation(contextToken) &&
726-
(isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken))) {
738+
if (insideJsDocTagTypeExpression ||
739+
(!isContextTokenValueLocation(contextToken) &&
740+
(isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)))) {
727741
// Its a type, but you can reach it by namespace.type as well
728742
return symbolCanbeReferencedAtTypeLocation(symbol);
729743
}
@@ -770,14 +784,16 @@ namespace ts.Completions {
770784
symbol = typeChecker.getAliasedSymbol(symbol);
771785
}
772786

787+
if (symbol.flags & SymbolFlags.Type) {
788+
return true;
789+
}
790+
773791
if (symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule)) {
774792
const exportedSymbols = typeChecker.getExportsOfModule(symbol);
775793
// If the exported symbols contains type,
776794
// symbol can be referenced at locations where type is allowed
777795
return forEach(exportedSymbols, symbolCanbeReferencedAtTypeLocation);
778796
}
779-
780-
return !!(symbol.flags & (SymbolFlags.NamespaceModule | SymbolFlags.Type));
781797
}
782798

783799
/**

src/services/utilities.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ namespace ts {
707707
}
708708
}
709709

710-
export function findPrecedingToken(position: number, sourceFile: SourceFile, startNode?: Node): Node {
710+
export function findPrecedingToken(position: number, sourceFile: SourceFile, startNode?: Node, includeJsDocComment?: boolean): Node {
711711
return find(startNode || sourceFile);
712712

713713
function findRightmostToken(n: Node): Node {
@@ -738,7 +738,7 @@ namespace ts {
738738
// NOTE: JsxText is a weird kind of node that can contain only whitespaces (since they are not counted as trivia).
739739
// if this is the case - then we should assume that token in question is located in previous child.
740740
if (position < child.end && (nodeHasTokens(child) || child.kind === SyntaxKind.JsxText)) {
741-
const start = child.getStart(sourceFile);
741+
const start = child.getStart(sourceFile, includeJsDocComment);
742742
const lookInPreviousChild =
743743
(start >= position) || // cursor in the leading trivia
744744
(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)