diff --git a/src/services/breakpoints.ts b/src/services/breakpoints.ts index fea4ae6bffad0..e5a419912553c 100644 --- a/src/services/breakpoints.ts +++ b/src/services/breakpoints.ts @@ -722,5 +722,5 @@ namespace ts.BreakpointResolver { return spanInNode(node.parent); } } - } + } } diff --git a/src/services/services.ts b/src/services/services.ts index 4187e2245ecfd..fa4333b749286 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1045,10 +1045,10 @@ namespace ts { getCancellationToken?(): HostCancellationToken; getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; - log? (s: string): void; - trace? (s: string): void; - error? (s: string): void; - useCaseSensitiveFileNames? (): boolean; + log?(s: string): void; + trace?(s: string): void; + error?(s: string): void; + useCaseSensitiveFileNames?(): boolean; /* * LS host can optionally implement this method if it wants to be completely in charge of module name resolution. @@ -2480,7 +2480,7 @@ namespace ts { } // should be start of dependency list - if (token !== SyntaxKind.OpenBracketToken) { + if (token !== SyntaxKind.OpenBracketToken) { return true; } @@ -4013,10 +4013,15 @@ namespace ts { } } - function getCompletionsAtPosition(fileName: string, position: number): CompletionInfo { synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + + if (isInString(sourceFile, position)) { + return getStringLiteralCompletionEntries(sourceFile, position); + } + const completionData = getCompletionData(fileName, position); if (!completionData) { return undefined; @@ -4029,12 +4034,10 @@ namespace ts { return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: getAllJsDocCompletionEntries() }; } - const sourceFile = getValidSourceFile(fileName); - const entries: CompletionEntry[] = []; if (isSourceFileJavaScript(sourceFile)) { - const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries); + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ false); addRange(entries, getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames)); } else { @@ -4058,7 +4061,7 @@ namespace ts { } } - getCompletionEntriesFromSymbols(symbols, entries); + getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true); } // Add keywords if this is not a member completion list @@ -4108,11 +4111,11 @@ namespace ts { })); } - function createCompletionEntry(symbol: Symbol, location: Node): CompletionEntry { + function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean): CompletionEntry { // Try to get a valid display name for this symbol, if we could not find one, then ignore it. // We would like to only show things that can be added after a dot, so for instance numeric properties can // not be accessed with a dot (a.1 <- invalid) - const displayName = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, /*performCharacterChecks*/ true, location); + const displayName = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, performCharacterChecks, location); if (!displayName) { return undefined; } @@ -4133,12 +4136,12 @@ namespace ts { }; } - function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: CompletionEntry[]): Map { + function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: CompletionEntry[], location: Node, performCharacterChecks: boolean): Map { const start = new Date().getTime(); const uniqueNames: Map = {}; if (symbols) { for (const symbol of symbols) { - const entry = createCompletionEntry(symbol, location); + const entry = createCompletionEntry(symbol, location, performCharacterChecks); if (entry) { const id = escapeIdentifier(entry.name); if (!lookUp(uniqueNames, id)) { @@ -4152,6 +4155,93 @@ namespace ts { log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (new Date().getTime() - start)); return uniqueNames; } + + function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number) { + const node = findPrecedingToken(position, sourceFile); + if (!node || node.kind !== SyntaxKind.StringLiteral) { + return undefined; + } + + const argumentInfo = SignatureHelp.getContainingArgumentInfo(node, position, sourceFile); + if (argumentInfo) { + // Get string literal completions from specialized signatures of the target + return getStringLiteralCompletionEntriesFromCallExpression(argumentInfo); + } + else if (isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) { + // Get all names of properties on the expression + return getStringLiteralCompletionEntriesFromElementAccess(node.parent); + } + else { + // Otherwise, get the completions from the contextual type if one exists + return getStringLiteralCompletionEntriesFromContextualType(node); + } + } + + function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo) { + const typeChecker = program.getTypeChecker(); + const candidates: Signature[] = []; + const entries: CompletionEntry[] = []; + + typeChecker.getResolvedSignature(argumentInfo.invocation, candidates); + + for (const candidate of candidates) { + if (candidate.parameters.length > argumentInfo.argumentIndex) { + const parameter = candidate.parameters[argumentInfo.argumentIndex]; + addStringLiteralCompletionsFromType(typeChecker.getTypeAtLocation(parameter.valueDeclaration), entries); + } + } + + if (entries.length) { + return { isMemberCompletion: false, isNewIdentifierLocation: true, entries }; + } + + return undefined; + } + + function getStringLiteralCompletionEntriesFromElementAccess(node: ElementAccessExpression) { + const typeChecker = program.getTypeChecker(); + const type = typeChecker.getTypeAtLocation(node.expression); + const entries: CompletionEntry[] = []; + if (type) { + getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, node, /*performCharacterChecks*/false); + if (entries.length) { + return { isMemberCompletion: true, isNewIdentifierLocation: true, entries }; + } + } + return undefined; + } + + function getStringLiteralCompletionEntriesFromContextualType(node: StringLiteral) { + const typeChecker = program.getTypeChecker(); + const type = typeChecker.getContextualType(node); + if (type) { + const entries: CompletionEntry[] = []; + addStringLiteralCompletionsFromType(type, entries); + if (entries.length) { + return { isMemberCompletion: false, isNewIdentifierLocation: false, entries }; + } + } + return undefined; + } + + function addStringLiteralCompletionsFromType(type: Type, result: CompletionEntry[]): void { + if (!type) { + return; + } + if (type.flags & TypeFlags.Union) { + forEach((type).types, t => addStringLiteralCompletionsFromType(t, result)); + } + else { + if (type.flags & TypeFlags.StringLiteral) { + result.push({ + name: (type).text, + kindModifiers: ScriptElementKindModifier.none, + kind: ScriptElementKind.variableElement, + sortText: "0" + }); + } + } + } } function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { @@ -4316,7 +4406,7 @@ namespace ts { // try get the call/construct signature from the type if it matches let callExpression: CallExpression; if (location.kind === SyntaxKind.CallExpression || location.kind === SyntaxKind.NewExpression) { - callExpression = location; + callExpression = location; } else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) { callExpression = location.parent; diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index 270774064894f..34c4ad5062a55 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -162,6 +162,7 @@ namespace ts.SignatureHelp { // // Did not find matching token // return null; // } + const emptyArray: any[] = []; const enum ArgumentListKind { @@ -170,7 +171,7 @@ namespace ts.SignatureHelp { TaggedTemplateArguments } - interface ArgumentListInfo { + export interface ArgumentListInfo { kind: ArgumentListKind; invocation: CallLikeExpression; argumentsSpan: TextSpan; @@ -188,7 +189,8 @@ namespace ts.SignatureHelp { return undefined; } - const argumentInfo = getContainingArgumentInfo(startingToken); + const argumentInfo = getContainingArgumentInfo(startingToken, position, sourceFile); + cancellationToken.throwIfCancellationRequested(); // Semantic filtering of signature help @@ -205,431 +207,432 @@ namespace ts.SignatureHelp { // We didn't have any sig help items produced by the TS compiler. If this is a JS // file, then see if we can figure out anything better. if (isSourceFileJavaScript(sourceFile)) { - return createJavaScriptSignatureHelpItems(argumentInfo); + return createJavaScriptSignatureHelpItems(argumentInfo, program); } return undefined; } - return createSignatureHelpItems(candidates, resolvedSignature, argumentInfo); + return createSignatureHelpItems(candidates, resolvedSignature, argumentInfo, typeChecker); + } - function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo): SignatureHelpItems { - if (argumentInfo.invocation.kind !== SyntaxKind.CallExpression) { - return undefined; - } + function createJavaScriptSignatureHelpItems(argumentInfo: ArgumentListInfo, program: Program): SignatureHelpItems { + if (argumentInfo.invocation.kind !== SyntaxKind.CallExpression) { + return undefined; + } - // See if we can find some symbol with the call expression name that has call signatures. - const callExpression = argumentInfo.invocation; - const expression = callExpression.expression; - const name = expression.kind === SyntaxKind.Identifier - ? expression - : expression.kind === SyntaxKind.PropertyAccessExpression - ? (expression).name - : undefined; + // See if we can find some symbol with the call expression name that has call signatures. + const callExpression = argumentInfo.invocation; + const expression = callExpression.expression; + const name = expression.kind === SyntaxKind.Identifier + ? expression + : expression.kind === SyntaxKind.PropertyAccessExpression + ? (expression).name + : undefined; - if (!name || !name.text) { - return undefined; - } + if (!name || !name.text) { + return undefined; + } - const typeChecker = program.getTypeChecker(); - for (const sourceFile of program.getSourceFiles()) { - const nameToDeclarations = sourceFile.getNamedDeclarations(); - const declarations = getProperty(nameToDeclarations, name.text); - - if (declarations) { - for (const declaration of declarations) { - const symbol = declaration.symbol; - if (symbol) { - const type = typeChecker.getTypeOfSymbolAtLocation(symbol, declaration); - if (type) { - const callSignatures = type.getCallSignatures(); - if (callSignatures && callSignatures.length) { - return createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo); - } + const typeChecker = program.getTypeChecker(); + for (const sourceFile of program.getSourceFiles()) { + const nameToDeclarations = sourceFile.getNamedDeclarations(); + const declarations = getProperty(nameToDeclarations, name.text); + + if (declarations) { + for (const declaration of declarations) { + const symbol = declaration.symbol; + if (symbol) { + const type = typeChecker.getTypeOfSymbolAtLocation(symbol, declaration); + if (type) { + const callSignatures = type.getCallSignatures(); + if (callSignatures && callSignatures.length) { + return createSignatureHelpItems(callSignatures, callSignatures[0], argumentInfo, typeChecker); } } } } } } + } - /** - * Returns relevant information for the argument list and the current argument if we are - * in the argument of an invocation; returns undefined otherwise. - */ - function getImmediatelyContainingArgumentInfo(node: Node): ArgumentListInfo { - if (node.parent.kind === SyntaxKind.CallExpression || node.parent.kind === SyntaxKind.NewExpression) { - const callExpression = node.parent; - // There are 3 cases to handle: - // 1. The token introduces a list, and should begin a sig help session - // 2. The token is either not associated with a list, or ends a list, so the session should end - // 3. The token is buried inside a list, and should give sig help - // - // The following are examples of each: - // - // Case 1: - // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a sig help session - // Case 2: - // fo#o#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end - // Case 3: - // foo(a#, #b#) -> The token is buried inside a list, and should give sig help - // Find out if 'node' is an argument, a type argument, or neither - if (node.kind === SyntaxKind.LessThanToken || - node.kind === SyntaxKind.OpenParenToken) { - // Find the list that starts right *after* the < or ( token. - // If the user has just opened a list, consider this item 0. - const list = getChildListThatStartsWithOpenerToken(callExpression, node, sourceFile); - const isTypeArgList = callExpression.typeArguments && callExpression.typeArguments.pos === list.pos; - Debug.assert(list !== undefined); - return { - kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, - invocation: callExpression, - argumentsSpan: getApplicableSpanForArguments(list), - argumentIndex: 0, - argumentCount: getArgumentCount(list) - }; - } - - // findListItemInfo can return undefined if we are not in parent's argument list - // or type argument list. This includes cases where the cursor is: - // - To the right of the closing paren, non-substitution template, or template tail. - // - Between the type arguments and the arguments (greater than token) - // - On the target of the call (parent.func) - // - On the 'new' keyword in a 'new' expression - const listItemInfo = findListItemInfo(node); - if (listItemInfo) { - const list = listItemInfo.list; - const isTypeArgList = callExpression.typeArguments && callExpression.typeArguments.pos === list.pos; - - const argumentIndex = getArgumentIndex(list, node); - const argumentCount = getArgumentCount(list); - - Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, - `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`); - - return { - kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, - invocation: callExpression, - argumentsSpan: getApplicableSpanForArguments(list), - argumentIndex: argumentIndex, - argumentCount: argumentCount - }; - } - } - else if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.parent.kind === SyntaxKind.TaggedTemplateExpression) { - // Check if we're actually inside the template; - // otherwise we'll fall out and return undefined. - if (isInsideTemplateLiteral(node, position)) { - return getArgumentListInfoForTemplate(node.parent, /*argumentIndex*/ 0); - } + /** + * Returns relevant information for the argument list and the current argument if we are + * in the argument of an invocation; returns undefined otherwise. + */ + function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo { + if (node.parent.kind === SyntaxKind.CallExpression || node.parent.kind === SyntaxKind.NewExpression) { + const callExpression = node.parent; + // There are 3 cases to handle: + // 1. The token introduces a list, and should begin a sig help session + // 2. The token is either not associated with a list, or ends a list, so the session should end + // 3. The token is buried inside a list, and should give sig help + // + // The following are examples of each: + // + // Case 1: + // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a sig help session + // Case 2: + // fo#o#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end + // Case 3: + // foo(a#, #b#) -> The token is buried inside a list, and should give sig help + // Find out if 'node' is an argument, a type argument, or neither + if (node.kind === SyntaxKind.LessThanToken || + node.kind === SyntaxKind.OpenParenToken) { + // Find the list that starts right *after* the < or ( token. + // If the user has just opened a list, consider this item 0. + const list = getChildListThatStartsWithOpenerToken(callExpression, node, sourceFile); + const isTypeArgList = callExpression.typeArguments && callExpression.typeArguments.pos === list.pos; + Debug.assert(list !== undefined); + return { + kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, + invocation: callExpression, + argumentsSpan: getApplicableSpanForArguments(list, sourceFile), + argumentIndex: 0, + argumentCount: getArgumentCount(list) + }; } - else if (node.kind === SyntaxKind.TemplateHead && node.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { - const templateExpression = node.parent; - const tagExpression = templateExpression.parent; - Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); - const argumentIndex = isInsideTemplateLiteral(node, position) ? 0 : 1; + // findListItemInfo can return undefined if we are not in parent's argument list + // or type argument list. This includes cases where the cursor is: + // - To the right of the closing paren, non-substitution template, or template tail. + // - Between the type arguments and the arguments (greater than token) + // - On the target of the call (parent.func) + // - On the 'new' keyword in a 'new' expression + const listItemInfo = findListItemInfo(node); + if (listItemInfo) { + const list = listItemInfo.list; + const isTypeArgList = callExpression.typeArguments && callExpression.typeArguments.pos === list.pos; - return getArgumentListInfoForTemplate(tagExpression, argumentIndex); - } - else if (node.parent.kind === SyntaxKind.TemplateSpan && node.parent.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { - const templateSpan = node.parent; - const templateExpression = templateSpan.parent; - const tagExpression = templateExpression.parent; - Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); - - // If we're just after a template tail, don't show signature help. - if (node.kind === SyntaxKind.TemplateTail && !isInsideTemplateLiteral(node, position)) { - return undefined; - } + const argumentIndex = getArgumentIndex(list, node); + const argumentCount = getArgumentCount(list); - const spanIndex = templateExpression.templateSpans.indexOf(templateSpan); - const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node); + Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, + `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`); - return getArgumentListInfoForTemplate(tagExpression, argumentIndex); + return { + kind: isTypeArgList ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments, + invocation: callExpression, + argumentsSpan: getApplicableSpanForArguments(list, sourceFile), + argumentIndex: argumentIndex, + argumentCount: argumentCount + }; } - return undefined; } - - function getArgumentIndex(argumentsList: Node, node: Node) { - // The list we got back can include commas. In the presence of errors it may - // also just have nodes without commas. For example "Foo(a b c)" will have 3 - // args without commas. We want to find what index we're at. So we count - // forward until we hit ourselves, only incrementing the index if it isn't a - // comma. - // - // Note: the subtlety around trailing commas (in getArgumentCount) does not apply - // here. That's because we're only walking forward until we hit the node we're - // on. In that case, even if we're after the trailing comma, we'll still see - // that trailing comma in the list, and we'll have generated the appropriate - // arg index. - let argumentIndex = 0; - const listChildren = argumentsList.getChildren(); - for (const child of listChildren) { - if (child === node) { - break; - } - if (child.kind !== SyntaxKind.CommaToken) { - argumentIndex++; - } + else if (node.kind === SyntaxKind.NoSubstitutionTemplateLiteral && node.parent.kind === SyntaxKind.TaggedTemplateExpression) { + // Check if we're actually inside the template; + // otherwise we'll fall out and return undefined. + if (isInsideTemplateLiteral(node, position)) { + return getArgumentListInfoForTemplate(node.parent, /*argumentIndex*/ 0, sourceFile); } - - return argumentIndex; } + else if (node.kind === SyntaxKind.TemplateHead && node.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { + const templateExpression = node.parent; + const tagExpression = templateExpression.parent; + Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); - function getArgumentCount(argumentsList: Node) { - // The argument count for a list is normally the number of non-comma children it has. - // For example, if you have "Foo(a,b)" then there will be three children of the arg - // list 'a' '' 'b'. So, in this case the arg count will be 2. However, there - // is a small subtlety. If you have "Foo(a,)", then the child list will just have - // 'a' ''. So, in the case where the last child is a comma, we increase the - // arg count by one to compensate. - // - // Note: this subtlety only applies to the last comma. If you had "Foo(a,," then - // we'll have: 'a' '' '' - // That will give us 2 non-commas. We then add one for the last comma, givin us an - // arg count of 3. - const listChildren = argumentsList.getChildren(); - - let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken); - if (listChildren.length > 0 && lastOrUndefined(listChildren).kind === SyntaxKind.CommaToken) { - argumentCount++; + const argumentIndex = isInsideTemplateLiteral(node, position) ? 0 : 1; + + return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); + } + else if (node.parent.kind === SyntaxKind.TemplateSpan && node.parent.parent.parent.kind === SyntaxKind.TaggedTemplateExpression) { + const templateSpan = node.parent; + const templateExpression = templateSpan.parent; + const tagExpression = templateExpression.parent; + Debug.assert(templateExpression.kind === SyntaxKind.TemplateExpression); + + // If we're just after a template tail, don't show signature help. + if (node.kind === SyntaxKind.TemplateTail && !isInsideTemplateLiteral(node, position)) { + return undefined; } - return argumentCount; + const spanIndex = templateExpression.templateSpans.indexOf(templateSpan); + const argumentIndex = getArgumentIndexForTemplatePiece(spanIndex, node, position); + + return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); } - // spanIndex is either the index for a given template span. - // This does not give appropriate results for a NoSubstitutionTemplateLiteral - function getArgumentIndexForTemplatePiece(spanIndex: number, node: Node): number { - // Because the TemplateStringsArray is the first argument, we have to offset each substitution expression by 1. - // There are three cases we can encounter: - // 1. We are precisely in the template literal (argIndex = 0). - // 2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1). - // 3. We are directly to the right of the template literal, but because we look for the token on the left, - // not enough to put us in the substitution expression; we should consider ourselves part of - // the *next* span's expression by offsetting the index (argIndex = (spanIndex + 1) + 1). - // - // Example: f `# abcd $#{# 1 + 1# }# efghi ${ #"#hello"# } # ` - // ^ ^ ^ ^ ^ ^ ^ ^ ^ - // Case: 1 1 3 2 1 3 2 2 1 - Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); - if (isTemplateLiteralKind(node.kind)) { - if (isInsideTemplateLiteral(node, position)) { - return 0; - } - return spanIndex + 2; + return undefined; + } + + function getArgumentIndex(argumentsList: Node, node: Node) { + // The list we got back can include commas. In the presence of errors it may + // also just have nodes without commas. For example "Foo(a b c)" will have 3 + // args without commas. We want to find what index we're at. So we count + // forward until we hit ourselves, only incrementing the index if it isn't a + // comma. + // + // Note: the subtlety around trailing commas (in getArgumentCount) does not apply + // here. That's because we're only walking forward until we hit the node we're + // on. In that case, even if we're after the trailing comma, we'll still see + // that trailing comma in the list, and we'll have generated the appropriate + // arg index. + let argumentIndex = 0; + const listChildren = argumentsList.getChildren(); + for (const child of listChildren) { + if (child === node) { + break; + } + if (child.kind !== SyntaxKind.CommaToken) { + argumentIndex++; } - return spanIndex + 1; } - function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number): ArgumentListInfo { - // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. - const argumentCount = tagExpression.template.kind === SyntaxKind.NoSubstitutionTemplateLiteral - ? 1 - : (tagExpression.template).templateSpans.length + 1; + return argumentIndex; + } + + function getArgumentCount(argumentsList: Node) { + // The argument count for a list is normally the number of non-comma children it has. + // For example, if you have "Foo(a,b)" then there will be three children of the arg + // list 'a' '' 'b'. So, in this case the arg count will be 2. However, there + // is a small subtlety. If you have "Foo(a,)", then the child list will just have + // 'a' ''. So, in the case where the last child is a comma, we increase the + // arg count by one to compensate. + // + // Note: this subtlety only applies to the last comma. If you had "Foo(a,," then + // we'll have: 'a' '' '' + // That will give us 2 non-commas. We then add one for the last comma, givin us an + // arg count of 3. + const listChildren = argumentsList.getChildren(); + + let argumentCount = countWhere(listChildren, arg => arg.kind !== SyntaxKind.CommaToken); + if (listChildren.length > 0 && lastOrUndefined(listChildren).kind === SyntaxKind.CommaToken) { + argumentCount++; + } - Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`); + return argumentCount; + } - return { - kind: ArgumentListKind.TaggedTemplateArguments, - invocation: tagExpression, - argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression), - argumentIndex: argumentIndex, - argumentCount: argumentCount - }; + // spanIndex is either the index for a given template span. + // This does not give appropriate results for a NoSubstitutionTemplateLiteral + function getArgumentIndexForTemplatePiece(spanIndex: number, node: Node, position: number): number { + // Because the TemplateStringsArray is the first argument, we have to offset each substitution expression by 1. + // There are three cases we can encounter: + // 1. We are precisely in the template literal (argIndex = 0). + // 2. We are in or to the right of the substitution expression (argIndex = spanIndex + 1). + // 3. We are directly to the right of the template literal, but because we look for the token on the left, + // not enough to put us in the substitution expression; we should consider ourselves part of + // the *next* span's expression by offsetting the index (argIndex = (spanIndex + 1) + 1). + // + // Example: f `# abcd $#{# 1 + 1# }# efghi ${ #"#hello"# } # ` + // ^ ^ ^ ^ ^ ^ ^ ^ ^ + // Case: 1 1 3 2 1 3 2 2 1 + Debug.assert(position >= node.getStart(), "Assumed 'position' could not occur before node."); + if (isTemplateLiteralKind(node.kind)) { + if (isInsideTemplateLiteral(node, position)) { + return 0; + } + return spanIndex + 2; } + return spanIndex + 1; + } - function getApplicableSpanForArguments(argumentsList: Node): TextSpan { - // We use full start and skip trivia on the end because we want to include trivia on - // both sides. For example, - // - // foo( /*comment */ a, b, c /*comment*/ ) - // | | - // - // The applicable span is from the first bar to the second bar (inclusive, - // but not including parentheses) - const applicableSpanStart = argumentsList.getFullStart(); - const applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false); - return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); + function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number, sourceFile: SourceFile): ArgumentListInfo { + // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. + const argumentCount = tagExpression.template.kind === SyntaxKind.NoSubstitutionTemplateLiteral + ? 1 + : (tagExpression.template).templateSpans.length + 1; + + Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`); + + return { + kind: ArgumentListKind.TaggedTemplateArguments, + invocation: tagExpression, + argumentsSpan: getApplicableSpanForTaggedTemplate(tagExpression, sourceFile), + argumentIndex: argumentIndex, + argumentCount: argumentCount + }; + } + + function getApplicableSpanForArguments(argumentsList: Node, sourceFile: SourceFile): TextSpan { + // We use full start and skip trivia on the end because we want to include trivia on + // both sides. For example, + // + // foo( /*comment */ a, b, c /*comment*/ ) + // | | + // + // The applicable span is from the first bar to the second bar (inclusive, + // but not including parentheses) + const applicableSpanStart = argumentsList.getFullStart(); + const applicableSpanEnd = skipTrivia(sourceFile.text, argumentsList.getEnd(), /*stopAfterLineBreak*/ false); + return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); + } + + function getApplicableSpanForTaggedTemplate(taggedTemplate: TaggedTemplateExpression, sourceFile: SourceFile): TextSpan { + const template = taggedTemplate.template; + const applicableSpanStart = template.getStart(); + let applicableSpanEnd = template.getEnd(); + + // We need to adjust the end position for the case where the template does not have a tail. + // Otherwise, we will not show signature help past the expression. + // For example, + // + // ` ${ 1 + 1 foo(10) + // | | + // + // This is because a Missing node has no width. However, what we actually want is to include trivia + // leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail. + if (template.kind === SyntaxKind.TemplateExpression) { + const lastSpan = lastOrUndefined((template).templateSpans); + if (lastSpan.literal.getFullWidth() === 0) { + applicableSpanEnd = skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false); + } } - function getApplicableSpanForTaggedTemplate(taggedTemplate: TaggedTemplateExpression): TextSpan { - const template = taggedTemplate.template; - const applicableSpanStart = template.getStart(); - let applicableSpanEnd = template.getEnd(); + return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); + } - // We need to adjust the end position for the case where the template does not have a tail. - // Otherwise, we will not show signature help past the expression. - // For example, - // - // ` ${ 1 + 1 foo(10) - // | | - // - // This is because a Missing node has no width. However, what we actually want is to include trivia - // leading up to the next token in case the user is about to type in a TemplateMiddle or TemplateTail. - if (template.kind === SyntaxKind.TemplateExpression) { - const lastSpan = lastOrUndefined((template).templateSpans); - if (lastSpan.literal.getFullWidth() === 0) { - applicableSpanEnd = skipTrivia(sourceFile.text, applicableSpanEnd, /*stopAfterLineBreak*/ false); - } + export function getContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo { + for (let n = node; n.kind !== SyntaxKind.SourceFile; n = n.parent) { + if (isFunctionBlock(n)) { + return undefined; } - return createTextSpan(applicableSpanStart, applicableSpanEnd - applicableSpanStart); - } + // If the node is not a subspan of its parent, this is a big problem. + // There have been crashes that might be caused by this violation. + if (n.pos < n.parent.pos || n.end > n.parent.end) { + Debug.fail("Node of kind " + n.kind + " is not a subspan of its parent of kind " + n.parent.kind); + } - function getContainingArgumentInfo(node: Node): ArgumentListInfo { - for (let n = node; n.kind !== SyntaxKind.SourceFile; n = n.parent) { - if (isFunctionBlock(n)) { - return undefined; - } + const argumentInfo = getImmediatelyContainingArgumentInfo(n, position, sourceFile); + if (argumentInfo) { + return argumentInfo; + } - // If the node is not a subspan of its parent, this is a big problem. - // There have been crashes that might be caused by this violation. - if (n.pos < n.parent.pos || n.end > n.parent.end) { - Debug.fail("Node of kind " + n.kind + " is not a subspan of its parent of kind " + n.parent.kind); - } - const argumentInfo = getImmediatelyContainingArgumentInfo(n); - if (argumentInfo) { - return argumentInfo; - } + // TODO: Handle generic call with incomplete syntax + } + return undefined; + } + function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node { + const children = parent.getChildren(sourceFile); + const indexOfOpenerToken = children.indexOf(openerToken); + Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); + return children[indexOfOpenerToken + 1]; + } - // TODO: Handle generic call with incomplete syntax + /** + * The selectedItemIndex could be negative for several reasons. + * 1. There are too many arguments for all of the overloads + * 2. None of the overloads were type compatible + * The solution here is to try to pick the best overload by picking + * either the first one that has an appropriate number of parameters, + * or the one with the most parameters. + */ + function selectBestInvalidOverloadIndex(candidates: Signature[], argumentCount: number): number { + let maxParamsSignatureIndex = -1; + let maxParams = -1; + for (let i = 0; i < candidates.length; i++) { + const candidate = candidates[i]; + + if (candidate.hasRestParameter || candidate.parameters.length >= argumentCount) { + return i; } - return undefined; - } - function getChildListThatStartsWithOpenerToken(parent: Node, openerToken: Node, sourceFile: SourceFile): Node { - const children = parent.getChildren(sourceFile); - const indexOfOpenerToken = children.indexOf(openerToken); - Debug.assert(indexOfOpenerToken >= 0 && children.length > indexOfOpenerToken + 1); - return children[indexOfOpenerToken + 1]; + if (candidate.parameters.length > maxParams) { + maxParams = candidate.parameters.length; + maxParamsSignatureIndex = i; + } } - /** - * The selectedItemIndex could be negative for several reasons. - * 1. There are too many arguments for all of the overloads - * 2. None of the overloads were type compatible - * The solution here is to try to pick the best overload by picking - * either the first one that has an appropriate number of parameters, - * or the one with the most parameters. - */ - function selectBestInvalidOverloadIndex(candidates: Signature[], argumentCount: number): number { - let maxParamsSignatureIndex = -1; - let maxParams = -1; - for (let i = 0; i < candidates.length; i++) { - const candidate = candidates[i]; - - if (candidate.hasRestParameter || candidate.parameters.length >= argumentCount) { - return i; - } + return maxParamsSignatureIndex; + } - if (candidate.parameters.length > maxParams) { - maxParams = candidate.parameters.length; - maxParamsSignatureIndex = i; - } + function createSignatureHelpItems(candidates: Signature[], bestSignature: Signature, argumentListInfo: ArgumentListInfo, typeChecker: TypeChecker): SignatureHelpItems { + const applicableSpan = argumentListInfo.argumentsSpan; + const isTypeParameterList = argumentListInfo.kind === ArgumentListKind.TypeArguments; + + const invocation = argumentListInfo.invocation; + const callTarget = getInvokedExpression(invocation); + const callTargetSymbol = typeChecker.getSymbolAtLocation(callTarget); + const callTargetDisplayParts = callTargetSymbol && symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined); + const items: SignatureHelpItem[] = map(candidates, candidateSignature => { + let signatureHelpParameters: SignatureHelpParameter[]; + const prefixDisplayParts: SymbolDisplayPart[] = []; + const suffixDisplayParts: SymbolDisplayPart[] = []; + + if (callTargetDisplayParts) { + addRange(prefixDisplayParts, callTargetDisplayParts); } - return maxParamsSignatureIndex; - } + if (isTypeParameterList) { + prefixDisplayParts.push(punctuationPart(SyntaxKind.LessThanToken)); + const typeParameters = candidateSignature.typeParameters; + signatureHelpParameters = typeParameters && typeParameters.length > 0 ? map(typeParameters, createSignatureHelpParameterForTypeParameter) : emptyArray; + suffixDisplayParts.push(punctuationPart(SyntaxKind.GreaterThanToken)); + const parameterParts = mapToDisplayParts(writer => + typeChecker.getSymbolDisplayBuilder().buildDisplayForParametersAndDelimiters(candidateSignature.thisType, candidateSignature.parameters, writer, invocation)); + addRange(suffixDisplayParts, parameterParts); + } + else { + const typeParameterParts = mapToDisplayParts(writer => + typeChecker.getSymbolDisplayBuilder().buildDisplayForTypeParametersAndDelimiters(candidateSignature.typeParameters, writer, invocation)); + addRange(prefixDisplayParts, typeParameterParts); + prefixDisplayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + + const parameters = candidateSignature.parameters; + signatureHelpParameters = parameters.length > 0 ? map(parameters, createSignatureHelpParameterForParameter) : emptyArray; + suffixDisplayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + } - function createSignatureHelpItems(candidates: Signature[], bestSignature: Signature, argumentListInfo: ArgumentListInfo): SignatureHelpItems { - const applicableSpan = argumentListInfo.argumentsSpan; - const isTypeParameterList = argumentListInfo.kind === ArgumentListKind.TypeArguments; - - const invocation = argumentListInfo.invocation; - const callTarget = getInvokedExpression(invocation); - const callTargetSymbol = typeChecker.getSymbolAtLocation(callTarget); - const callTargetDisplayParts = callTargetSymbol && symbolToDisplayParts(typeChecker, callTargetSymbol, /*enclosingDeclaration*/ undefined, /*meaning*/ undefined); - const items: SignatureHelpItem[] = map(candidates, candidateSignature => { - let signatureHelpParameters: SignatureHelpParameter[]; - const prefixDisplayParts: SymbolDisplayPart[] = []; - const suffixDisplayParts: SymbolDisplayPart[] = []; - - if (callTargetDisplayParts) { - addRange(prefixDisplayParts, callTargetDisplayParts); - } + const returnTypeParts = mapToDisplayParts(writer => + typeChecker.getSymbolDisplayBuilder().buildReturnTypeDisplay(candidateSignature, writer, invocation)); + addRange(suffixDisplayParts, returnTypeParts); - if (isTypeParameterList) { - prefixDisplayParts.push(punctuationPart(SyntaxKind.LessThanToken)); - const typeParameters = candidateSignature.typeParameters; - signatureHelpParameters = typeParameters && typeParameters.length > 0 ? map(typeParameters, createSignatureHelpParameterForTypeParameter) : emptyArray; - suffixDisplayParts.push(punctuationPart(SyntaxKind.GreaterThanToken)); - const parameterParts = mapToDisplayParts(writer => - typeChecker.getSymbolDisplayBuilder().buildDisplayForParametersAndDelimiters(candidateSignature.thisType, candidateSignature.parameters, writer, invocation)); - addRange(suffixDisplayParts, parameterParts); - } - else { - const typeParameterParts = mapToDisplayParts(writer => - typeChecker.getSymbolDisplayBuilder().buildDisplayForTypeParametersAndDelimiters(candidateSignature.typeParameters, writer, invocation)); - addRange(prefixDisplayParts, typeParameterParts); - prefixDisplayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - - const parameters = candidateSignature.parameters; - signatureHelpParameters = parameters.length > 0 ? map(parameters, createSignatureHelpParameterForParameter) : emptyArray; - suffixDisplayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - } + return { + isVariadic: candidateSignature.hasRestParameter, + prefixDisplayParts, + suffixDisplayParts, + separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()], + parameters: signatureHelpParameters, + documentation: candidateSignature.getDocumentationComment() + }; + }); - const returnTypeParts = mapToDisplayParts(writer => - typeChecker.getSymbolDisplayBuilder().buildReturnTypeDisplay(candidateSignature, writer, invocation)); - addRange(suffixDisplayParts, returnTypeParts); + const argumentIndex = argumentListInfo.argumentIndex; - return { - isVariadic: candidateSignature.hasRestParameter, - prefixDisplayParts, - suffixDisplayParts, - separatorDisplayParts: [punctuationPart(SyntaxKind.CommaToken), spacePart()], - parameters: signatureHelpParameters, - documentation: candidateSignature.getDocumentationComment() - }; - }); + // argumentCount is the *apparent* number of arguments. + const argumentCount = argumentListInfo.argumentCount; - const argumentIndex = argumentListInfo.argumentIndex; + let selectedItemIndex = candidates.indexOf(bestSignature); + if (selectedItemIndex < 0) { + selectedItemIndex = selectBestInvalidOverloadIndex(candidates, argumentCount); + } - // argumentCount is the *apparent* number of arguments. - const argumentCount = argumentListInfo.argumentCount; + Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`); - let selectedItemIndex = candidates.indexOf(bestSignature); - if (selectedItemIndex < 0) { - selectedItemIndex = selectBestInvalidOverloadIndex(candidates, argumentCount); - } + return { + items, + applicableSpan, + selectedItemIndex, + argumentIndex, + argumentCount + }; - Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`); + function createSignatureHelpParameterForParameter(parameter: Symbol): SignatureHelpParameter { + const displayParts = mapToDisplayParts(writer => + typeChecker.getSymbolDisplayBuilder().buildParameterDisplay(parameter, writer, invocation)); return { - items, - applicableSpan, - selectedItemIndex, - argumentIndex, - argumentCount + name: parameter.name, + documentation: parameter.getDocumentationComment(), + displayParts, + isOptional: typeChecker.isOptionalParameter(parameter.valueDeclaration) }; + } - function createSignatureHelpParameterForParameter(parameter: Symbol): SignatureHelpParameter { - const displayParts = mapToDisplayParts(writer => - typeChecker.getSymbolDisplayBuilder().buildParameterDisplay(parameter, writer, invocation)); - - return { - name: parameter.name, - documentation: parameter.getDocumentationComment(), - displayParts, - isOptional: typeChecker.isOptionalParameter(parameter.valueDeclaration) - }; - } - - function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter): SignatureHelpParameter { - const displayParts = mapToDisplayParts(writer => - typeChecker.getSymbolDisplayBuilder().buildTypeParameterDisplay(typeParameter, writer, invocation)); + function createSignatureHelpParameterForTypeParameter(typeParameter: TypeParameter): SignatureHelpParameter { + const displayParts = mapToDisplayParts(writer => + typeChecker.getSymbolDisplayBuilder().buildTypeParameterDisplay(typeParameter, writer, invocation)); - return { - name: typeParameter.symbol.name, - documentation: emptyArray, - displayParts, - isOptional: false - }; - } + return { + name: typeParameter.symbol.name, + documentation: emptyArray, + displayParts, + isOptional: false + }; } } } \ No newline at end of file diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 54372877b9a03..d49fa39043571 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -426,9 +426,27 @@ namespace ts { } } - export function isInString(sourceFile: SourceFile, position: number) { - const token = getTokenAtPosition(sourceFile, position); - return token && (token.kind === SyntaxKind.StringLiteral || token.kind === SyntaxKind.StringLiteralType) && position > token.getStart(sourceFile); + export function isInString(sourceFile: SourceFile, position: number): boolean { + const previousToken = findPrecedingToken(position, sourceFile); + if (previousToken && + (previousToken.kind === SyntaxKind.StringLiteral || previousToken.kind === SyntaxKind.StringLiteralType)) { + const start = previousToken.getStart(); + const end = previousToken.getEnd(); + + // To be "in" one of these literals, the position has to be: + // 1. entirely within the token text. + // 2. at the end position of an unterminated token. + // 3. at the end of a regular expression (due to trailing flags like '/foo/g'). + if (start < position && position < end) { + return true; + } + + if (position === end) { + return !!(previousToken).isUnterminated; + } + } + + return false; } export function isInComment(sourceFile: SourceFile, position: number) { diff --git a/tests/cases/fourslash/completionForStringLiteral.ts b/tests/cases/fourslash/completionForStringLiteral.ts new file mode 100644 index 0000000000000..cdbe1589f730d --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteral.ts @@ -0,0 +1,15 @@ +/// + +////type Options = "Option 1" | "Option 2" | "Option 3"; +////var x: Options = "/*1*/Option 3"; +//// +////function f(a: Options) { }; +////f("/*2*/ + +goTo.marker('1'); +verify.completionListContains("Option 1"); +verify.memberListCount(3); + +goTo.marker('2'); +verify.completionListContains("Option 2"); +verify.memberListCount(3); diff --git a/tests/cases/fourslash/completionForStringLiteral2.ts b/tests/cases/fourslash/completionForStringLiteral2.ts new file mode 100644 index 0000000000000..6f0768d6c6bd9 --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteral2.ts @@ -0,0 +1,20 @@ +/// + +////var o = { +//// foo() { }, +//// bar: 0, +//// "some other name": 1 +////}; +//// +////o["/*1*/bar"]; +////o["/*2*/ + +goTo.marker('1'); +verify.completionListContains("foo"); +verify.completionListAllowsNewIdentifier(); +verify.memberListCount(3); + +goTo.marker('2'); +verify.completionListContains("some other name"); +verify.completionListAllowsNewIdentifier(); +verify.memberListCount(3); diff --git a/tests/cases/fourslash/completionForStringLiteral3.ts b/tests/cases/fourslash/completionForStringLiteral3.ts new file mode 100644 index 0000000000000..8c1a7cab2edff --- /dev/null +++ b/tests/cases/fourslash/completionForStringLiteral3.ts @@ -0,0 +1,20 @@ +/// + +////declare function f(a: "A", b: number): void; +////declare function f(a: "B", b: number): void; +////declare function f(a: "C", b: number): void; +////declare function f(a: string, b: number): void; +//// +////f("/*1*/C", 2); +//// +////f("/*2*/ + +goTo.marker('1'); +verify.completionListContains("A"); +verify.completionListAllowsNewIdentifier(); +verify.memberListCount(3); + +goTo.marker('2'); +verify.completionListContains("A"); +verify.completionListAllowsNewIdentifier(); +verify.memberListCount(3); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 71e1c3cc5d708..540a650313845 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -52,13 +52,13 @@ declare module ts { None = 0, Block = 1, Smart = 2, - } + } interface OutputFile { name: string; writeByteOrderMark: boolean; text: string; - } + } } declare namespace FourSlashInterface { @@ -139,7 +139,7 @@ declare namespace FourSlashInterface { isValidBraceCompletionAtPostion(openingBrace?: string): void; } class verify extends verifyNegatable { - assertHasRanges(ranges: FourSlash.Range[]): void; + assertHasRanges(ranges: Range[]): void; caretAtMarker(markerName?: string): void; indentationIs(numberOfSpaces: number): void; indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle?: ts.IndentStyle): void;