diff --git a/src/services/completions.ts b/src/services/completions.ts index 6d1d48b22b828..a6fd37fde6880 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -2,10 +2,12 @@ namespace ts.Completions { export enum SortText { LocationPriority = "0", - SuggestedClassMembers = "1", - GlobalsOrKeywords = "2", - AutoImportSuggestions = "3", - JavascriptIdentifiers = "4" + OptionalMember = "1", + MemberDeclaredBySpreadAssignment = "2", + SuggestedClassMembers = "3", + GlobalsOrKeywords = "4", + AutoImportSuggestions = "5", + JavascriptIdentifiers = "6" } export type Log = (message: string) => void; @@ -1077,6 +1079,7 @@ namespace ts.Completions { const attrsType = jsxContainer && typeChecker.getContextualType(jsxContainer.attributes); if (!attrsType) return GlobalsSearch.Continue; symbols = filterJsxAttributes(getPropertiesForObjectExpression(attrsType, jsxContainer!.attributes, typeChecker), jsxContainer!.attributes.properties); + setSortTextToOptionalMember(); completionKind = CompletionKind.MemberLike; isNewIdentifierLocation = false; return GlobalsSearch.Success; @@ -1495,6 +1498,8 @@ namespace ts.Completions { // Add filtered items to the completion list symbols = filterObjectMembersList(typeMembers, Debug.assertDefined(existingMembers)); } + setSortTextToOptionalMember(); + return GlobalsSearch.Success; } @@ -1866,6 +1871,7 @@ namespace ts.Completions { return contextualMemberSymbols; } + const membersDeclaredBySpreadAssignment = createMap(); const existingMemberNames = createUnderscoreEscapedMap(); for (const m of existingMembers) { // Ignore omitted expressions for missing members @@ -1874,7 +1880,8 @@ namespace ts.Completions { m.kind !== SyntaxKind.BindingElement && m.kind !== SyntaxKind.MethodDeclaration && m.kind !== SyntaxKind.GetAccessor && - m.kind !== SyntaxKind.SetAccessor) { + m.kind !== SyntaxKind.SetAccessor && + m.kind !== SyntaxKind.SpreadAssignment) { continue; } @@ -1885,7 +1892,10 @@ namespace ts.Completions { let existingName: __String | undefined; - if (isBindingElement(m) && m.propertyName) { + if (isSpreadAssignment(m)) { + setMembersDeclaredBySpreadAssignment(m, membersDeclaredBySpreadAssignment); + } + else if (isBindingElement(m) && m.propertyName) { // include only identifiers in completion list if (m.propertyName.kind === SyntaxKind.Identifier) { existingName = m.propertyName.escapedText; @@ -1902,7 +1912,43 @@ namespace ts.Completions { existingMemberNames.set(existingName!, true); // TODO: GH#18217 } - return contextualMemberSymbols.filter(m => !existingMemberNames.get(m.escapedName)); + const filteredSymbols = contextualMemberSymbols.filter(m => !existingMemberNames.get(m.escapedName)); + setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); + + return filteredSymbols; + } + + function setMembersDeclaredBySpreadAssignment(declaration: SpreadAssignment | JsxSpreadAttribute, membersDeclaredBySpreadAssignment: Map) { + const expression = declaration.expression; + const symbol = typeChecker.getSymbolAtLocation(expression); + const type = symbol && typeChecker.getTypeOfSymbolAtLocation(symbol, expression); + const properties = type && (type).properties; + if (properties) { + properties.forEach(property => { + membersDeclaredBySpreadAssignment.set(property.name, true); + }); + } + } + + // Set SortText to OptionalMember if it is an optinoal member + function setSortTextToOptionalMember() { + symbols.forEach(m => { + if (m.flags & SymbolFlags.Optional) { + symbolToSortTextMap[getSymbolId(m)] = symbolToSortTextMap[getSymbolId(m)] || SortText.OptionalMember; + } + }); + } + + // Set SortText to MemberDeclaredBySpreadAssignment if it is fulfilled by spread assignment + function setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment: Map, contextualMemberSymbols: Symbol[]): void { + if (membersDeclaredBySpreadAssignment.size === 0) { + return; + } + for (const contextualMemberSymbol of contextualMemberSymbols) { + if (membersDeclaredBySpreadAssignment.has(contextualMemberSymbol.name)) { + symbolToSortTextMap[getSymbolId(contextualMemberSymbol)] = SortText.MemberDeclaredBySpreadAssignment; + } + } } /** @@ -1956,6 +2002,7 @@ namespace ts.Completions { */ function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray): Symbol[] { const seenNames = createUnderscoreEscapedMap(); + const membersDeclaredBySpreadAssignment = createMap(); for (const attr of attributes) { // If this is the current item we are editing right now, do not filter it out if (isCurrentlyEditingNode(attr)) { @@ -1965,9 +2012,15 @@ namespace ts.Completions { if (attr.kind === SyntaxKind.JsxAttribute) { seenNames.set(attr.name.escapedText, true); } + else if (isJsxSpreadAttribute(attr)) { + setMembersDeclaredBySpreadAssignment(attr, membersDeclaredBySpreadAssignment); + } } + const filteredSymbols = symbols.filter(a => !seenNames.get(a.escapedName)); + + setSortTextToMemberDeclaredBySpreadAssignment(membersDeclaredBySpreadAssignment, filteredSymbols); - return symbols.filter(a => !seenNames.get(a.escapedName)); + return filteredSymbols; } function isCurrentlyEditingNode(node: Node): boolean { diff --git a/tests/cases/fourslash/completionsAtIncompleteObjectLiteralProperty.ts b/tests/cases/fourslash/completionsAtIncompleteObjectLiteralProperty.ts index 130b70de94919..3682b31004034 100644 --- a/tests/cases/fourslash/completionsAtIncompleteObjectLiteralProperty.ts +++ b/tests/cases/fourslash/completionsAtIncompleteObjectLiteralProperty.ts @@ -10,5 +10,5 @@ verify.completions({ marker: "", - exact: ["abc"], + exact: [{ name: 'abc', kind: 'property', kindModifiers: 'declare,optional', sortText: completion.SortText.OptionalMember }], }); diff --git a/tests/cases/fourslash/completionsOptionalMethod.ts b/tests/cases/fourslash/completionsOptionalMethod.ts index 4c70e31606299..b585dca345c04 100644 --- a/tests/cases/fourslash/completionsOptionalMethod.ts +++ b/tests/cases/fourslash/completionsOptionalMethod.ts @@ -5,4 +5,4 @@ ////declare const x: { m?(): void }; ////x./**/ -verify.completions({ marker: "", exact: "m" }); +verify.completions({ marker: "", exact: "m" }); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsPropertiesPriorities.ts b/tests/cases/fourslash/completionsPropertiesPriorities.ts new file mode 100644 index 0000000000000..f2269243e31ed --- /dev/null +++ b/tests/cases/fourslash/completionsPropertiesPriorities.ts @@ -0,0 +1,31 @@ +/// +// @strict: true + +//// interface I { +//// B?: number; +//// a: number; +//// c?: string; +//// d: string +//// } + +//// const foo = { +//// a: 1, +//// B: 2 +//// } + +//// const i: I = { +//// ...foo, +//// /*a*/ +//// } + +verify.completions( + { + marker: ['a'], + exact: [ + { name: 'B', kindModifiers: 'optional', sortText: completion.SortText.MemberDeclaredBySpreadAssignment, kind: 'property' }, + { name: 'a', sortText: completion.SortText.MemberDeclaredBySpreadAssignment, kind: 'property' }, + { name: 'c', kindModifiers: 'optional', sortText: completion.SortText.OptionalMember, kind: 'property' }, + { name: 'd', sortText: completion.SortText.LocationPriority, kind: 'property' } + ] + } +); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsWithOptionalProperties.ts b/tests/cases/fourslash/completionsWithOptionalProperties.ts index e40029ddefac0..78b0564647415 100644 --- a/tests/cases/fourslash/completionsWithOptionalProperties.ts +++ b/tests/cases/fourslash/completionsWithOptionalProperties.ts @@ -13,6 +13,8 @@ verify.completions({ marker: "", - includes: ['world'] + exact: [ + { name: "world", kind: "property", kindModifiers: "optional", sortText: completion.SortText.OptionalMember } + ] }); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 05799a156420a..d242e51d32b6b 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -672,10 +672,12 @@ declare namespace completion { type Entry = FourSlashInterface.ExpectedCompletionEntryObject; export const enum SortText { LocationPriority = "0", - SuggestedClassMembers = "1", - GlobalsOrKeywords = "2", - AutoImportSuggestions = "3", - JavascriptIdentifiers = "4" + OptionalMember = "1", + MemberDeclaredBySpreadAssignment = "2", + SuggestedClassMembers = "3", + GlobalsOrKeywords = "4", + AutoImportSuggestions = "5", + JavascriptIdentifiers = "6" } export const globalThisEntry: Entry; export const undefinedVarEntry: Entry; diff --git a/tests/cases/fourslash/tsxCompletion12.ts b/tests/cases/fourslash/tsxCompletion12.ts index 3b3b3ac547095..843ed1532ffd5 100644 --- a/tests/cases/fourslash/tsxCompletion12.ts +++ b/tests/cases/fourslash/tsxCompletion12.ts @@ -23,7 +23,13 @@ //// let opt4 = ; verify.completions( - { marker: ["1", "2", "5"], exact: ["propx", "propString", "optional"] }, - { marker: "3", exact: ["propString", "optional"] }, + { + marker: ["1", "2", "5"], + exact: ["propx", "propString", { name: "optional", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }] + }, + { + marker: "3", + exact: ["propString", { name: "optional", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }] + }, { marker: "4", exact: "propString" }, ); diff --git a/tests/cases/fourslash/tsxCompletion13.ts b/tests/cases/fourslash/tsxCompletion13.ts index b1ac2c7af7a2a..1fec1223d0cc0 100644 --- a/tests/cases/fourslash/tsxCompletion13.ts +++ b/tests/cases/fourslash/tsxCompletion13.ts @@ -31,7 +31,28 @@ //// let opt = ; verify.completions( - { marker: ["1", "6"], exact: ["onClick", "children", "className", "goTo"] }, - { marker: "2", exact: ["onClick", "className", "goTo"] }, - { marker: ["3", "4", "5"], exact: ["children", "className"] }, + { + marker: ["1", "6"], + exact: [ + "onClick", + { name: "children", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "className", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + "goTo" + ] + }, + { + marker: "2", + exact: [ + "onClick", + { name: "className", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + "goTo" + ] + }, + { + marker: ["3", "4", "5"], + exact: [ + { name: "children", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember }, + { name: "className", kind: "JSX attribute", kindModifiers: "optional", sortText: completion.SortText.OptionalMember } + ] + }, ); diff --git a/tests/cases/fourslash/tsxCompletion7.ts b/tests/cases/fourslash/tsxCompletion7.ts index 87638f38e3805..a54e860b35f21 100644 --- a/tests/cases/fourslash/tsxCompletion7.ts +++ b/tests/cases/fourslash/tsxCompletion7.ts @@ -10,4 +10,10 @@ //// let y = { ONE: '' }; //// var x =
; -verify.completions({ marker: "", exact: ["ONE", "TWO"] }); +verify.completions({ + marker: "", + exact: [ + { name: "ONE", kind: "JSX attribute", kindModifiers: "declare", sortText: completion.SortText.MemberDeclaredBySpreadAssignment }, + { name: "TWO", kind: "JSX attribute", kindModifiers: "declare", sortText: completion.SortText.LocationPriority } + ] +});