From ba1e2a8cab451afde494f72c2dde4aa6b0bc11bb Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 19 Feb 2021 12:56:22 -0800 Subject: [PATCH 1/8] Add 'data' property to completion entry for better cooperation between completions and completion details --- src/compiler/checker.ts | 1 + src/compiler/types.ts | 1 + src/harness/client.ts | 8 +- src/harness/fourslashImpl.ts | 24 +++-- src/harness/fourslashInterfaceImpl.ts | 1 + src/harness/harnessLanguageService.ts | 4 +- src/server/protocol.ts | 7 ++ src/server/session.ts | 4 +- src/services/completions.ts | 100 ++++++++++++++---- src/services/services.ts | 4 +- src/services/shims.ts | 6 +- src/services/types.ts | 14 ++- .../cancellableLanguageServiceOperations.ts | 2 +- ...mpletionsImport_defaultAndNamedConflict.ts | 52 +++++++++ ...ompletionsImport_exportEquals_anonymous.ts | 12 ++- tests/cases/fourslash/fourslash.ts | 8 ++ 16 files changed, 201 insertions(+), 47 deletions(-) create mode 100644 tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7b70751a4adf2..615db885c599c 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 9f3042896223e..09b4a74211a5d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4172,6 +4172,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..69f4716bcd064 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,8 +216,8 @@ 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, 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); diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 454240b7a726d..552ce613a6265 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); @@ -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..bd7c98146b14d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -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 as any); }); return simplifiedResult ? result.map(details => ({ ...details, codeActions: map(details.codeActions, action => this.mapCodeAction(action)) })) 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..4ef096a7f83e5 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,13 @@ namespace ts { entries: CompletionEntry[]; } + export interface CompletionEntryData { + fileName?: string; + ambientModuleName?: string; + isPackageJsonImport?: true; + exportName: string; + } + // see comments in protocol.ts export interface CompletionEntry { name: string; @@ -1154,6 +1163,7 @@ namespace ts { isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; + 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/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, From b9de8562e6730804ec1e34b6be9a394f5c2b8b68 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 19 Feb 2021 13:13:57 -0800 Subject: [PATCH 2/8] Add doc comment --- src/services/types.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/services/types.ts b/src/services/types.ts index 4ef096a7f83e5..c09d0d7d59da3 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1139,9 +1139,16 @@ namespace ts { } 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; } @@ -1163,6 +1170,14 @@ 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; } From 1d78c0754bea65df32c21c84e25d0be8385de550 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 19 Feb 2021 13:19:43 -0800 Subject: [PATCH 3/8] Update API baselines --- .../reference/api/tsserverlibrary.d.ts | 36 +++++++++++++++++-- tests/baselines/reference/api/typescript.d.ts | 29 +++++++++++++-- 2 files changed, 59 insertions(+), 6 deletions(-) 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; From 901267c85b2256d8942db708920c09e7f4a3d8ef Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 22 Feb 2021 14:44:43 -0800 Subject: [PATCH 4/8] Add server test --- src/harness/client.ts | 2 +- src/harness/fourslashImpl.ts | 4 +- src/server/session.ts | 4 +- ...nsImport_defaultAndNamedConflict_server.ts | 48 +++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts diff --git a/src/harness/client.ts b/src/harness/client.ts index 69f4716bcd064..cd39f1d3aee03 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -216,7 +216,7 @@ namespace ts.server { }; } - getCompletionEntryDetails(fileName: string, position: number, entryName: string, _options: FormatCodeOptions | FormatCodeSettings | undefined, source: string | undefined, data: unknown): CompletionEntryDetails { + 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); diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 552ce613a6265..7c48ffbec689f 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -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) { diff --git a/src/server/session.ts b/src/server/session.ts index bd7c98146b14d..162b461b8dcc0 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1810,10 +1810,10 @@ namespace ts.server { const prefix = args.prefix || ""; const entries = 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)); 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` +}); From 11ae342e14ce6418cd84e1badd21bfea5bbeaf11 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 22 Feb 2021 14:48:33 -0800 Subject: [PATCH 5/8] =?UTF-8?q?Test=20session=E2=80=99s=20Full=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/harness/client.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/harness/client.ts b/src/harness/client.ts index cd39f1d3aee03..4acfca3e0eb91 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -219,11 +219,10 @@ namespace ts.server { 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 { From fa42e2d24271416167173a1f24497ff3444c29a4 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 22 Feb 2021 15:08:20 -0800 Subject: [PATCH 6/8] Fix tests --- src/testRunner/unittests/tsserver/completions.ts | 3 ++- src/testRunner/unittests/tsserver/partialSemanticServer.ts | 3 ++- tests/cases/fourslash/completionListForUnicodeEscapeName.ts | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) 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/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"] }, ); From 3e39819f57bb186eba04385c85ec02c726447526 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 22 Feb 2021 16:43:48 -0800 Subject: [PATCH 7/8] stableSort to fix server fourslash test --- src/server/session.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/session.ts b/src/server/session.ts index 162b461b8dcc0..46409b7fe1091 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, 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, 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; From 62db7d04628ab3c68df8f21226bf8a394ab59805 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 26 Feb 2021 15:14:45 -0800 Subject: [PATCH 8/8] Explicit verification of data parameter --- src/server/session.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/server/session.ts b/src/server/session.ts index 46409b7fe1091..aeb695bf1477b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1838,7 +1838,7 @@ namespace ts.server { const result = mapDefined(args.entryNames, entryName => { 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 as any); + 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")); + } }