diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 8eef1485fe7..ea0f09ff688 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -435,7 +435,7 @@ namespace FourSlash { } } - private markerName(m: Marker): string { + public markerName(m: Marker): string { return ts.forEachEntry(this.testData.markerPositions, (marker, name) => { if (marker === m) { return name; @@ -3768,6 +3768,10 @@ namespace FourSlashInterface { return this.state.getMarkerByName(name); } + public markerName(m: FourSlash.Marker) { + return this.state.markerName(m); + } + public ranges(): FourSlash.Range[] { return this.state.getRanges(); } @@ -3810,6 +3814,7 @@ namespace FourSlashInterface { this.state.goToEachMarker(markers, typeof a === "function" ? a : b); } + public rangeStart(range: FourSlash.Range) { this.state.goToRangeStart(range); } diff --git a/src/services/completions.ts b/src/services/completions.ts index e16cbe9cde6..d6e166a00e0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -994,6 +994,7 @@ namespace ts.Completions { // Since this is qualified name check its a type node location const isTypeLocation = insideJsDocTagTypeExpression || isPartOfTypeNode(node.parent); const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node); + const allowTypeOrValue = isRhsOfImportDeclaration || (!isTypeLocation && isPossiblyTypeArgumentPosition(contextToken, sourceFile)); if (isEntityName(node)) { let symbol = typeChecker.getSymbolAtLocation(node); if (symbol) { @@ -1004,7 +1005,7 @@ namespace ts.Completions { const exportedSymbols = Debug.assertEachDefined(typeChecker.getExportsOfModule(symbol), "getExportsOfModule() should all be defined"); const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess((node.parent), symbol.name); const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); - const isValidAccess = isRhsOfImportDeclaration ? + const isValidAccess = allowTypeOrValue ? // Any kind is allowed when dotting off namespace in internal import equals declaration (symbol: Symbol) => isValidTypeAccess(symbol) || isValidValueAccess(symbol) : isTypeLocation ? isValidTypeAccess : isValidValueAccess; @@ -1173,8 +1174,9 @@ namespace ts.Completions { } function filterGlobalCompletion(symbols: Symbol[]): void { - const isTypeCompletion = insideJsDocTagTypeExpression || !isContextTokenValueLocation(contextToken) && (isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)); - if (isTypeCompletion) keywordFilters = KeywordCompletionFilters.TypeKeywords; + const isTypeOnlyCompletion = insideJsDocTagTypeExpression || !isContextTokenValueLocation(contextToken) && (isPartOfTypeNode(location) || isContextTokenTypeLocation(contextToken)); + const allowTypes = isTypeOnlyCompletion || !isContextTokenValueLocation(contextToken) && isPossiblyTypeArgumentPosition(contextToken, sourceFile); + if (isTypeOnlyCompletion) keywordFilters = KeywordCompletionFilters.TypeKeywords; filterMutate(symbols, symbol => { if (!isSourceFile(location)) { @@ -1190,9 +1192,12 @@ namespace ts.Completions { return !!(symbol.flags & SymbolFlags.Namespace); } - if (isTypeCompletion) { + if (allowTypes) { // Its a type, but you can reach it by namespace.type as well - return symbolCanBeReferencedAtTypeLocation(symbol); + const symbolAllowedAsType = symbolCanBeReferencedAtTypeLocation(symbol); + if (symbolAllowedAsType || isTypeOnlyCompletion) { + return symbolAllowedAsType; + } } } @@ -1204,7 +1209,7 @@ namespace ts.Completions { function isContextTokenValueLocation(contextToken: Node) { return contextToken && contextToken.kind === SyntaxKind.TypeOfKeyword && - contextToken.parent.kind === SyntaxKind.TypeQuery; + (contextToken.parent.kind === SyntaxKind.TypeQuery || isTypeOfExpression(contextToken.parent)); } function isContextTokenTypeLocation(contextToken: Node): boolean { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 36cdb402b8b..122430cdb85 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -890,6 +890,108 @@ namespace ts { return isTemplateLiteralKind(token.kind) && position > token.getStart(sourceFile); } + export function findPrecedingMatchingToken(token: Node, matchingTokenKind: SyntaxKind, sourceFile: SourceFile) { + const tokenKind = token.kind; + let remainingMatchingTokens = 0; + while (true) { + token = findPrecedingToken(token.getFullStart(), sourceFile); + if (!token) { + return undefined; + } + + if (token.kind === matchingTokenKind) { + if (remainingMatchingTokens === 0) { + return token; + } + + remainingMatchingTokens--; + } + else if (token.kind === tokenKind) { + remainingMatchingTokens++; + } + } + } + + export function isPossiblyTypeArgumentPosition(token: Node, sourceFile: SourceFile) { + let remainingLessThanTokens = 0; + while (token) { + switch (token.kind) { + case SyntaxKind.LessThanToken: + // Found the beginning of the generic argument expression + token = findPrecedingToken(token.getFullStart(), sourceFile); + const tokenIsIdentifier = token && isIdentifier(token); + if (!remainingLessThanTokens || !tokenIsIdentifier) { + return tokenIsIdentifier; + } + remainingLessThanTokens--; + break; + + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + remainingLessThanTokens++; + // falls through + case SyntaxKind.GreaterThanGreaterThanToken: + remainingLessThanTokens++; + // falls through + case SyntaxKind.GreaterThanToken: + remainingLessThanTokens++; + break; + + case SyntaxKind.CloseBraceToken: + // This can be object type, skip untill we find the matching open brace token + // Skip untill the matching open brace token + token = findPrecedingMatchingToken(token, SyntaxKind.OpenBraceToken, sourceFile); + if (!token) return false; + break; + + case SyntaxKind.CloseParenToken: + // This can be object type, skip untill we find the matching open brace token + // Skip untill the matching open brace token + token = findPrecedingMatchingToken(token, SyntaxKind.OpenParenToken, sourceFile); + if (!token) return false; + break; + + case SyntaxKind.CloseBracketToken: + // This can be object type, skip untill we find the matching open brace token + // Skip untill the matching open brace token + token = findPrecedingMatchingToken(token, SyntaxKind.OpenBracketToken, sourceFile); + if (!token) return false; + break; + + // Valid tokens in a type name. Skip. + case SyntaxKind.CommaToken: + case SyntaxKind.EqualsGreaterThanToken: + + case SyntaxKind.Identifier: + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + + case SyntaxKind.TypeOfKeyword: + case SyntaxKind.ExtendsKeyword: + case SyntaxKind.KeyOfKeyword: + case SyntaxKind.DotToken: + case SyntaxKind.BarToken: + case SyntaxKind.QuestionToken: + case SyntaxKind.ColonToken: + break; + + default: + if (isTypeNode(token)) { + break; + } + + // Invalid token in type + return false; + } + + token = findPrecedingToken(token.getFullStart(), sourceFile); + } + + return false; + } + + /** * Returns true if the cursor at position in sourceFile is within a comment. * diff --git a/tests/cases/fourslash/completionListInUnclosedTypeArguments.ts b/tests/cases/fourslash/completionListInUnclosedTypeArguments.ts new file mode 100644 index 00000000000..43458be9ff4 --- /dev/null +++ b/tests/cases/fourslash/completionListInUnclosedTypeArguments.ts @@ -0,0 +1,42 @@ +/// + +////let x = 10; +////type Type = void; +////declare function f(): void; +////declare function f2(): void; +////f +////f +////f(); +//// +////f2 +////f2 +////f2(); +//// +////f2/*4x*/T/*5x*/y/*6x*/ +////f2<() =>/*1y*/T/*2y*/y/*3y*/, () =>/*4y*/T/*5y*/y/*6y*/ +////f2/*1z*/T/*2z*/y/*3z*/ + + +goTo.eachMarker((marker) => { + const markerName = test.markerName(marker); + if (markerName.endsWith("TypeOnly")) { + verify.not.completionListContains("x"); + } + else { + verify.completionListContains("x"); + } + + if (markerName.endsWith("ValueOnly")) { + verify.not.completionListContains("Type"); + } + else { + verify.completionListContains("Type"); + } +}); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index b318b05c8c8..fd53a5acf2e 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -113,6 +113,7 @@ declare namespace FourSlashInterface { class test_ { markers(): Marker[]; markerNames(): string[]; + markerName(m: Marker): string; marker(name?: string): Marker; ranges(): Range[]; spans(): Array<{ start: number, length: number }>;