diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index bfd16e297c064..97789e47abc24 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -600,6 +600,7 @@ namespace ts { }, tryGetMemberInModuleExports: (name, symbol) => tryGetMemberInModuleExports(escapeLeadingUnderscores(name), symbol), tryGetMemberInModuleExportsAndProperties: (name, symbol) => tryGetMemberInModuleExportsAndProperties(escapeLeadingUnderscores(name), symbol), + tryFindAmbientModule: moduleName => tryFindAmbientModule(moduleName, /*withAugmentations*/ true), tryFindAmbientModuleWithoutAugmentations: moduleName => { // we deliberately exclude augmentations // since we are only interested in declarations of the module itself diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ecce409a8ac64..a74e4135cf72a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4166,6 +4166,7 @@ namespace ts { /* @internal */ createSymbol(flags: SymbolFlags, name: __String): TransientSymbol; /* @internal */ createIndexInfo(type: Type, isReadonly: boolean, declaration?: SignatureDeclaration): IndexInfo; /* @internal */ isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node | undefined, meaning: SymbolFlags, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult; + /* @internal */ tryFindAmbientModule(moduleName: string): Symbol | undefined; /* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol | undefined; /* @internal */ getSymbolWalker(accept?: (symbol: Symbol) => boolean): SymbolWalker; diff --git a/src/harness/client.ts b/src/harness/client.ts index f4f703d9164e3..4acfca3e0eb91 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -205,9 +205,9 @@ namespace ts.server { isNewIdentifierLocation: false, entries: response.body!.map(entry => { // TODO: GH#18217 if (entry.replacementSpan !== undefined) { - const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, isRecommended } = entry; + const { name, kind, kindModifiers, sortText, replacementSpan, hasAction, source, data, isRecommended } = entry; // TODO: GH#241 - const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, isRecommended }; + const res: CompletionEntry = { name, kind, kindModifiers, sortText, replacementSpan: this.decodeSpan(replacementSpan, fileName), hasAction, source, data: data as any, isRecommended }; return res; } @@ -216,14 +216,13 @@ namespace ts.server { }; } - getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined): CompletionEntryDetails { - const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source }] }; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, _preferences: UserPreferences | undefined, data: unknown): CompletionEntryDetails { + const args: protocol.CompletionDetailsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), entryNames: [{ name: entryName, source, data }] }; - const request = this.processRequest(CommandNames.CompletionDetails, args); - const response = this.processResponse(request); - Debug.assert(response.body!.length === 1, "Unexpected length of completion details response body."); - const convertedCodeActions = map(response.body![0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) })); - return { ...response.body![0], codeActions: convertedCodeActions }; + const request = this.processRequest(CommandNames.CompletionDetailsFull, args); + const response = this.processResponse(request); + Debug.assert(response.body.length === 1, "Unexpected length of completion details response body."); + return response.body[0]; } getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 454240b7a726d..7c48ffbec689f 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -399,7 +399,7 @@ namespace FourSlash { } const memo = Utils.memoize( (_version: number, _active: string, _caret: number, _selectEnd: number, _marker: string, ...args: any[]) => (ls[key] as Function)(...args), - (...args) => args.join("|,|") + (...args) => args.map(a => a && typeof a === "object" ? JSON.stringify(a) : a).join("|,|") ); proxy[key] = (...args: any[]) => memo( target.languageServiceAdapterHost.getScriptInfo(target.activeFile.fileName)!.version, @@ -867,7 +867,7 @@ namespace FourSlash { nameToEntries.set(entry.name, [entry]); } else { - if (entries.some(e => e.source === entry.source)) { + if (entries.some(e => e.source === entry.source && this.deepEqual(e.data, entry.data))) { this.raiseError(`Duplicate completions for ${entry.name}`); } entries.push(entry); @@ -885,8 +885,8 @@ namespace FourSlash { const name = typeof include === "string" ? include : include.name; const found = nameToEntries.get(name); if (!found) throw this.raiseError(`Includes: completion '${name}' not found.`); - assert(found.length === 1, `Must use 'exact' for multiple completions with same name: '${name}'`); - this.verifyCompletionEntry(ts.first(found), include); + if (!found.length) throw this.raiseError(`Includes: no completions with name '${name}' remain unmatched.`); + this.verifyCompletionEntry(found.shift()!, include); } } if (options.excludes) { @@ -933,7 +933,7 @@ namespace FourSlash { assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, this.messageAtLastKnownMarker(`Actual entry: ${JSON.stringify(actual)}`)); if (expected.text !== undefined) { - const actualDetails = ts.Debug.checkDefined(this.getCompletionEntryDetails(actual.name, actual.source), `No completion details available for name '${actual.name}' and source '${actual.source}'`); + const actualDetails = ts.Debug.checkDefined(this.getCompletionEntryDetails(actual.name, actual.source, actual.data), `No completion details available for name '${actual.name}' and source '${actual.source}'`); assert.equal(ts.displayPartsToString(actualDetails.displayParts), expected.text, "Expected 'text' property to match 'displayParts' string"); assert.equal(ts.displayPartsToString(actualDetails.documentation), expected.documentation || "", "Expected 'documentation' property to match 'documentation' display parts string"); // TODO: GH#23587 @@ -1254,6 +1254,16 @@ namespace FourSlash { } + private deepEqual(a: unknown, b: unknown) { + try { + this.assertObjectsEqual(a, b); + return true; + } + catch { + return false; + } + } + public verifyDisplayPartsOfReferencedSymbol(expected: ts.SymbolDisplayPart[]) { const referencedSymbols = this.findReferencesAtCaret()!; @@ -1281,11 +1291,11 @@ namespace FourSlash { return this.languageService.getCompletionsAtPosition(this.activeFile.fileName, this.currentCaretPosition, options); } - private getCompletionEntryDetails(entryName: string, source?: string, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined { + private getCompletionEntryDetails(entryName: string, source: string | undefined, data: ts.CompletionEntryData | undefined, preferences?: ts.UserPreferences): ts.CompletionEntryDetails | undefined { if (preferences) { this.configure(preferences); } - return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences); + return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings, source, preferences, data); } private getReferencesAtCaret() { @@ -2796,14 +2806,14 @@ namespace FourSlash { public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) { this.goToMarker(markerName); - const details = this.getCompletionEntryDetails(options.name, options.source, options.preferences); + const details = this.getCompletionEntryDetails(options.name, options.source, options.data, options.preferences); if (!details) { const completions = this.getCompletionListAtCaret(options.preferences)?.entries; const matchingName = completions?.filter(e => e.name === options.name); const detailMessage = matchingName?.length ? `\n Found ${matchingName.length} with name '${options.name}' from source(s) ${matchingName.map(e => `'${e.source}'`).join(", ")}.` : ` (In fact, there were no completions with name '${options.name}' at all.)`; - return this.raiseError(`No completions were found for the given name, source, and preferences.` + detailMessage); + return this.raiseError(`No completions were found for the given name, source/data, and preferences.` + detailMessage); } const codeActions = details.codeActions; if (codeActions?.length !== 1) { diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index 09b52997b1285..a909dd1c08027 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -1701,6 +1701,7 @@ namespace FourSlashInterface { export interface VerifyCompletionActionOptions extends NewContentOptions { name: string; source?: string; + data?: ts.CompletionEntryData; description: string; preferences?: ts.UserPreferences; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 1a056683f912c..17026bd5001b0 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -474,8 +474,8 @@ namespace Harness.LanguageService { getCompletionsAtPosition(fileName: string, position: number, preferences: ts.UserPreferences | undefined): ts.CompletionInfo { return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position, preferences)); } - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined): ts.CompletionEntryDetails { - return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences)); + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: ts.FormatCodeOptions | undefined, source: string | undefined, preferences: ts.UserPreferences | undefined, data: ts.CompletionEntryData | undefined): ts.CompletionEntryDetails { + return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(formatOptions), source, preferences, data)); } getCompletionEntrySymbol(): ts.Symbol { throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 1ce0c66019f6d..612500e68e41a 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2159,6 +2159,7 @@ namespace ts.server.protocol { export interface CompletionEntryIdentifier { name: string; source?: string; + data?: unknown; } /** @@ -2245,6 +2246,12 @@ namespace ts.server.protocol { * in the project package.json. */ isPackageJsonImport?: true; + /** + * A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`, + * that allows TS Server to look up the symbol represented by the completion item, disambiguating + * items with the same name. + */ + data?: unknown; } /** diff --git a/src/server/session.ts b/src/server/session.ts index dc0d4f42f5c79..aeb695bf1477b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1808,14 +1808,14 @@ namespace ts.server { if (kind === protocol.CommandTypes.CompletionsFull) return completions; const prefix = args.prefix || ""; - const entries = mapDefined(completions.entries, entry => { + const entries = stableSort(mapDefined(completions.entries, entry => { if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) { - const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended, isPackageJsonImport } = entry; + const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended, isPackageJsonImport, data } = entry; const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined; // Use `hasAction || undefined` to avoid serializing `false`. - return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended, isPackageJsonImport }; + return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended, isPackageJsonImport, data }; } - }).sort((a, b) => compareStringsCaseSensitiveUI(a.name, b.name)); + }), (a, b) => compareStringsCaseSensitiveUI(a.name, b.name)); if (kind === protocol.CommandTypes.Completions) { if (completions.metadata) (entries as WithMetadata).metadata = completions.metadata; @@ -1837,8 +1837,8 @@ namespace ts.server { const formattingOptions = project.projectService.getFormatCodeOptions(file); const result = mapDefined(args.entryNames, entryName => { - const { name, source } = typeof entryName === "string" ? { name: entryName, source: undefined } : entryName; - return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file)); + const { name, source, data } = typeof entryName === "string" ? { name: entryName, source: undefined, data: undefined } : entryName; + return project.getLanguageService().getCompletionEntryDetails(file, position, name, formattingOptions, source, this.getPreferences(file), data ? cast(data, isCompletionEntryData) : undefined); }); return simplifiedResult ? result.map(details => ({ ...details, codeActions: map(details.codeActions, action => this.mapCodeAction(action)) })) @@ -3118,4 +3118,12 @@ namespace ts.server { isDefinition }; } + + function isCompletionEntryData(data: any): data is CompletionEntryData { + return data === undefined || data && typeof data === "object" + && typeof data.exportName === "string" + && (data.fileName === undefined || typeof data.fileName === "string") + && (data.ambientModuleName === undefined || typeof data.ambientModuleName === "string" + && (data.isPackageJsonImport === undefined || typeof data.isPackageJsonImport === "boolean")); + } } diff --git a/src/services/completions.ts b/src/services/completions.ts index f5f7176237b9b..2113c8e06ab04 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -48,6 +48,8 @@ namespace ts.Completions { moduleSymbol: Symbol; isDefaultExport: boolean; isFromPackageJson?: boolean; + exportName: string; + fileName?: string; } function originIsThisType(origin: SymbolOriginInfo): boolean { @@ -104,7 +106,6 @@ namespace ts.Completions { export interface AutoImportSuggestion { symbol: Symbol; symbolName: string; - skipFilter: boolean; origin: SymbolOriginInfoExport; } export interface ImportSuggestionsForFileCache { @@ -422,6 +423,7 @@ namespace ts.Completions { ): CompletionEntry | undefined { let insertText: string | undefined; let replacementSpan = getReplacementSpanForContextToken(contextToken); + let data: CompletionEntryData | undefined; const insertQuestionDot = origin && originIsNullableMember(origin); const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; @@ -472,6 +474,15 @@ namespace ts.Completions { return undefined; } + if (originIsExport(origin)) { + data = { + exportName: origin.exportName, + fileName: origin.fileName, + ambientModuleName: origin.fileName ? undefined : stripQuotes(origin.moduleSymbol.name), + isPackageJsonImport: origin.isFromPackageJson ? true : undefined, + }; + } + // TODO(drosen): Right now we just permit *all* semantic meanings when calling // 'getSymbolKind' which is permissible given that it is backwards compatible; but // really we should consider passing the meaning for the node so that we don't report @@ -491,6 +502,7 @@ namespace ts.Completions { insertText, replacementSpan, isPackageJsonImport: originIsPackageJsonImport(origin) || undefined, + data, }; } @@ -669,6 +681,7 @@ namespace ts.Completions { export interface CompletionEntryIdentifier { name: string; source?: string; + data?: CompletionEntryData; } export function getCompletionEntryDetails( @@ -1485,25 +1498,35 @@ namespace ts.Completions { if (shouldOfferImportCompletions()) { const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; - const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host); - if (!detailsEntryId && importSuggestionsCache) { - importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion()); + if (detailsEntryId?.data) { + const autoImport = getAutoImportSymbolFromCompletionEntryData(detailsEntryId.data); + if (autoImport) { + const symbolId = getSymbolId(autoImport.symbol); + symbols.push(autoImport.symbol); + symbolToOriginInfoMap[symbolId] = autoImport.origin; + } } - autoImportSuggestions.forEach(({ symbol, symbolName, skipFilter, origin }) => { - if (detailsEntryId) { - if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) { + else { + const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host); + if (!detailsEntryId && importSuggestionsCache) { + importSuggestionsCache.set(sourceFile.fileName, autoImportSuggestions, host.getProjectVersion && host.getProjectVersion()); + } + autoImportSuggestions.forEach(({ symbol, symbolName, origin }) => { + if (detailsEntryId) { + if (detailsEntryId.source && stripQuotes(origin.moduleSymbol.name) !== detailsEntryId.source) { + return; + } + } + else if (!stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { return; } - } - else if (!skipFilter && !stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { - return; - } - const symbolId = getSymbolId(symbol); - symbols.push(symbol); - symbolToOriginInfoMap[symbolId] = origin; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; - }); + const symbolId = getSymbolId(symbol); + symbols.push(symbol); + symbolToOriginInfoMap[symbolId] = origin; + symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + }); + } } filterGlobalCompletion(symbols); } @@ -1673,7 +1696,7 @@ namespace ts.Completions { const seenResolvedModules = new Map(); const results = createMultiMap(); - codefix.forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, /*useAutoImportProvider*/ true, (moduleSymbol, _, program, isFromPackageJson) => { + codefix.forEachExternalModuleToImportFrom(program, host, sourceFile, !detailsEntryId, /*useAutoImportProvider*/ true, (moduleSymbol, file, program, isFromPackageJson) => { // Perf -- ignore other modules if this is a request for details if (detailsEntryId && detailsEntryId.source && stripQuotes(moduleSymbol.name) !== detailsEntryId.source) { return; @@ -1689,7 +1712,7 @@ namespace ts.Completions { // Don't add another completion for `export =` of a symbol that's already global. // So in `declare namespace foo {} declare module "foo" { export = foo; }`, there will just be the global completion for `foo`. if (resolvedModuleSymbol !== moduleSymbol && every(resolvedModuleSymbol.declarations, isNonGlobalDeclaration)) { - pushSymbol(resolvedModuleSymbol, moduleSymbol, isFromPackageJson, /*skipFilter*/ true); + pushSymbol(resolvedModuleSymbol, InternalSymbolName.ExportEquals, moduleSymbol, file, isFromPackageJson); } for (const symbol of typeChecker.getExportsAndPropertiesOfModule(moduleSymbol)) { @@ -1698,14 +1721,14 @@ namespace ts.Completions { continue; } - pushSymbol(symbol, moduleSymbol, isFromPackageJson, /*skipFilter*/ false); + pushSymbol(symbol, symbol.name, moduleSymbol, file, isFromPackageJson); } }); log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`); return flatten(arrayFrom(results.values())); - function pushSymbol(symbol: Symbol, moduleSymbol: Symbol, isFromPackageJson: boolean, skipFilter: boolean) { + function pushSymbol(symbol: Symbol, exportName: string, moduleSymbol: Symbol, file: SourceFile | undefined, isFromPackageJson: boolean) { const isDefaultExport = symbol.escapedName === InternalSymbolName.Default; const nonLocalSymbol = symbol; if (isDefaultExport) { @@ -1718,17 +1741,50 @@ namespace ts.Completions { const symbolName = getNameForExportedSymbol(symbol, target); const existingSuggestions = results.get(getSymbolId(original)); if (!some(existingSuggestions, s => s.symbolName === symbolName && moduleSymbolsAreDuplicateOrigins(moduleSymbol, s.origin.moduleSymbol))) { - const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, isDefaultExport, isFromPackageJson }; + const origin: SymbolOriginInfoExport = { + kind: SymbolOriginInfoKind.Export, + moduleSymbol, + isDefaultExport, + isFromPackageJson, + exportName, + fileName: file?.fileName + }; results.add(getSymbolId(original), { symbol, symbolName, origin, - skipFilter, }); } } } + function getAutoImportSymbolFromCompletionEntryData(data: CompletionEntryData): { symbol: Symbol, origin: SymbolOriginInfoExport } | undefined { + const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program; + const checker = containingProgram.getTypeChecker(); + const moduleSymbol = + data.ambientModuleName ? checker.tryFindAmbientModule(data.ambientModuleName) : + data.fileName ? checker.getMergedSymbol(Debug.checkDefined(containingProgram.getSourceFile(data.fileName)).symbol) : + undefined; + + if (!moduleSymbol) return undefined; + let symbol = data.exportName === InternalSymbolName.ExportEquals + ? checker.resolveExternalModuleSymbol(moduleSymbol) + : checker.tryGetMemberInModuleExportsAndProperties(data.exportName, moduleSymbol); + if (!symbol) return undefined; + const isDefaultExport = data.exportName === InternalSymbolName.Default; + symbol = isDefaultExport && getLocalSymbolForExportDefault(symbol) || symbol; + return { + symbol, + origin: { + kind: SymbolOriginInfoKind.Export, + moduleSymbol, + isDefaultExport, + exportName: data.exportName, + fileName: data.fileName, + } + }; + } + /** * Determines whether a module symbol is redundant with another for purposes of offering * auto-import completions for exports of the same symbol. Exports of the same symbol diff --git a/src/services/services.ts b/src/services/services.ts index 1463d81dfdecc..884e890dccb20 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1558,14 +1558,14 @@ namespace ts { options.triggerCharacter); } - function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions): CompletionEntryDetails | undefined { + function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions, data?: CompletionEntryData): CompletionEntryDetails | undefined { synchronizeHostData(); return Completions.getCompletionEntryDetails( program, log, getValidSourceFile(fileName), position, - { name, source }, + { name, source, data }, host, (formattingOptions && formatting.getFormatContext(formattingOptions, host))!, // TODO: GH#18217 preferences, diff --git a/src/services/shims.ts b/src/services/shims.ts index e986820765ad9..65ac30dbaef01 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -150,7 +150,7 @@ namespace ts { getEncodedSemanticClassifications(fileName: string, start: number, length: number, format?: SemanticClassificationFormat): string; getCompletionsAtPosition(fileName: string, position: number, preferences: UserPreferences | undefined): string; - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined): string; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): string; getQuickInfoAtPosition(fileName: string, position: number): string; @@ -962,12 +962,12 @@ namespace ts { } /** Get a string based representation of a completion list entry details */ - public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined) { + public getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: string/*Services.FormatCodeOptions*/ | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined) { return this.forwardJSONCall( `getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`, () => { const localOptions: FormatCodeOptions = formatOptions === undefined ? undefined : JSON.parse(formatOptions); - return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences); + return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions, source, preferences, data); } ); } diff --git a/src/services/types.ts b/src/services/types.ts index 82563150ecb8e..c09d0d7d59da3 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -424,10 +424,11 @@ namespace ts { * * @param fileName The path to the file * @param position A zero based index of the character where you want the entries - * @param entryName The name from an existing completion which came from `getCompletionsAtPosition` + * @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition` * @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility - * @param source Source code for the current file, can be undefined for backwards compatibility + * @param source `source` property from the completion entry * @param preferences User settings, can be undefined for backwards compatibility + * @param data `data` property from the completion entry */ getCompletionEntryDetails( fileName: string, @@ -436,6 +437,7 @@ namespace ts { formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined, + data: CompletionEntryData | undefined, ): CompletionEntryDetails | undefined; getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined; @@ -1136,6 +1138,20 @@ namespace ts { entries: CompletionEntry[]; } + export interface CompletionEntryData { + /** The file name declaring the export's module symbol, if it was an external module */ + fileName?: string; + /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ + ambientModuleName?: string; + /** True if the export was found in the package.json AutoImportProvider */ + isPackageJsonImport?: true; + /** + * The name of the property or export in the module's symbol table. Differs from the completion name + * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. + */ + exportName: string; + } + // see comments in protocol.ts export interface CompletionEntry { name: string; @@ -1154,6 +1170,15 @@ namespace ts { isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; + /** + * A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`, + * that allows TS Server to look up the symbol represented by the completion item, disambiguating + * items with the same name. Currently only defined for auto-import completions, but the type is + * `unknown` in the protocol, so it can be changed as needed to support other kinds of completions. + * The presence of this property should generally not be used to assume that this completion entry + * is an auto-import. + */ + data?: CompletionEntryData; } export interface CompletionEntryDetails { diff --git a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts index bd8ed2714acd5..26ad16d8ebcef 100644 --- a/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts +++ b/src/testRunner/unittests/services/cancellableLanguageServiceOperations.ts @@ -47,7 +47,7 @@ namespace ts { placeOpenBraceOnNewLineForControlBlocks: false, }; verifyOperationCancelledAfter(file, 1, service => // The LS doesn't do any top-level checks on the token for completion entry details, so the first check is within the checker - service.getCompletionEntryDetails("file.ts", file.lastIndexOf("f"), "foo", options, /*content*/ undefined, {})!, r => assert.exists(r.displayParts) + service.getCompletionEntryDetails("file.ts", file.lastIndexOf("f"), "foo", options, /*source*/ undefined, {}, /*data*/ undefined)!, r => assert.exists(r.displayParts) ); }); diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts index 2117d8924aa0c..ccf4bc934611d 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -39,6 +39,7 @@ namespace ts.projectSystem { isPackageJsonImport: undefined, sortText: Completions.SortText.AutoImportSuggestions, source: "/a", + data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined } }; assert.deepEqual(response, { isGlobalCompletion: true, @@ -50,7 +51,7 @@ namespace ts.projectSystem { const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = { ...requestLocation, - entryNames: [{ name: "foo", source: "/a" }], + entryNames: [{ name: "foo", source: "/a", data: { exportName: "foo", fileName: "/a.ts" } }], }; const detailsResponse = executeSessionRequest(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs); diff --git a/src/testRunner/unittests/tsserver/partialSemanticServer.ts b/src/testRunner/unittests/tsserver/partialSemanticServer.ts index 2c8b8ed14b068..5ef5cb6ccecc4 100644 --- a/src/testRunner/unittests/tsserver/partialSemanticServer.ts +++ b/src/testRunner/unittests/tsserver/partialSemanticServer.ts @@ -70,7 +70,8 @@ import { something } from "something"; isPackageJsonImport: undefined, isRecommended: undefined, replacementSpan: undefined, - source: undefined + source: undefined, + data: undefined, }; } }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index d2e7603a3c959..f9ec18ef04cd0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5513,12 +5513,13 @@ declare namespace ts { * * @param fileName The path to the file * @param position A zero based index of the character where you want the entries - * @param entryName The name from an existing completion which came from `getCompletionsAtPosition` + * @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition` * @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility - * @param source Source code for the current file, can be undefined for backwards compatibility + * @param source `source` property from the completion entry * @param preferences User settings, can be undefined for backwards compatibility + * @param data `data` property from the completion entry */ - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): CompletionEntryDetails | undefined; getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined; /** * Gets semantic information about the identifier at a particular position in a @@ -6078,6 +6079,19 @@ declare namespace ts { isNewIdentifierLocation: boolean; entries: CompletionEntry[]; } + interface CompletionEntryData { + /** The file name declaring the export's module symbol, if it was an external module */ + fileName?: string; + /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ + ambientModuleName?: string; + /** True if the export was found in the package.json AutoImportProvider */ + isPackageJsonImport?: true; + /** + * The name of the property or export in the module's symbol table. Differs from the completion name + * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. + */ + exportName: string; + } interface CompletionEntry { name: string; kind: ScriptElementKind; @@ -6095,6 +6109,15 @@ declare namespace ts { isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; + /** + * A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`, + * that allows TS Server to look up the symbol represented by the completion item, disambiguating + * items with the same name. Currently only defined for auto-import completions, but the type is + * `unknown` in the protocol, so it can be changed as needed to support other kinds of completions. + * The presence of this property should generally not be used to assume that this completion entry + * is an auto-import. + */ + data?: CompletionEntryData; } interface CompletionEntryDetails { name: string; @@ -8112,6 +8135,7 @@ declare namespace ts.server.protocol { interface CompletionEntryIdentifier { name: string; source?: string; + data?: unknown; } /** * Completion entry details request; value of command field is @@ -8194,6 +8218,12 @@ declare namespace ts.server.protocol { * in the project package.json. */ isPackageJsonImport?: true; + /** + * A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`, + * that allows TS Server to look up the symbol represented by the completion item, disambiguating + * items with the same name. + */ + data?: unknown; } /** * Additional completion entry details, available on demand diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 87bb3c3e61ef8..84a0e049356c2 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5513,12 +5513,13 @@ declare namespace ts { * * @param fileName The path to the file * @param position A zero based index of the character where you want the entries - * @param entryName The name from an existing completion which came from `getCompletionsAtPosition` + * @param entryName The `name` from an existing completion which came from `getCompletionsAtPosition` * @param formatOptions How should code samples in the completions be formatted, can be undefined for backwards compatibility - * @param source Source code for the current file, can be undefined for backwards compatibility + * @param source `source` property from the completion entry * @param preferences User settings, can be undefined for backwards compatibility + * @param data `data` property from the completion entry */ - getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined): CompletionEntryDetails | undefined; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, formatOptions: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences | undefined, data: CompletionEntryData | undefined): CompletionEntryDetails | undefined; getCompletionEntrySymbol(fileName: string, position: number, name: string, source: string | undefined): Symbol | undefined; /** * Gets semantic information about the identifier at a particular position in a @@ -6078,6 +6079,19 @@ declare namespace ts { isNewIdentifierLocation: boolean; entries: CompletionEntry[]; } + interface CompletionEntryData { + /** The file name declaring the export's module symbol, if it was an external module */ + fileName?: string; + /** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */ + ambientModuleName?: string; + /** True if the export was found in the package.json AutoImportProvider */ + isPackageJsonImport?: true; + /** + * The name of the property or export in the module's symbol table. Differs from the completion name + * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. + */ + exportName: string; + } interface CompletionEntry { name: string; kind: ScriptElementKind; @@ -6095,6 +6109,15 @@ declare namespace ts { isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; + /** + * A property to be sent back to TS Server in the CompletionDetailsRequest, along with `name`, + * that allows TS Server to look up the symbol represented by the completion item, disambiguating + * items with the same name. Currently only defined for auto-import completions, but the type is + * `unknown` in the protocol, so it can be changed as needed to support other kinds of completions. + * The presence of this property should generally not be used to assume that this completion entry + * is an auto-import. + */ + data?: CompletionEntryData; } interface CompletionEntryDetails { name: string; diff --git a/tests/cases/fourslash/completionListForUnicodeEscapeName.ts b/tests/cases/fourslash/completionListForUnicodeEscapeName.ts index 7c18e6c82a08c..47bb4bbd0bed0 100644 --- a/tests/cases/fourslash/completionListForUnicodeEscapeName.ts +++ b/tests/cases/fourslash/completionListForUnicodeEscapeName.ts @@ -6,7 +6,7 @@ /////*3*/ verify.completions( - { marker: "0", includes: ["B", "\u0042"] }, - { marker: "2", excludes: ["C", "\u0043", "A", "\u0041"], isNewIdentifierLocation: true }, - { marker: "3", includes: ["B", "\u0042", "A", "\u0041", "C", "\u0043"] }, + { marker: "0", includes: ["B"] }, + { marker: "2", excludes: ["C", "A"], isNewIdentifierLocation: true }, + { marker: "3", includes: ["B", "A", "C"] }, ); diff --git a/tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts b/tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts new file mode 100644 index 0000000000000..5d8b8ba3cf363 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts @@ -0,0 +1,52 @@ +/// + +// @noLib: true + +// @Filename: /someModule.ts +//// export const someModule = 0; +//// export default 1; + +// @Filename: /index.ts +//// someMo/**/ + +verify.completions({ + marker: "", + exact: [ + completion.globalThisEntry, + completion.undefinedVarEntry, + { + name: "someModule", + source: "/someModule", + sourceDisplay: "./someModule", + text: "const someModule: 0", + kind: "const", + kindModifiers: "export", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "someModule", + source: "/someModule", + sourceDisplay: "./someModule", + text: "(property) default: 1", + kind: "property", + kindModifiers: "export", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + ...completion.statementKeywordsWithTypes + ], + preferences: { + includeCompletionsForModuleExports: true + } +}); + +verify.applyCodeActionFromCompletion("", { + name: "someModule", + source: "/someModule", + data: { exportName: "default", fileName: "/someModule.ts" }, + description: `Import default 'someModule' from module "./someModule"`, + newFileContent: `import someModule from "./someModule"; + +someMo` +}); diff --git a/tests/cases/fourslash/completionsImport_exportEquals_anonymous.ts b/tests/cases/fourslash/completionsImport_exportEquals_anonymous.ts index 6c80b652a9377..f279bef27cdbc 100644 --- a/tests/cases/fourslash/completionsImport_exportEquals_anonymous.ts +++ b/tests/cases/fourslash/completionsImport_exportEquals_anonymous.ts @@ -28,12 +28,20 @@ verify.completions( exact: [ completion.globalThisEntry, completion.undefinedVarEntry, - exportEntry, ...completion.statementKeywordsWithTypes ], preferences }, - { marker: "1", includes: exportEntry, preferences } + { + marker: "1", + exact: [ + completion.globalThisEntry, + completion.undefinedVarEntry, + exportEntry, + ...completion.statementKeywordsWithTypes + ], + preferences + } ); verify.applyCodeActionFromCompletion("0", { name: "fooBar", diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 387d843190454..bf81a295f023f 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -98,6 +98,13 @@ declare module ts { character: number; } + interface CompletionEntryData { + fileName?: string; + ambientModuleName?: string; + isPackageJsonImport?: true; + exportName: string; + } + function flatMap(array: ReadonlyArray, mapfn: (x: T, i: number) => U | ReadonlyArray | undefined): U[]; } @@ -253,6 +260,7 @@ declare namespace FourSlashInterface { applyCodeActionFromCompletion(markerName: string, options: { name: string, source?: string, + data?: ts.CompletionEntryData, description: string, newFileContent?: string, newRangeContent?: string, diff --git a/tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts b/tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts new file mode 100644 index 0000000000000..16dd9d7fb1dce --- /dev/null +++ b/tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts @@ -0,0 +1,48 @@ +/// + +// @Filename: /tsconfig.json +//// { "compilerOptions": { "noLib": true } } + +// @Filename: /someModule.ts +//// export const someModule = 0; +//// export default 1; + +// @Filename: /index.ts +//// someMo/**/ + +verify.completions({ + marker: "", + includes: [ + { + name: "someModule", + source: "/someModule", + sourceDisplay: "./someModule", + text: "const someModule: 0", + kind: "const", + kindModifiers: "export", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "someModule", + source: "/someModule", + sourceDisplay: "./someModule", + text: "(property) default: 1", + kind: "property", + kindModifiers: "export", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + ], + preferences: { + includeCompletionsForModuleExports: true + } +}); + +verify.applyCodeActionFromCompletion("", { + name: "someModule", + source: "/someModule", + data: { exportName: "default", fileName: "/someModule.ts" }, + description: `Import default 'someModule' from module "./someModule"`, + newFileContent: `import someModule from "./someModule";\r\n\r\nsomeMo` +});