diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 56b273d5c2ea3..0d01c46edd7b5 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -7684,6 +7684,10 @@ "category": "Message", "code": 95186 }, + "Add missing comma for object member completion '{0}'.": { + "category": "Message", + "code": 95187 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/services/completions.ts b/src/services/completions.ts index d39b357776885..2b47de8b7db94 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -164,6 +164,7 @@ import { isFunctionLikeDeclaration, isFunctionLikeKind, isFunctionTypeNode, + isGetAccessorDeclaration, isIdentifier, isIdentifierText, isImportableFile, @@ -217,9 +218,11 @@ import { isPrivateIdentifier, isPrivateIdentifierClassElementDeclaration, isPropertyAccessExpression, + isPropertyAssignment, isPropertyDeclaration, isPropertyNameLiteral, isRegularExpressionLiteral, + isSetAccessorDeclaration, isShorthandPropertyAssignment, isSingleOrDoubleQuote, isSourceFile, @@ -443,6 +446,8 @@ export enum CompletionSource { ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/", /** Case completions for switch statements */ SwitchCases = "SwitchCases/", + /** Completions for an Object literal expression */ + ObjectLiteralMemberWithComma = "ObjectLiteralMemberWithComma/", } /** @internal */ @@ -1683,6 +1688,30 @@ function createCompletionEntry( hasAction = true; } + // Provide object member completions when missing commas, and insert missing commas. + // For example: + // + // interface I { + // a: string; + // b: number + // } + // + // const cc: I = { a: "red" | } + // + // Completion should add a comma after "red" and provide completions for b + if (completionKind === CompletionKind.ObjectPropertyDeclaration && contextToken && findPrecedingToken(contextToken.pos, sourceFile, contextToken)?.kind !== SyntaxKind.CommaToken) { + if (isMethodDeclaration(contextToken.parent.parent) || + isGetAccessorDeclaration(contextToken.parent.parent) || + isSetAccessorDeclaration(contextToken.parent.parent) || + isSpreadAssignment(contextToken.parent) || + findAncestor(contextToken.parent, isPropertyAssignment)?.getLastToken(sourceFile) === contextToken || + isShorthandPropertyAssignment(contextToken.parent) && getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== getLineAndCharacterOfPosition(sourceFile, position).line) { + + source = CompletionSource.ObjectLiteralMemberWithComma; + hasAction = true; + } + } + if (preferences.includeCompletionsWithClassMemberSnippets && preferences.includeCompletionsWithInsertText && completionKind === CompletionKind.MemberLike && @@ -2664,7 +2693,8 @@ function getSymbolCompletionFromEntryId( return info && info.name === entryId.name && ( entryId.source === CompletionSource.ClassMemberSnippet && symbol.flags & SymbolFlags.ClassMember || entryId.source === CompletionSource.ObjectLiteralMethodSnippet && symbol.flags & (SymbolFlags.Property | SymbolFlags.Method) - || getSourceFromOrigin(origin) === entryId.source) + || getSourceFromOrigin(origin) === entryId.source + || entryId.source === CompletionSource.ObjectLiteralMemberWithComma) ? { type: "symbol" as const, symbol, location, origin, contextToken, previousToken, isJsxInitializer, isTypeOnlyLocation } : undefined; }) || { type: "none" }; @@ -2860,6 +2890,23 @@ function getCompletionEntryCodeActionsAndSourceDisplay( return { codeActions: [codeAction], sourceDisplay: undefined }; } + if (source === CompletionSource.ObjectLiteralMemberWithComma && contextToken) { + const changes = textChanges.ChangeTracker.with( + { host, formatContext, preferences }, + tracker => tracker.insertText(sourceFile, contextToken.end, ",") + ); + + if (changes) { + return { + sourceDisplay: undefined, + codeActions: [{ + changes, + description: diagnosticToString([Diagnostics.Add_missing_comma_for_object_member_completion_0, name]), + }], + }; + } + } + if (!origin || !(originIsExport(origin) || originIsResolvedExport(origin))) { return { codeActions: undefined, sourceDisplay: undefined }; } @@ -4156,7 +4203,7 @@ function getCompletionData( */ function tryGetObjectLikeCompletionSymbols(): GlobalsSearch | undefined { const symbolsStartIndex = symbols.length; - const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken); + const objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken, position, sourceFile); if (!objectLikeContainer) return GlobalsSearch.Continue; // We're looking up possible property names from contextual/inferred/declared type. @@ -4884,7 +4931,7 @@ function getCompletionData( * Returns the immediate owning object literal or binding pattern of a context token, * on the condition that one exists and that the context implies completion should be given. */ -function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined): ObjectLiteralExpression | ObjectBindingPattern | undefined { +function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined, position: number, sourceFile: SourceFile): ObjectLiteralExpression | ObjectBindingPattern | undefined { if (contextToken) { const { parent } = contextToken; switch (contextToken.kind) { @@ -4899,8 +4946,33 @@ function tryGetObjectLikeCompletionContainer(contextToken: Node | undefined): Ob case SyntaxKind.AsyncKeyword: return tryCast(parent.parent, isObjectLiteralExpression); case SyntaxKind.Identifier: - return (contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent) - ? contextToken.parent.parent : undefined; + if ((contextToken as Identifier).text === "async" && isShorthandPropertyAssignment(contextToken.parent)) { + return contextToken.parent.parent; + } + else { + if (isObjectLiteralExpression(contextToken.parent.parent) && + (isSpreadAssignment(contextToken.parent) || isShorthandPropertyAssignment(contextToken.parent) && + (getLineAndCharacterOfPosition(sourceFile, contextToken.getEnd()).line !== getLineAndCharacterOfPosition(sourceFile, position).line))) { + return contextToken.parent.parent; + } + const ancestorNode = findAncestor(parent, isPropertyAssignment); + if (ancestorNode?.getLastToken(sourceFile) === contextToken && isObjectLiteralExpression(ancestorNode.parent)) { + return ancestorNode.parent; + } + } + break; + default: + if (parent.parent?.parent && (isMethodDeclaration(parent.parent) || isGetAccessorDeclaration(parent.parent) || isSetAccessorDeclaration(parent.parent)) && isObjectLiteralExpression(parent.parent.parent)) { + return parent.parent.parent; + } + if (isSpreadAssignment(parent) && isObjectLiteralExpression(parent.parent)) { + return parent.parent; + } + const ancestorNode = findAncestor(parent, isPropertyAssignment); + if (contextToken.kind !== SyntaxKind.ColonToken && ancestorNode?.getLastToken(sourceFile) === contextToken && + isObjectLiteralExpression(ancestorNode.parent)) { + return ancestorNode.parent; + } } } diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions1.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions1.ts new file mode 100644 index 0000000000000..6345fcb6569ee --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions1.ts @@ -0,0 +1,43 @@ +/// +//// interface ColorPalette { +//// primary?: string; +//// secondary?: string; +//// } + +//// let colors: ColorPalette = { +//// primary: "red" +//// /**/ +//// }; + +verify.completions({ + marker: "", + includes: [{ + name: "secondary", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("", { + name: "secondary", + description: `Add missing comma for object member completion 'secondary'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface ColorPalette { + primary?: string; + secondary?: string; +} +let colors: ColorPalette = { + primary: "red", + +};`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions10.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions10.ts new file mode 100644 index 0000000000000..5f98448820949 --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions10.ts @@ -0,0 +1,47 @@ +/// +//// interface TTTT { +//// aaa: string, +//// bbb?: number +//// } +//// const uuu: TTTT = { +//// get aaa() { +//// return "" +//// } +//// /**/ +//// } + +verify.completions({ + marker: "", + includes: [{ + name: "bbb", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, + }); + + verify.applyCodeActionFromCompletion("", { + name: "bbb", + description: `Add missing comma for object member completion 'bbb'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface TTTT { + aaa: string, + bbb?: number +} +const uuu: TTTT = { + get aaa() { + return "" + }, + +}`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, + }); + \ No newline at end of file diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions2.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions2.ts new file mode 100644 index 0000000000000..8a67507a409c0 --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions2.ts @@ -0,0 +1,48 @@ +/// +//// interface ColorPalette { +//// primary?: string; +//// secondary?: string; +//// } + +//// interface I { +//// color: ColorPalette; +//// } + +//// const a: I = { +//// color: {primary: "red" /**/} +//// } + +verify.completions({ + marker: "", + includes: [{ + name: "secondary", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + } +}); + +verify.applyCodeActionFromCompletion("", { + name: "secondary", + description: `Add missing comma for object member completion 'secondary'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: +`interface ColorPalette { + primary?: string; + secondary?: string; +} +interface I { + color: ColorPalette; +} +const a: I = { + color: {primary: "red", } +}`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions3.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions3.ts new file mode 100644 index 0000000000000..a8003c105888e --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions3.ts @@ -0,0 +1,45 @@ +/// +////interface T { +//// aaa?: string; +//// foo(): void; +//// } +//// const obj: T = { +//// foo() { +// +//// } +//// /**/ +//// } + +verify.completions({ + marker: "", + includes: [{ + name: "aaa", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("", { + name: "aaa", + description: `Add missing comma for object member completion 'aaa'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface T { + aaa?: string; + foo(): void; + } + const obj: T = { + foo() { + }, + + }`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, + }); diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions4.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions4.ts new file mode 100644 index 0000000000000..3ef13a46e4b1d --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions4.ts @@ -0,0 +1,42 @@ +/// +////interface T { +//// aaa: number; +//// bbb?: number; +//// } +//// const obj: T = { +//// aaa: 1 * (2 + 3) +//// /**/ +//// } + +verify.completions({ + marker: "", + includes: [{ + name: "bbb", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("", { + name: "bbb", + description: `Add missing comma for object member completion 'bbb'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface T { + aaa: number; + bbb?: number; + } + const obj: T = { + aaa: 1 * (2 + 3), + + }`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions5.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions5.ts new file mode 100644 index 0000000000000..9da760b90d493 --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions5.ts @@ -0,0 +1,39 @@ +/// + +//// type E = {} +//// type F = string +//// interface I { e: E, f?: F } +//// const i: I = { e: {} +//// /**/ +//// }; + +verify.completions({ + marker: "", + includes: [{ + name: "f", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("", { + name: "f", + description: `Add missing comma for object member completion 'f'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `type E = {} +type F = string +interface I { e: E, f?: F } +const i: I = { e: {}, + +};`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions6.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions6.ts new file mode 100644 index 0000000000000..0ab643271c48e --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions6.ts @@ -0,0 +1,45 @@ + + +/// + +//// type E = {} +//// type F = string +//// const i= { e: {} }; +//// interface I { e: E, f?: F } +//// const k: I = { +//// ["e"]: i +//// /**/ +//// } + +verify.completions({ + marker: "", + includes: [{ + name: "f", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("", { + name: "f", + description: `Add missing comma for object member completion 'f'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `type E = {} +type F = string +const i= { e: {} }; +interface I { e: E, f?: F } +const k: I = { + ["e"]: i, + +}`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions7.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions7.ts new file mode 100644 index 0000000000000..3cd6c3c011f5a --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions7.ts @@ -0,0 +1,129 @@ + + +/// + +// @Filename: a.ts +//// interface I { +//// aaa: number, +//// bbb: number, +//// } +//// +//// interface U { +//// a: number, +//// b: { +//// c: { +//// d: { +//// aaa: number, +//// } +//// } +//// +//// }, +//// } +//// const num: U = {} as any; +//// +//// const l: I = { +//// ...num.b.c.d +//// /*a*/ +//// } + +// @Filename: b.ts +//// interface pp { +//// aaa: string; +//// bbb: number; +//// } +//// +//// const abc: pp = { +//// aaa: "", +//// bbb: 1, +//// } +//// +//// const cab: pp = { +//// ...abc +//// /*b*/ +//// } + +verify.completions({ + marker: "a", + includes: [{ + name: "bbb", + sortText: completion.SortText.LocationPriority, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("a", { + name: "bbb", + description: `Add missing comma for object member completion 'bbb'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface I { + aaa: number, + bbb: number, +} + +interface U { + a: number, + b: { + c: { + d: { + aaa: number, + } + } + + }, +} +const num: U = {} as any; + +const l: I = { + ...num.b.c.d, + +}`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.completions({ + marker: "b", + includes: [{ + name: "aaa", + sortText: completion.SortText.MemberDeclaredBySpreadAssignment, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("b", { + name: "aaa", + description: `Add missing comma for object member completion 'aaa'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface pp { + aaa: string; + bbb: number; +} + +const abc: pp = { + aaa: "", + bbb: 1, +} + +const cab: pp = { + ...abc, + +}`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions8.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions8.ts new file mode 100644 index 0000000000000..5f2681dbb83c5 --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions8.ts @@ -0,0 +1,45 @@ +/// + +//// interface A { +//// b: string, +//// c?: number, +//// } +//// const b = "" +//// const a: A = { +//// b +//// /**/ +//// } + +verify.completions({ + marker: "", + includes: [{ + name: "c", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("", { + name: "c", + description: `Add missing comma for object member completion 'c'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface A { + b: string, + c?: number, +} +const b = "" +const a: A = { + b, + +}`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/completionsObjectLiteralExpressions9.ts b/tests/cases/fourslash/completionsObjectLiteralExpressions9.ts new file mode 100644 index 0000000000000..fcf9537c61fd0 --- /dev/null +++ b/tests/cases/fourslash/completionsObjectLiteralExpressions9.ts @@ -0,0 +1,139 @@ +/// +// @Filename: a.ts +////interface T { +//// aaa: number; +//// bbb?: number; +//// ccc?: number; +//// } +//// const obj: T = { +//// aaa: 1 * (2 + 3) +//// c/*a*/ +//// } + +// @Filename: b.ts +////interface T { +//// aaa?: string; +//// foo(): void; +//// } +//// const obj: T = { +//// foo() { +// +//// } +//// aa/*b*/ +//// } + +// @Filename: c.ts +//// interface ColorPalette { +//// primary?: string; +//// secondary?: string; +//// } +//// interface I { +//// color: ColorPalette; +//// } +//// const a: I = { +//// color: {primary: "red" sec/**/} +//// } + +verify.completions({ + marker: "a", + includes: [{ + name: "bbb", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("a", { + name: "bbb", + description: `Add missing comma for object member completion 'bbb'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface T { + aaa: number; + bbb?: number; + ccc?: number; + } + const obj: T = { + aaa: 1 * (2 + 3), + c + }`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.completions({ + marker: "b", + includes: [{ + name: "aaa", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.applyCodeActionFromCompletion("b", { + name: "aaa", + description: `Add missing comma for object member completion 'aaa'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface T { + aaa?: string; + foo(): void; + } + const obj: T = { + foo() { + }, + aa + }`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + +verify.completions({ + marker: "", + includes: [{ + name: "secondary", + sortText: completion.SortText.OptionalMember, + hasAction: true, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + }], + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + } +}); + +verify.applyCodeActionFromCompletion("", { + name: "secondary", + description: `Add missing comma for object member completion 'secondary'.`, + source: completion.CompletionSource.ObjectLiteralMemberWithComma, + newFileContent: + `interface ColorPalette { + primary?: string; + secondary?: string; +} +interface I { + color: ColorPalette; +} +const a: I = { + color: {primary: "red", sec} +}`, + preferences: { + allowIncompleteCompletions: true, + includeInsertTextCompletions: true, + }, +}); + \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 82321aef548a9..4121114c72be6 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -914,6 +914,7 @@ declare namespace completion { TypeOnlyAlias = "TypeOnlyAlias/", ObjectLiteralMethodSnippet = "ObjectLiteralMethodSnippet/", SwitchCases = "SwitchCases/", + ObjectLiteralMemberWithComma = "ObjectLiteralMemberWithComma/", } export const globalThisEntry: Entry; export const undefinedVarEntry: Entry;