diff --git a/src/services/completions.ts b/src/services/completions.ts index 492aca5b26c7e..bce1eadaf26df 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -78,7 +78,7 @@ namespace ts.Completions { } function completionInfoFromData(sourceFile: SourceFile, typeChecker: TypeChecker, compilerOptions: CompilerOptions, log: Log, completionData: CompletionData, includeInsertTextCompletions: boolean): CompletionInfo { - const { symbols, completionKind, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion } = completionData; + const { symbols, completionKind, isNewIdentifierLocation, location, propertyAccessToConvert, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer } = completionData; if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && isJsxClosingElement(location.parent)) { // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, @@ -99,7 +99,7 @@ namespace ts.Completions { const entries: CompletionEntry[] = []; if (isSourceFileJavaScript(sourceFile)) { - const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, recommendedCompletion, symbolToOriginInfoMap); + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap); getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target, entries); } else { @@ -107,7 +107,7 @@ namespace ts.Completions { return undefined; } - getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, recommendedCompletion, symbolToOriginInfoMap); + getCompletionEntriesFromSymbols(symbols, entries, location, sourceFile, typeChecker, compilerOptions.target, log, completionKind, includeInsertTextCompletions, propertyAccessToConvert, isJsxInitializer, recommendedCompletion, symbolToOriginInfoMap); } // TODO add filter for keyword based on type/value/namespace and also location @@ -167,6 +167,7 @@ namespace ts.Completions { origin: SymbolOriginInfo | undefined, recommendedCompletion: Symbol | undefined, propertyAccessToConvert: PropertyAccessExpression | undefined, + isJsxInitializer: boolean, includeInsertTextCompletions: boolean, ): CompletionEntry | undefined { const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind); @@ -174,19 +175,27 @@ namespace ts.Completions { return undefined; } const { name, needsConvertPropertyAccess } = info; - if (needsConvertPropertyAccess && !includeInsertTextCompletions) { - return undefined; - } let insertText: string | undefined; let replacementSpan: TextSpan | undefined; - if (origin && origin.type === "this-type") { - insertText = needsConvertPropertyAccess ? `this["${name}"]` : `this.${name}`; + if (includeInsertTextCompletions) { + if (origin && origin.type === "this-type") { + insertText = needsConvertPropertyAccess ? `this["${name}"]` : `this.${name}`; + } + else if (needsConvertPropertyAccess) { + // TODO: GH#20619 Use configured quote style + insertText = `["${name}"]`; + replacementSpan = createTextSpanFromBounds(findChildOfKind(propertyAccessToConvert!, SyntaxKind.DotToken, sourceFile)!.getStart(sourceFile), propertyAccessToConvert!.name.end); + } + + if (isJsxInitializer) { + if (insertText === undefined) insertText = name; + insertText = `{${insertText}}`; + } } - else if (needsConvertPropertyAccess) { - // TODO: GH#20619 Use configured quote style - insertText = `["${name}"]`; - replacementSpan = createTextSpanFromBounds(findChildOfKind(propertyAccessToConvert!, SyntaxKind.DotToken, sourceFile)!.getStart(sourceFile), propertyAccessToConvert!.name.end); + + if (insertText !== undefined && !includeInsertTextCompletions) { + return undefined; } // TODO(drosen): Right now we just permit *all* semantic meanings when calling @@ -235,6 +244,7 @@ namespace ts.Completions { kind: CompletionKind, includeInsertTextCompletions?: boolean, propertyAccessToConvert?: PropertyAccessExpression | undefined, + isJsxInitializer?: boolean, recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, ): Map { @@ -246,7 +256,7 @@ namespace ts.Completions { const uniques = createMap(); for (const symbol of symbols) { const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; - const entry = createCompletionEntry(symbol, location, sourceFile, typeChecker, target, kind, origin, recommendedCompletion, propertyAccessToConvert, includeInsertTextCompletions); + const entry = createCompletionEntry(symbol, location, sourceFile, typeChecker, target, kind, origin, recommendedCompletion, propertyAccessToConvert, isJsxInitializer, includeInsertTextCompletions); if (!entry) { continue; } @@ -483,6 +493,7 @@ namespace ts.Completions { location: Node; symbolToOriginInfoMap: SymbolOriginInfoMap; previousToken: Node; + readonly isJsxInitializer: boolean; } function getSymbolCompletionFromEntryId( typeChecker: TypeChecker, @@ -501,7 +512,7 @@ namespace ts.Completions { return { type: "request", request: completionData }; } - const { symbols, location, completionKind, symbolToOriginInfoMap, previousToken } = completionData; + const { symbols, location, completionKind, symbolToOriginInfoMap, previousToken, isJsxInitializer } = completionData; // Find the symbol with the matching entry name. // We don't need to perform character checks here because we're only comparing the @@ -510,7 +521,7 @@ namespace ts.Completions { return firstDefined(symbols, (symbol): SymbolCompletion => { // TODO: Shouldn't need return type annotation (GH#12632) const origin = symbolToOriginInfoMap[getSymbolId(symbol)]; const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target, origin, completionKind); - return info && info.name === name && getSourceFromOrigin(origin) === source ? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken } : undefined; + return info && info.name === name && getSourceFromOrigin(origin) === source ? { type: "symbol" as "symbol", symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer } : undefined; }) || { type: "none" }; } @@ -678,6 +689,7 @@ namespace ts.Completions { readonly symbolToOriginInfoMap: SymbolOriginInfoMap; readonly recommendedCompletion: Symbol | undefined; readonly previousToken: Node | undefined; + readonly isJsxInitializer: boolean; } type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag }; @@ -859,6 +871,7 @@ namespace ts.Completions { let isRightOfDot = false; let isRightOfOpenTag = false; let isStartingCloseTag = false; + let isJsxInitializer = false; let location = getTouchingPropertyName(sourceFile, position, insideJsDocTagTypeExpression); // TODO: GH#15853 if (contextToken) { @@ -917,6 +930,10 @@ namespace ts.Completions { location = contextToken; } break; + + case SyntaxKind.JsxAttribute: + isJsxInitializer = previousToken.kind === SyntaxKind.EqualsToken; + break; } } } @@ -962,7 +979,7 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, typeChecker); - return { kind: CompletionDataKind.Data, symbols, completionKind, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken }; + return { kind: CompletionDataKind.Data, symbols, completionKind, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; diff --git a/tests/cases/fourslash/completionsJsxAttributeInitializer.ts b/tests/cases/fourslash/completionsJsxAttributeInitializer.ts new file mode 100644 index 0000000000000..510e0630de96c --- /dev/null +++ b/tests/cases/fourslash/completionsJsxAttributeInitializer.ts @@ -0,0 +1,23 @@ +/// + +// @Filename: /a.tsx +////function f(this: { p: number; "a b": number }, x: number): void { +////
; +////} + +goTo.marker(); + +verify.completionListContains("x", "(parameter) x: number", "", "parameter", undefined, undefined, { + includeInsertTextCompletions: true, + insertText: "{x}", +}); + +verify.completionListContains("p", "(property) p: number", "", "property", undefined, undefined, { + includeInsertTextCompletions: true, + insertText: "{this.p}", +}); + +verify.completionListContains("a b", '(property) "a b": number', "", "property", undefined, undefined, { + includeInsertTextCompletions: true, + insertText: '{this["a b"]}', +});