diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 94bfe7efc2e..25ed46dde55 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6452,6 +6452,7 @@ namespace ts { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; + readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/src/server/protocol.ts b/src/server/protocol.ts index c86876f1caa..925e7638f9d 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3007,6 +3007,12 @@ namespace ts.server.protocol { * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. */ readonly includeCompletionsWithInsertText?: boolean; + /** + * Unless this option is `false`, or `includeCompletionsWithInsertText` is not enabled, + * member completion lists triggered with `.` will include entries on potentially-null and potentially-undefined + * values, with insertion text to replace preceding `.` tokens with `?.`. + */ + readonly includeAutomaticOptionalChainCompletions?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly allowTextChangesInNewFiles?: boolean; readonly lazyConfiguredProjectsFromExternalProject?: boolean; diff --git a/src/services/completions.ts b/src/services/completions.ts index 11f92c3d4eb..076cc32f374 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -338,6 +338,7 @@ namespace ts.Completions { ): CompletionEntry | undefined { let insertText: string | undefined; let replacementSpan: TextSpan | undefined; + const insertQuestionDot = origin && originIsNullableMember(origin); const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; if (origin && originIsThisType(origin)) { @@ -780,7 +781,7 @@ namespace ts.Completions { sourceFile: SourceFile, isUncheckedFile: boolean, position: number, - preferences: Pick, + preferences: Pick, detailsEntryId: CompletionEntryIdentifier | undefined, host: LanguageServiceHost, ): CompletionData | Request | undefined { @@ -1116,8 +1117,17 @@ namespace ts.Completions { let type = typeChecker.getTypeOfSymbolAtLocation(symbol, node).getNonOptionalType(); let insertQuestionDot = false; if (type.isNullableType()) { - insertQuestionDot = isRightOfDot && !isRightOfQuestionDot; - type = type.getNonNullableType(); + const canCorrectToQuestionDot = + isRightOfDot && + !isRightOfQuestionDot && + preferences.includeAutomaticOptionalChainCompletions !== false; + + if (canCorrectToQuestionDot || isRightOfQuestionDot) { + type = type.getNonNullableType(); + if (canCorrectToQuestionDot) { + insertQuestionDot = true; + } + } } addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot); } @@ -1137,8 +1147,17 @@ namespace ts.Completions { let type = typeChecker.getTypeAtLocation(node).getNonOptionalType(); let insertQuestionDot = false; if (type.isNullableType()) { - insertQuestionDot = isRightOfDot && !isRightOfQuestionDot; - type = type.getNonNullableType(); + const canCorrectToQuestionDot = + isRightOfDot && + !isRightOfQuestionDot && + preferences.includeAutomaticOptionalChainCompletions !== false; + + if (canCorrectToQuestionDot || isRightOfQuestionDot) { + type = type.getNonNullableType(); + if (canCorrectToQuestionDot) { + insertQuestionDot = true; + } + } } addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index aa1945e74b9..b02074e75ef 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3171,6 +3171,7 @@ declare namespace ts { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; + readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ @@ -8295,6 +8296,12 @@ declare namespace ts.server.protocol { * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. */ readonly includeCompletionsWithInsertText?: boolean; + /** + * Unless this option is `false`, or `includeCompletionsWithInsertText` is not enabled, + * member completion lists triggered with `.` will include entries on potentially-null and potentially-undefined + * values, with insertion text to replace preceding `.` tokens with `?.`. + */ + readonly includeAutomaticOptionalChainCompletions?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly allowTextChangesInNewFiles?: boolean; readonly lazyConfiguredProjectsFromExternalProject?: boolean; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 7c756d8a0a3..7361ccb6768 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3171,6 +3171,7 @@ declare namespace ts { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; + readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ diff --git a/tests/cases/fourslash/completionNoAutoInsertQuestionDotWithUserPreferencesOff.ts b/tests/cases/fourslash/completionNoAutoInsertQuestionDotWithUserPreferencesOff.ts new file mode 100644 index 00000000000..a1071a37f93 --- /dev/null +++ b/tests/cases/fourslash/completionNoAutoInsertQuestionDotWithUserPreferencesOff.ts @@ -0,0 +1,20 @@ +/// +// @strict: true + +//// interface User { +//// address?: { +//// city: string; +//// "postal code": string; +//// } +//// }; +//// declare const user: User; +//// user.address[|./**/|] + +verify.completions({ + marker: "", + exact: [], + preferences: { + includeInsertTextCompletions: true, + includeAutomaticOptionalChainCompletions: false + }, +}); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 5672f430434..cd184df3bed 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -583,6 +583,7 @@ declare namespace FourSlashInterface { readonly quotePreference?: "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; readonly includeInsertTextCompletions?: boolean; + readonly includeAutomaticOptionalChainCompletions?: boolean; readonly importModuleSpecifierPreference?: "relative" | "non-relative"; readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; }