diff --git a/src/services/completions.ts b/src/services/completions.ts index 725eff8f56a..8c34e7e7de6 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -4,7 +4,7 @@ namespace ts.Completions { export type Log = (message: string) => void; - type SymbolOriginInfo = { type: "this-type" } | SymbolOriginInfoExport; + type SymbolOriginInfo = { type: "this-type" } | { type: "symbol-member" } | SymbolOriginInfoExport; interface SymbolOriginInfoExport { type: "export"; moduleSymbol: Symbol; @@ -208,9 +208,9 @@ namespace ts.Completions { } // We should only have needsConvertPropertyAccess if there's a property access to convert. But see #21790. // Somehow there was a global with a non-identifier name. Hopefully someone will complain about getting a "foo bar" global completion and provide a repro. - else if (needsConvertPropertyAccess && propertyAccessToConvert) { - insertText = `[${quote(name, preferences)}]`; - const dot = findChildOfKind(propertyAccessToConvert, SyntaxKind.DotToken, sourceFile)!; + else if ((origin && origin.type === "symbol-member" || needsConvertPropertyAccess) && propertyAccessToConvert) { + insertText = needsConvertPropertyAccess ? `[${quote(name, preferences)}]` : `[${name}]`; + const dot = findChildOfKind(propertyAccessToConvert!, SyntaxKind.DotToken, sourceFile)!; // If the text after the '.' starts with this name, write over it. Else, add new text. const end = startsWith(name, propertyAccessToConvert.name.text) ? propertyAccessToConvert.name.end : dot.end; replacementSpan = createTextSpanFromBounds(dot.getStart(sourceFile), end); @@ -1064,12 +1064,33 @@ namespace ts.Completions { else { for (const symbol of type.getApparentProperties()) { if (typeChecker.isValidPropertyAccessForCompletions((node.parent), type, symbol)) { - symbols.push(symbol); + addPropertySymbol(symbol); } } } } + function addPropertySymbol(symbol: Symbol) { + // If this is e.g. [Symbol.iterator], add a completion for `Symbol`. + const symbolSymbol = firstDefined(symbol.declarations, decl => { + const name = getNameOfDeclaration(decl); + const leftName = name.kind === SyntaxKind.ComputedPropertyName ? getLeftMostName(name.expression) : undefined; + return leftName && typeChecker.getSymbolAtLocation(leftName); + }); + if (symbolSymbol) { + symbols.push(symbolSymbol); + symbolToOriginInfoMap[getSymbolId(symbolSymbol)] = { type: "symbol-member" }; + } + else { + symbols.push(symbol); + } + } + + /** Given 'a.b.c', returns 'a'. */ + function getLeftMostName(e: Expression): Identifier | undefined { + return isIdentifier(e) ? e : isPropertyAccessExpression(e) ? getLeftMostName(e.expression) : undefined; + } + function tryGetGlobalSymbols(): boolean { const result: GlobalsSearch = tryGetObjectLikeCompletionSymbols() || tryGetImportOrExportClauseCompletionSymbols() diff --git a/tests/cases/fourslash/completionsSymbolMembers.ts b/tests/cases/fourslash/completionsSymbolMembers.ts new file mode 100644 index 00000000000..269443e23dd --- /dev/null +++ b/tests/cases/fourslash/completionsSymbolMembers.ts @@ -0,0 +1,15 @@ +/// + +////declare const Symbol: (s: string) => symbol; +////const s = Symbol("s"); +////interface I { [s]: number }; +////declare const i: I; +////i[|./*i*/|]; +//// +////namespace N { export const s2 = Symbol("s2"); } +////interface J { [N.s2]: number; } +////declare const j: J; +////j[|./*j*/|]; + +verify.completionsAt("i", [{ name: "s", insertText: "[s]", replacementSpan: test.ranges()[0] }], { includeInsertTextCompletions: true }); +verify.completionsAt("j", [{ name: "N", insertText: "[N]", replacementSpan: test.ranges()[1] }], { includeInsertTextCompletions: true })