From 60422cb4c953ac74a716f8862230dcc0f3ae6568 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 17 Feb 2021 11:50:18 -0800 Subject: [PATCH 01/35] WIP --- src/services/codefixes/importFixes.ts | 19 ++++++++++++++++++- src/services/completions.ts | 26 ++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 6a23151ae4d59..1344daa7ecb5e 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -222,7 +222,6 @@ namespace ts.codefix { function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol"); - // We sort the best codefixes first, so taking `first` is best. return getBestFix(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, program, host); } @@ -275,6 +274,24 @@ namespace ts.codefix { return result; } + export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean) { + const result: MultiMap = createMultiMap(); + const compilerOptions = program.getCompilerOptions(); + forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _moduleFile, program) => { + const checker = program.getTypeChecker(); + const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); + if (defaultInfo) { + const original = skipAlias(defaultInfo.symbol, checker); + result.add(original, { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker) }); + } + for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { + const original = skipAlias(exported, checker); + result.add(original, { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker) }); + } + }); + return result; + } + function isTypeOnlySymbol(s: Symbol, checker: TypeChecker): boolean { return !(skipAlias(s, checker).flags & SymbolFlags.Value); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 91aab7306e3af..5356bd3d3f0f0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1007,6 +1007,7 @@ namespace ts.Completions { let isStartingCloseTag = false; let isJsxInitializer: IsJsxInitializer = false; let isJsxIdentifierExpected = false; + let importCompletionNode: ImportEqualsDeclaration | ImportDeclaration | undefined; let location = getTouchingPropertyName(sourceFile, position); if (contextToken) { @@ -1017,6 +1018,7 @@ namespace ts.Completions { } let parent = contextToken.parent; + importCompletionNode = getImportCompletionNode(parent); if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) { isRightOfDot = contextToken.kind === SyntaxKind.DotToken; isRightOfQuestionDot = contextToken.kind === SyntaxKind.QuestionDotToken; @@ -1050,7 +1052,7 @@ namespace ts.Completions { return undefined; } } - else if (sourceFile.languageVariant === LanguageVariant.JSX) { + else if (!importCompletionNode && sourceFile.languageVariant === LanguageVariant.JSX) { // // If the tagname is a property access expression, we will then walk up to the top most of property access expression. // Then, try to get a JSX container and its associated attributes type. @@ -1403,6 +1405,7 @@ namespace ts.Completions { || tryGetConstructorCompletion() || tryGetClassLikeCompletionSymbols() || tryGetJsxCompletionSymbols() + || tryGetImportCompletionSymbols() || (getGlobalCompletions(), GlobalsSearch.Success); return result === GlobalsSearch.Success; } @@ -1431,6 +1434,13 @@ namespace ts.Completions { return GlobalsSearch.Success; } + function tryGetImportCompletionSymbols(): GlobalsSearch { + if (!importCompletionNode) return GlobalsSearch.Continue; + if (!shouldOfferImportCompletions()) return GlobalsSearch.Fail; + collectAndFilterAutoImportCompletions(); + return GlobalsSearch.Success; + } + function getGlobalCompletions(): void { keywordFilters = tryGetFunctionLikeBodyCompletionContainer(contextToken) ? KeywordCompletionFilters.FunctionLikeBodyKeywords : KeywordCompletionFilters.All; @@ -1495,7 +1505,10 @@ namespace ts.Completions { } } } + collectAndFilterAutoImportCompletions(/*resolveModuleSpecifier*/ false); + } + function collectAndFilterAutoImportCompletions(resolveModuleSpecifier: boolean) { if (shouldOfferImportCompletions()) { const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; if (detailsEntryId?.data) { @@ -2341,7 +2354,6 @@ namespace ts.Completions { case SyntaxKind.InterfaceKeyword: case SyntaxKind.FunctionKeyword: case SyntaxKind.VarKeyword: - case SyntaxKind.ImportKeyword: case SyntaxKind.LetKeyword: case SyntaxKind.ConstKeyword: case SyntaxKind.InferKeyword: @@ -2914,4 +2926,14 @@ namespace ts.Completions { } return undefined; } + + function getImportCompletionNode(parent: Node) { + if (isImportEqualsDeclaration(parent)) { + return nodeIsMissing(parent.moduleReference) ? parent : undefined; + } + if (isNamedImports(parent) || isNamespaceImport(parent)) { + return nodeIsMissing(parent.parent.parent.moduleSpecifier) ? parent.parent.parent : undefined; + } + return undefined; + } } From 081f564d39c59e0388f38489762f7eb2f1d1dfe1 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 18 Feb 2021 14:58:44 -0800 Subject: [PATCH 02/35] WIP --- src/server/protocol.ts | 11 ++- src/services/codefixes/importFixes.ts | 49 ++++++++------ src/services/completions.ts | 96 ++++++++++++++++++++++++--- src/services/types.ts | 3 + 4 files changed, 129 insertions(+), 30 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 60ec24f0f6bf0..69472b9da81fd 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2247,6 +2247,10 @@ namespace ts.server.protocol { * Identifier (not necessarily human-readable) identifying where this completion came from. */ source?: string; + /** + * Human-readable description of the `source`. + */ + sourceDisplay?: SymbolDisplayPart[]; /** * If true, this completion should be highlighted as recommended. There will only be one of these. * This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class. @@ -2308,9 +2312,14 @@ namespace ts.server.protocol { codeActions?: CodeAction[]; /** - * Human-readable description of the `source` from the CompletionEntry. + * @deprecated Use `sourceDisplay` instead. */ source?: SymbolDisplayPart[]; + + /** + * Human-readable description of the `source` from the CompletionEntry. + */ + sourceDisplay?: SymbolDisplayPart[]; } /** @deprecated Prefer CompletionInfoResponse, which supports several top-level fields in addition to the array of entries. */ diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 1344daa7ecb5e..ad0a02b9f76c7 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -190,6 +190,8 @@ namespace ts.codefix { readonly importKind: ImportKind; /** If true, can't use an es6 import from a js file. */ readonly exportedSymbolIsTypeOnly: boolean; + /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ + readonly isFromPackageJson: boolean; } /** Information needed to augment an existing import declaration. */ @@ -215,7 +217,7 @@ namespace ts.codefix { : getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, /*useAutoImportProvider*/ true); const useRequire = shouldUseRequire(sourceFile, program); const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); - const moduleSpecifier = getBestFix(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences), sourceFile, program, host).moduleSpecifier; + const moduleSpecifier = getBestModuleSpecifier(exportInfos, sourceFile, position, preferTypeOnlyImport, useRequire, program, host, preferences); const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, useRequire, host, preferences); return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) }; } @@ -231,21 +233,21 @@ namespace ts.codefix { function getSymbolExportInfoForSymbol(symbol: Symbol, moduleSymbol: Symbol, importingFile: SourceFile, program: Program, host: LanguageServiceHost): SymbolExportInfo { const compilerOptions = program.getCompilerOptions(); - const mainProgramInfo = getInfoWithChecker(program.getTypeChecker()); + const mainProgramInfo = getInfoWithChecker(program.getTypeChecker(), /*isFromPackageJson*/ false); if (mainProgramInfo) { return mainProgramInfo; } const autoImportProvider = host.getPackageJsonAutoImportProvider?.()?.getTypeChecker(); - return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider), `Could not find symbol in specified module for code actions`); + return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider, /*isFromPackageJson*/ true), `Could not find symbol in specified module for code actions`); - function getInfoWithChecker(checker: TypeChecker): SymbolExportInfo | undefined { + function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined { const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) { - return { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker) }; + return { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } const named = checker.tryGetMemberInModuleExportsAndProperties(symbol.name, moduleSymbol); if (named && skipAlias(named, checker) === symbol) { - return { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker) }; + return { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } } } @@ -253,7 +255,7 @@ namespace ts.codefix { function getAllReExportingModules(importingFile: SourceFile, exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean): readonly SymbolExportInfo[] { const result: SymbolExportInfo[] = []; const compilerOptions = program.getCompilerOptions(); - forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ false, useAutoImportProvider, (moduleSymbol, moduleFile, program) => { + forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ false, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); // Don't import from a re-export when looking "up" like to `./index` or `../index`. if (moduleFile && moduleSymbol !== exportingModuleSymbol && startsWith(importingFile.fileName, getDirectoryPath(moduleFile.fileName))) { @@ -262,31 +264,35 @@ namespace ts.codefix { const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol) { - result.push({ moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker) }); + result.push({ moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) { - result.push({ moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker) }); + result.push({ moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); return result; } + export function getBestModuleSpecifier(exportInfo: readonly SymbolExportInfo[], importingFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences) { + return getBestFix(getNewImportInfos(program, importingFile, position, preferTypeOnlyImport, useRequire, exportInfo, host, preferences), importingFile, program, host).moduleSpecifier; + } + export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean) { const result: MultiMap = createMultiMap(); const compilerOptions = program.getCompilerOptions(); - forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _moduleFile, program) => { + forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo) { const original = skipAlias(defaultInfo.symbol, checker); - result.add(original, { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker) }); + result.add(original, { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { const original = skipAlias(exported, checker); - result.add(original, { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker) }); + result.add(original, { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); } }); return result; @@ -441,12 +447,13 @@ namespace ts.codefix { ): readonly (FixAddNewImport | FixUseImportType)[] { const isJs = isSourceFileJS(sourceFile); const compilerOptions = program.getCompilerOptions(); + const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); return flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) => - moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, createModuleSpecifierResolutionHost(program, host), preferences) + moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences) .map((moduleSpecifier): FixAddNewImport | FixUseImportType => // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. - exportedSymbolIsTypeOnly && isJs - ? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.checkDefined(position, "position should be defined") } + exportedSymbolIsTypeOnly && isJs && position !== undefined + ? { kind: ImportFixKind.ImportType, moduleSpecifier, position } : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, useRequire, typeOnly: preferTypeOnlyImport })); } @@ -514,7 +521,7 @@ namespace ts.codefix { if (!umdSymbol) return undefined; const symbol = checker.getAliasedSymbol(umdSymbol); const symbolName = umdSymbol.name; - const exportInfos: readonly SymbolExportInfo[] = [{ moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false }]; + const exportInfos: readonly SymbolExportInfo[] = [{ moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false, isFromPackageJson: false }]; const useRequire = shouldUseRequire(sourceFile, program); const fixes = getFixForImport(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences); return { fixes, symbolName }; @@ -598,23 +605,23 @@ namespace ts.codefix { // For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once. // Maps symbol id to info for modules providing that symbol (original export + re-exports). const originalSymbolToExportInfos = createMultiMap(); - function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind, checker: TypeChecker): void { - originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker) }); + function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind, checker: TypeChecker, isFromPackageJson: boolean): void { + originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); } - forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _, program) => { + forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _, program, isFromPackageJson) => { const checker = program.getTypeChecker(); cancellationToken.throwIfCancellationRequested(); const compilerOptions = program.getCompilerOptions(); const defaultInfo = getDefaultLikeExportInfo(sourceFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) { - addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.kind, checker); + addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.kind, checker, isFromPackageJson); } // check exports with the same name const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { - addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ImportKind.Named, checker); + addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ImportKind.Named, checker, isFromPackageJson); } }); return originalSymbolToExportInfos; diff --git a/src/services/completions.ts b/src/services/completions.ts index 5356bd3d3f0f0..417f794f5b00b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -34,6 +34,7 @@ namespace ts.Completions { Export = 1 << 2, Promise = 1 << 3, Nullable = 1 << 4, + ResolvedExport = 1 << 5, SymbolMemberNoExport = SymbolMember, SymbolMemberExport = SymbolMember | Export, @@ -45,6 +46,7 @@ namespace ts.Completions { interface SymbolOriginInfoExport extends SymbolOriginInfo { kind: SymbolOriginInfoKind; + symbolName: string; moduleSymbol: Symbol; isDefaultExport: boolean; isFromPackageJson?: boolean; @@ -52,6 +54,13 @@ namespace ts.Completions { fileName?: string; } + interface SymbolOriginInfoResolvedExport extends SymbolOriginInfo { + kind: SymbolOriginInfoKind; + symbolName: string; + moduleSpecifier: string; + isFromPackageJson?: boolean; + } + function originIsThisType(origin: SymbolOriginInfo): boolean { return !!(origin.kind & SymbolOriginInfoKind.ThisType); } @@ -64,8 +73,16 @@ namespace ts.Completions { return !!(origin && origin.kind & SymbolOriginInfoKind.Export); } + function originIsResolvedExport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoResolvedExport { + return !!(origin && origin.kind === SymbolOriginInfoKind.ResolvedExport); + } + + function originIncludesSymbolName(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport | SymbolOriginInfoResolvedExport { + return originIsExport(origin) || originIsResolvedExport(origin); + } + function originIsPackageJsonImport(origin: SymbolOriginInfo | undefined): origin is SymbolOriginInfoExport { - return originIsExport(origin) && !!origin.isFromPackageJson; + return (originIsExport(origin) || originIsResolvedExport(origin)) && !!origin.isFromPackageJson; } function originIsPromise(origin: SymbolOriginInfo): boolean { @@ -85,7 +102,7 @@ namespace ts.Completions { * Map from symbol id -> SymbolOriginInfo. * Only populated for symbols that come from other modules. */ - type SymbolOriginInfoMap = (SymbolOriginInfo | SymbolOriginInfoExport | undefined)[]; + type SymbolOriginInfoMap = (SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined)[]; type SymbolSortTextMap = (SortText | undefined)[]; @@ -108,6 +125,11 @@ namespace ts.Completions { symbolName: string; origin: SymbolOriginInfoExport; } + export interface AutoImportSuggestionWithModuleSpecifier { + symbol: Symbol; + symbolName: string; + moduleSpecifier: string; + } export interface ImportSuggestionsForFileCache { clear(): void; get(fileName: string, checker: TypeChecker, projectVersion?: string): readonly AutoImportSuggestion[] | undefined; @@ -470,6 +492,10 @@ namespace ts.Completions { replacementSpan = createTextSpanFromBounds(propertyAccessToConvert.getStart(sourceFile), propertyAccessToConvert.end); } + if (originIsResolvedExport(origin)) { + insertText = `${origin.symbolName} from "${origin.moduleSpecifier}"`; + } + if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { return undefined; } @@ -523,6 +549,9 @@ namespace ts.Completions { if (originIsExport(origin)) { return stripQuotes(origin.moduleSymbol.name); } + if (originIsResolvedExport(origin)) { + return origin.moduleSpecifier; + } if (origin?.kind === SymbolOriginInfoKind.ThisType) { return CompletionSource.ThisProperty; } @@ -750,7 +779,7 @@ namespace ts.Completions { } export function createCompletionDetails(name: string, kindModifiers: string, kind: ScriptElementKind, displayParts: SymbolDisplayPart[], documentation?: SymbolDisplayPart[], tags?: JSDocTagInfo[], codeActions?: CodeAction[], source?: SymbolDisplayPart[]): CompletionEntryDetails { - return { name, kindModifiers, kind, displayParts, documentation, tags, codeActions, source }; + return { name, kindModifiers, kind, displayParts, documentation, tags, codeActions, source, sourceDisplay: source }; } interface CodeActionsAndSourceDisplay { @@ -1007,7 +1036,7 @@ namespace ts.Completions { let isStartingCloseTag = false; let isJsxInitializer: IsJsxInitializer = false; let isJsxIdentifierExpected = false; - let importCompletionNode: ImportEqualsDeclaration | ImportDeclaration | undefined; + let importCompletionNode: ImportEqualsDeclaration | ImportDeclaration | Token | undefined; let location = getTouchingPropertyName(sourceFile, position); if (contextToken) { @@ -1018,7 +1047,7 @@ namespace ts.Completions { } let parent = contextToken.parent; - importCompletionNode = getImportCompletionNode(parent); + importCompletionNode = getImportCompletionNode(contextToken); if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) { isRightOfDot = contextToken.kind === SyntaxKind.DotToken; isRightOfQuestionDot = contextToken.kind === SyntaxKind.QuestionDotToken; @@ -1437,7 +1466,7 @@ namespace ts.Completions { function tryGetImportCompletionSymbols(): GlobalsSearch { if (!importCompletionNode) return GlobalsSearch.Continue; if (!shouldOfferImportCompletions()) return GlobalsSearch.Fail; - collectAndFilterAutoImportCompletions(); + collectAndFilterAutoImportCompletions(!!importCompletionNode); return GlobalsSearch.Success; } @@ -1519,6 +1548,9 @@ namespace ts.Completions { symbolToOriginInfoMap[symbolId] = autoImport.origin; } } + else if (resolveModuleSpecifier) { + collectAutoImportsWithModuleSpecifiers(lowerCaseTokenText); + } else { const autoImportSuggestions = getSymbolsFromOtherSourceFileExports(program.getCompilerOptions().target!, host); if (!detailsEntryId && importSuggestionsCache) { @@ -1689,6 +1721,47 @@ namespace ts.Completions { typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules)); } + /** Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` */ + function collectAutoImportsWithModuleSpecifiers(lowerCaseTokenText: string) { + if (detailsEntryId?.source && host.resolveModuleNames) { + const resolved = Debug.checkDefined( + host.resolveModuleNames([detailsEntryId.source], sourceFile.fileName, /*reusedNames*/ undefined, /*redirectedReference*/ undefined, program.getCompilerOptions())[0], + "Completion entry source did not pass module resolution" + ); + + const moduleSymbol = Debug.checkDefined( + program.getSourceFile(resolved.resolvedFileName) || host.getPackageJsonAutoImportProvider?.()?.getSourceFile(resolved.resolvedFileName), + "Could not find an existing SourceFile for completion entry source" + ).symbol; + + const checker = program.getTypeChecker(); + const exportedSymbol = Debug.checkDefined( + checker.tryGetMemberInModuleExportsAndProperties(detailsEntryId.name, moduleSymbol) || + host.getPackageJsonAutoImportProvider?.()?.getTypeChecker().tryGetMemberInModuleExportsAndProperties(detailsEntryId.name, moduleSymbol), + "Could not find an export by the completion entry name at the completion entry source" + ); + + const symbolId = getSymbolId(exportedSymbol); + symbols.push(exportedSymbol); + symbolToOriginInfoMap[symbolId] = { kind: SymbolOriginInfoKind.ResolvedExport, moduleSpecifier: detailsEntryId.source, symbolName: detailsEntryId.name }; + return; + } + + const target = getEmitScriptTarget(program.getCompilerOptions()); + const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*useAutoImportProvider*/ true); + exportInfo.forEach((info, symbol) => { + const symbolName = getNameForExportedSymbol(symbol, target); + if (stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { + const moduleSpecifier = codefix.getBestModuleSpecifier(info, sourceFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, program, host, preferences); + const symbolId = getSymbolId(symbol); + symbols.push(symbol); + // `isFromPackageJson` should all be the same + symbolToOriginInfoMap[symbolId] = { kind: SymbolOriginInfoKind.ResolvedExport, moduleSpecifier, symbolName, isFromPackageJson: info[0].isFromPackageJson }; + symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + } + }); + } + /** * Gathers symbols that can be imported from other files, de-duplicating along the way. Symbols can be "duplicates" * if re-exported from another module by the same name, e.g. `export { foo } from "./a"`. @@ -1757,6 +1830,7 @@ namespace ts.Completions { const origin: SymbolOriginInfoExport = { kind: SymbolOriginInfoKind.Export, moduleSymbol, + symbolName, isDefaultExport, isFromPackageJson, exportName, @@ -1791,6 +1865,7 @@ namespace ts.Completions { origin: { kind: SymbolOriginInfoKind.Export, moduleSymbol, + symbolName: "", isDefaultExport, exportName: data.exportName, fileName: data.fileName, @@ -2610,7 +2685,7 @@ namespace ts.Completions { kind: CompletionKind, jsxIdentifierExpected: boolean, ): CompletionEntryDisplayNameForSymbol | undefined { - const name = originIsExport(origin) ? getNameForExportedSymbol(symbol, target) : symbol.name; + const name = originIncludesSymbolName(origin) ? origin.symbolName : symbol.name; if (name === undefined // If the symbol is external module, don't show it in the completion list // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) @@ -2927,13 +3002,18 @@ namespace ts.Completions { return undefined; } - function getImportCompletionNode(parent: Node) { + function getImportCompletionNode(contextToken: Node) { + const parent = contextToken.parent; if (isImportEqualsDeclaration(parent)) { return nodeIsMissing(parent.moduleReference) ? parent : undefined; } if (isNamedImports(parent) || isNamespaceImport(parent)) { return nodeIsMissing(parent.parent.parent.moduleSpecifier) ? parent.parent.parent : undefined; } + if (isImportKeyword(contextToken) && isSourceFile(parent)) { + // A lone import keyword with nothing following it does not parse as a statement at all + return contextToken; + } return undefined; } } diff --git a/src/services/types.ts b/src/services/types.ts index 9542ad56600fc..02a2c11014da3 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1168,6 +1168,7 @@ namespace ts { replacementSpan?: TextSpan; hasAction?: true; source?: string; + sourceDisplay?: SymbolDisplayPart[]; isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; @@ -1190,7 +1191,9 @@ namespace ts { documentation?: SymbolDisplayPart[]; tags?: JSDocTagInfo[]; codeActions?: CodeAction[]; + /** @deprecated Use `sourceDisplay` instead. */ source?: SymbolDisplayPart[]; + sourceDisplay?: SymbolDisplayPart[]; } export interface OutliningSpan { From 48bfb22e312fcfae6a5bd3056888cbaa48efbb4d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 3 Mar 2021 12:22:03 -0800 Subject: [PATCH 03/35] Get completion details working --- src/compiler/utilities.ts | 13 ++ src/services/codefixes/importFixes.ts | 71 +++---- src/services/completions.ts | 177 ++++++++++-------- src/services/types.ts | 4 + .../fourslash/completionEntryForImportName.ts | 22 --- 5 files changed, 148 insertions(+), 139 deletions(-) delete mode 100644 tests/cases/fourslash/completionEntryForImportName.ts diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f3c08902f55ff..dfd0912ba3efb 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2401,6 +2401,19 @@ namespace ts { return decl.kind === SyntaxKind.FunctionDeclaration || isVariableDeclaration(decl) && decl.initializer && isFunctionLike(decl.initializer); } + export function tryGetModuleSpecifierFromDeclaration(node: AnyImportOrRequire): string | undefined { + switch (node.kind) { + case SyntaxKind.VariableDeclaration: + return node.initializer.arguments[0].text; + case SyntaxKind.ImportDeclaration: + return tryCast(node.moduleSpecifier, isStringLiteralLike)?.text; + case SyntaxKind.ImportEqualsDeclaration: + return tryCast(tryCast(node.moduleReference, isExternalModuleReference)?.expression, isStringLiteralLike)?.text; + default: + Debug.assertNever(node); + } + } + export function importFromModuleSpecifier(node: StringLiteralLike): AnyValidImportOrReExport { return tryGetImportFromModuleSpecifier(node) || Debug.failBadSyntaxKind(node.parent); } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index ad0a02b9f76c7..fbf45fbfc6b07 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -156,11 +156,13 @@ namespace ts.codefix { readonly kind: ImportFixKind.UseNamespace; readonly namespacePrefix: string; readonly position: number; + readonly moduleSpecifier: string; } interface FixUseImportType { readonly kind: ImportFixKind.ImportType; readonly moduleSpecifier: string; readonly position: number; + readonly exportInfo: SymbolExportInfo; } interface FixAddToExistingImport { readonly kind: ImportFixKind.AddToExisting; @@ -175,6 +177,7 @@ namespace ts.codefix { readonly importKind: ImportKind; readonly typeOnly: boolean; readonly useRequire: boolean; + readonly exportInfo?: SymbolExportInfo; } const enum ImportKind { @@ -184,8 +187,9 @@ namespace ts.codefix { CommonJS, } - /** Information about how a symbol is exported from a module. (We don't need to store the exported symbol, just its module.) */ + /** Information about how a symbol is exported from a module. */ interface SymbolExportInfo { + readonly symbol: Symbol; readonly moduleSymbol: Symbol; readonly importKind: ImportKind; /** If true, can't use an es6 import from a js file. */ @@ -217,14 +221,13 @@ namespace ts.codefix { : getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, /*useAutoImportProvider*/ true); const useRequire = shouldUseRequire(sourceFile, program); const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); - const moduleSpecifier = getBestModuleSpecifier(exportInfos, sourceFile, position, preferTypeOnlyImport, useRequire, program, host, preferences); const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, useRequire, host, preferences); - return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) }; + return { moduleSpecifier: fix.moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) }; } function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol"); - return getBestFix(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, program, host); + return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, program, host); } function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction { @@ -243,11 +246,11 @@ namespace ts.codefix { function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined { const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) { - return { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } const named = checker.tryGetMemberInModuleExportsAndProperties(symbol.name, moduleSymbol); if (named && skipAlias(named, checker) === symbol) { - return { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: named, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } } } @@ -264,35 +267,35 @@ namespace ts.codefix { const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol) { - result.push({ moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); + result.push({ symbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) { - result.push({ moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + result.push({ symbol: exported, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); return result; } - export function getBestModuleSpecifier(exportInfo: readonly SymbolExportInfo[], importingFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences) { - return getBestFix(getNewImportInfos(program, importingFile, position, preferTypeOnlyImport, useRequire, exportInfo, host, preferences), importingFile, program, host).moduleSpecifier; + export function getBestImportFixForExports(exportInfo: readonly SymbolExportInfo[], importingFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences) { + return getBestFix(getNewImportFixes(program, importingFile, position, preferTypeOnlyImport, useRequire, exportInfo, host, preferences), importingFile, program, host); } export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean) { - const result: MultiMap = createMultiMap(); + const result: MultiMap = createMultiMap(); const compilerOptions = program.getCompilerOptions(); forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo) { const original = skipAlias(defaultInfo.symbol, checker); - result.add(original, { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); + result.add(original, { symbol: defaultInfo.symbol, exportSymbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { const original = skipAlias(exported, checker); - result.add(original, { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); + result.add(original, { symbol: exported, exportSymbol: exported, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); } }); return result; @@ -306,7 +309,7 @@ namespace ts.codefix { return isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); } - function getFixForImport( + function getImportFixes( exportInfos: readonly SymbolExportInfo[], symbolName: string, /** undefined only for missing JSX namespace */ @@ -342,10 +345,11 @@ namespace ts.codefix { // and it is up to the user to decide which one fits best. return firstDefined(existingImports, ({ declaration }): FixUseNamespaceImport | undefined => { const namespacePrefix = getNamespaceLikeImportText(declaration); - if (namespacePrefix) { + const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration); + if (namespacePrefix && moduleSpecifier) { const moduleSymbol = getTargetModuleFromNamespaceLikeImport(declaration, checker); if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(symbolName))) { - return { kind: ImportFixKind.UseNamespace, namespacePrefix, position }; + return { kind: ImportFixKind.UseNamespace, namespacePrefix, position, moduleSpecifier }; } } }); @@ -387,10 +391,10 @@ namespace ts.codefix { : undefined; } const { importClause } = declaration; - if (!importClause) return undefined; + if (!importClause || !isStringLiteralLike(declaration.moduleSpecifier)) return undefined; const { name, namedBindings } = importClause; return importKind === ImportKind.Default && !name || importKind === ImportKind.Named && (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports) - ? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: importClause, importKind, moduleSpecifier: declaration.moduleSpecifier.getText(), canUseTypeOnlyImport } + ? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: importClause, importKind, moduleSpecifier: declaration.moduleSpecifier.text, canUseTypeOnlyImport } : undefined; }); } @@ -435,7 +439,7 @@ namespace ts.codefix { return true; } - function getNewImportInfos( + function getNewImportFixes( program: Program, sourceFile: SourceFile, position: number | undefined, @@ -448,13 +452,13 @@ namespace ts.codefix { const isJs = isSourceFileJS(sourceFile); const compilerOptions = program.getCompilerOptions(); const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); - return flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) => - moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences) + return flatMap(moduleSymbols, exportInfo => + moduleSpecifiers.getModuleSpecifiers(exportInfo.moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences) .map((moduleSpecifier): FixAddNewImport | FixUseImportType => // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. - exportedSymbolIsTypeOnly && isJs && position !== undefined - ? { kind: ImportFixKind.ImportType, moduleSpecifier, position } - : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, useRequire, typeOnly: preferTypeOnlyImport })); + exportInfo.exportedSymbolIsTypeOnly && isJs && position !== undefined + ? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo } + : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind: exportInfo.importKind, useRequire, typeOnly: preferTypeOnlyImport, exportInfo })); } function getFixesForAddImport( @@ -469,16 +473,13 @@ namespace ts.codefix { preferences: UserPreferences, ): readonly (FixAddNewImport | FixUseImportType)[] { const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, preferTypeOnlyImport, useRequire)); - return existingDeclaration ? [existingDeclaration] : getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences); + return existingDeclaration ? [existingDeclaration] : getNewImportFixes(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences); } function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo, preferTypeOnlyImport: boolean, useRequire: boolean): FixAddNewImport | undefined { - const moduleSpecifier = declaration.kind === SyntaxKind.ImportDeclaration ? declaration.moduleSpecifier : - declaration.kind === SyntaxKind.VariableDeclaration ? declaration.initializer.arguments[0] : - declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference ? declaration.moduleReference.expression : - undefined; - return moduleSpecifier && isStringLiteral(moduleSpecifier) - ? { kind: ImportFixKind.AddNew, moduleSpecifier: moduleSpecifier.text, importKind, typeOnly: preferTypeOnlyImport, useRequire } + const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration); + return moduleSpecifier + ? { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, typeOnly: preferTypeOnlyImport, useRequire } : undefined; } @@ -521,9 +522,9 @@ namespace ts.codefix { if (!umdSymbol) return undefined; const symbol = checker.getAliasedSymbol(umdSymbol); const symbolName = umdSymbol.name; - const exportInfos: readonly SymbolExportInfo[] = [{ moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false, isFromPackageJson: false }]; + const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false, isFromPackageJson: false }]; const useRequire = shouldUseRequire(sourceFile, program); - const fixes = getFixForImport(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences); + const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences); return { fixes, symbolName }; } function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined { @@ -577,7 +578,7 @@ namespace ts.codefix { const useRequire = shouldUseRequire(sourceFile, program); const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host); const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) => - getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences))); + getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences))); return { fixes, symbolName }; } @@ -606,7 +607,7 @@ namespace ts.codefix { // Maps symbol id to info for modules providing that symbol (original export + re-exports). const originalSymbolToExportInfos = createMultiMap(); function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind, checker: TypeChecker, isFromPackageJson: boolean): void { - originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); + originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); } forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _, program, isFromPackageJson) => { const checker = program.getTypeChecker(); diff --git a/src/services/completions.ts b/src/services/completions.ts index 417f794f5b00b..ed5e70ef6001d 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -54,11 +54,8 @@ namespace ts.Completions { fileName?: string; } - interface SymbolOriginInfoResolvedExport extends SymbolOriginInfo { - kind: SymbolOriginInfoKind; - symbolName: string; + interface SymbolOriginInfoResolvedExport extends SymbolOriginInfoExport { moduleSpecifier: string; - isFromPackageJson?: boolean; } function originIsThisType(origin: SymbolOriginInfo): boolean { @@ -102,7 +99,7 @@ namespace ts.Completions { * Map from symbol id -> SymbolOriginInfo. * Only populated for symbols that come from other modules. */ - type SymbolOriginInfoMap = (SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined)[]; + type SymbolOriginInfoMap = Record; type SymbolSortTextMap = (SortText | undefined)[]; @@ -500,12 +497,13 @@ namespace ts.Completions { return undefined; } - if (originIsExport(origin)) { + if (originIsExport(origin) || originIsResolvedExport(origin)) { data = { exportName: origin.exportName, fileName: origin.fileName, ambientModuleName: origin.fileName ? undefined : stripQuotes(origin.moduleSymbol.name), isPackageJsonImport: origin.isFromPackageJson ? true : undefined, + moduleSpecifier: originIsResolvedExport(origin) ? origin.moduleSpecifier : undefined, }; } @@ -680,6 +678,23 @@ namespace ts.Completions { host: LanguageServiceHost, preferences: UserPreferences, ): SymbolCompletion | { type: "request", request: Request } | { type: "literal", literal: string | number | PseudoBigInt } | { type: "none" } { + if (entryId.data) { + const autoImport = getAutoImportSymbolFromCompletionEntryData(entryId.name, entryId.data, program, host); + if (autoImport) { + return { + type: "symbol", + symbol: autoImport.symbol, + location: getTouchingPropertyName(sourceFile, position), + previousToken: findPrecedingToken(position, sourceFile, /*startNode*/ undefined)!, + isJsxInitializer: false, + isTypeOnlyLocation: false, + symbolToOriginInfoMap: { + [getSymbolId(autoImport.symbol)]: autoImport.origin + } + }; + } + } + const compilerOptions = program.getCompilerOptions(); const completionData = getCompletionData(program, log, sourceFile, isUncheckedFile(sourceFile, compilerOptions), position, { includeCompletionsForModuleExports: true, includeCompletionsWithInsertText: true }, entryId, host); if (!completionData) { @@ -751,7 +766,7 @@ namespace ts.Completions { } case "symbol": { const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion; - const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences); + const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, entryId.data); return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } case "literal": { @@ -798,7 +813,12 @@ namespace ts.Completions { previousToken: Node | undefined, formatContext: formatting.FormatContext, preferences: UserPreferences, + data: CompletionEntryData | undefined, ): CodeActionsAndSourceDisplay { + if (data?.moduleSpecifier) { + return { codeActions: undefined, sourceDisplay: [textPart(data.moduleSpecifier)] }; + } + const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)]; if (!symbolOriginInfo || !originIsExport(symbolOriginInfo)) { return { codeActions: undefined, sourceDisplay: undefined }; @@ -1383,10 +1403,20 @@ namespace ts.Completions { if (firstAccessibleSymbol && !symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)]) { symbols.push(firstAccessibleSymbol); const moduleSymbol = firstAccessibleSymbol.parent; - symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = - !moduleSymbol || !isExternalModuleSymbol(moduleSymbol) - ? { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) } - : { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), moduleSymbol, isDefaultExport: false }; + if (!moduleSymbol || !isExternalModuleSymbol(moduleSymbol)) { + symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }; + } + else { + const origin: SymbolOriginInfoExport = { + kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), + moduleSymbol, + isDefaultExport: false, + symbolName: firstAccessibleSymbol.name, + exportName: firstAccessibleSymbol.name, + fileName: isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? cast(moduleSymbol.declarations![0], isSourceFile).fileName : undefined, + }; + symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = origin; + } } else if (preferences.includeCompletionsWithInsertText) { addSymbolOriginInfo(symbol); @@ -1538,17 +1568,10 @@ namespace ts.Completions { } function collectAndFilterAutoImportCompletions(resolveModuleSpecifier: boolean) { + Debug.assert(!detailsEntryId?.data); if (shouldOfferImportCompletions()) { const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; - if (detailsEntryId?.data) { - const autoImport = getAutoImportSymbolFromCompletionEntryData(detailsEntryId.data); - if (autoImport) { - const symbolId = getSymbolId(autoImport.symbol); - symbols.push(autoImport.symbol); - symbolToOriginInfoMap[symbolId] = autoImport.origin; - } - } - else if (resolveModuleSpecifier) { + if (resolveModuleSpecifier) { collectAutoImportsWithModuleSpecifiers(lowerCaseTokenText); } else { @@ -1723,42 +1746,32 @@ namespace ts.Completions { /** Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` */ function collectAutoImportsWithModuleSpecifiers(lowerCaseTokenText: string) { - if (detailsEntryId?.source && host.resolveModuleNames) { - const resolved = Debug.checkDefined( - host.resolveModuleNames([detailsEntryId.source], sourceFile.fileName, /*reusedNames*/ undefined, /*redirectedReference*/ undefined, program.getCompilerOptions())[0], - "Completion entry source did not pass module resolution" - ); - - const moduleSymbol = Debug.checkDefined( - program.getSourceFile(resolved.resolvedFileName) || host.getPackageJsonAutoImportProvider?.()?.getSourceFile(resolved.resolvedFileName), - "Could not find an existing SourceFile for completion entry source" - ).symbol; - - const checker = program.getTypeChecker(); - const exportedSymbol = Debug.checkDefined( - checker.tryGetMemberInModuleExportsAndProperties(detailsEntryId.name, moduleSymbol) || - host.getPackageJsonAutoImportProvider?.()?.getTypeChecker().tryGetMemberInModuleExportsAndProperties(detailsEntryId.name, moduleSymbol), - "Could not find an export by the completion entry name at the completion entry source" - ); - - const symbolId = getSymbolId(exportedSymbol); - symbols.push(exportedSymbol); - symbolToOriginInfoMap[symbolId] = { kind: SymbolOriginInfoKind.ResolvedExport, moduleSpecifier: detailsEntryId.source, symbolName: detailsEntryId.name }; - return; - } - + Debug.assert(!detailsEntryId?.data); const target = getEmitScriptTarget(program.getCompilerOptions()); const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*useAutoImportProvider*/ true); - exportInfo.forEach((info, symbol) => { - const symbolName = getNameForExportedSymbol(symbol, target); - if (stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { - const moduleSpecifier = codefix.getBestModuleSpecifier(info, sourceFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, program, host, preferences); - const symbolId = getSymbolId(symbol); - symbols.push(symbol); - // `isFromPackageJson` should all be the same - symbolToOriginInfoMap[symbolId] = { kind: SymbolOriginInfoKind.ResolvedExport, moduleSpecifier, symbolName, isFromPackageJson: info[0].isFromPackageJson }; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; - } + exportInfo.forEach(info => { + const reExportsByName = arrayToMultiMap(info, ({ exportSymbol }) => getNameForExportedSymbol(exportSymbol, target)); + reExportsByName.forEach((info, symbolName) => { + if (stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { + const { moduleSpecifier, exportInfo } = codefix.getBestImportFixForExports(info, sourceFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, program, host, preferences); + if (!exportInfo) return; + const symbolId = getSymbolId(exportInfo.symbol); + const isAmbientModule = !isExternalModuleNameRelative(stripQuotes(exportInfo.moduleSymbol.name)); + const origin: SymbolOriginInfoResolvedExport = { + kind: SymbolOriginInfoKind.ResolvedExport, + moduleSpecifier, + symbolName, + exportName: exportInfo.symbol.name, + fileName: isAmbientModule ? undefined : cast(exportInfo.moduleSymbol.declarations![0], isSourceFile).fileName, + isDefaultExport: exportInfo.symbol.name === InternalSymbolName.Default, + moduleSymbol: exportInfo.moduleSymbol, + isFromPackageJson: info[0].isFromPackageJson // should be the same for each 'info' + }; + symbols.push(exportInfo.symbol); + symbolToOriginInfoMap[symbolId] = origin; + symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + } + }); }); } @@ -1845,34 +1858,6 @@ namespace ts.Completions { } } - 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, - symbolName: "", - 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 @@ -2674,6 +2659,34 @@ namespace ts.Completions { } } + function getAutoImportSymbolFromCompletionEntryData(name: string, data: CompletionEntryData, program: Program, host: LanguageServiceHost): { 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: data.moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, + moduleSymbol, + symbolName: name, + isDefaultExport, + exportName: data.exportName, + fileName: data.fileName, + } + }; + } + interface CompletionEntryDisplayNameForSymbol { readonly name: string; readonly needsConvertPropertyAccess: boolean; diff --git a/src/services/types.ts b/src/services/types.ts index 02a2c11014da3..f92ac6d560ecf 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1151,6 +1151,10 @@ namespace ts { * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. */ exportName: string; + /** + * Set for auto imports with eagerly resolved module specifiers. + */ + moduleSpecifier?: string; } // see comments in protocol.ts diff --git a/tests/cases/fourslash/completionEntryForImportName.ts b/tests/cases/fourslash/completionEntryForImportName.ts deleted file mode 100644 index 2567d7f3e117c..0000000000000 --- a/tests/cases/fourslash/completionEntryForImportName.ts +++ /dev/null @@ -1,22 +0,0 @@ -/// - -////import /*1*/ /*2*/ - -verify.completions({ marker: "1", exact: undefined }); -edit.insert('q'); -verify.completions({ exact: undefined }); -verifyIncompleteImportName(); - -goTo.marker('2'); -edit.insert(" = "); -verifyIncompleteImportName(); - -goTo.marker("2"); -edit.moveRight(" = ".length); -edit.insert("a."); -verifyIncompleteImportName(); - -function verifyIncompleteImportName() { - verify.completions({ marker: "1", exact: undefined }); - verify.quickInfoIs("import q"); -} \ No newline at end of file From 9e456b524cb622dae7efc6bd6747bb58b2d6040d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Wed, 3 Mar 2021 18:12:38 -0800 Subject: [PATCH 04/35] Start unifying eager and lazy auto imports --- src/services/codefixes/importFixes.ts | 18 +- src/services/completions.ts | 246 ++++++++---------- ...mpletionsImport_defaultAndNamedConflict.ts | 8 +- ...nsImport_defaultAndNamedConflict_server.ts | 8 +- 4 files changed, 134 insertions(+), 146 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index fbf45fbfc6b07..bcd0fc054b7e3 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -284,21 +284,29 @@ namespace ts.codefix { } export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean) { - const result: MultiMap = createMultiMap(); + const result: MultiMap = createMultiMap(); const compilerOptions = program.getCompilerOptions(); + const target = getEmitScriptTarget(compilerOptions); forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo) { - const original = skipAlias(defaultInfo.symbol, checker); - result.add(original, { symbol: defaultInfo.symbol, exportSymbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); + const name = getNameForExportedSymbol(getLocalSymbolForExportDefault(defaultInfo.symbol) || defaultInfo.symbol, target); + result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), { symbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { - const original = skipAlias(exported, checker); - result.add(original, { symbol: exported, exportSymbol: exported, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(original, checker), isFromPackageJson }); + if (exported !== defaultInfo?.symbol) { + result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), { symbol: exported, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + } } }); return result; + + function key(name: string, alias: Symbol, moduleSymbol: Symbol, checker: TypeChecker) { + const moduleName = stripQuotes(moduleSymbol.name); + const moduleKey = isExternalModuleNameRelative(moduleName) ? "/" : moduleName; + return `${name}|${getSymbolId(skipAlias(alias, checker))}|${moduleKey}`; + } } function isTypeOnlySymbol(s: Symbol, checker: TypeChecker): boolean { diff --git a/src/services/completions.ts b/src/services/completions.ts index ed5e70ef6001d..9a31c83ba823f 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1193,7 +1193,7 @@ namespace ts.Completions { let symbols: Symbol[] = []; const symbolToOriginInfoMap: SymbolOriginInfoMap = []; const symbolToSortTextMap: SymbolSortTextMap = []; - const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache(); + // const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache(); const isTypeOnly = isTypeOnlyCompletion(); if (isRightOfDot || isRightOfQuestionDot) { @@ -1571,30 +1571,7 @@ namespace ts.Completions { Debug.assert(!detailsEntryId?.data); if (shouldOfferImportCompletions()) { const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; - if (resolveModuleSpecifier) { - collectAutoImportsWithModuleSpecifiers(lowerCaseTokenText); - } - 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; - } - - const symbolId = getSymbolId(symbol); - symbols.push(symbol); - symbolToOriginInfoMap[symbolId] = origin; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; - }); - } + collectAutoImports(lowerCaseTokenText, resolveModuleSpecifier); } filterGlobalCompletion(symbols); } @@ -1745,33 +1722,36 @@ namespace ts.Completions { } /** Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` */ - function collectAutoImportsWithModuleSpecifiers(lowerCaseTokenText: string) { + function collectAutoImports(lowerCaseTokenText: string, resolveModuleSpecifiers: boolean) { Debug.assert(!detailsEntryId?.data); - const target = getEmitScriptTarget(program.getCompilerOptions()); const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*useAutoImportProvider*/ true); - exportInfo.forEach(info => { - const reExportsByName = arrayToMultiMap(info, ({ exportSymbol }) => getNameForExportedSymbol(exportSymbol, target)); - reExportsByName.forEach((info, symbolName) => { - if (stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { - const { moduleSpecifier, exportInfo } = codefix.getBestImportFixForExports(info, sourceFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, program, host, preferences); - if (!exportInfo) return; - const symbolId = getSymbolId(exportInfo.symbol); - const isAmbientModule = !isExternalModuleNameRelative(stripQuotes(exportInfo.moduleSymbol.name)); - const origin: SymbolOriginInfoResolvedExport = { - kind: SymbolOriginInfoKind.ResolvedExport, - moduleSpecifier, - symbolName, - exportName: exportInfo.symbol.name, - fileName: isAmbientModule ? undefined : cast(exportInfo.moduleSymbol.declarations![0], isSourceFile).fileName, - isDefaultExport: exportInfo.symbol.name === InternalSymbolName.Default, - moduleSymbol: exportInfo.moduleSymbol, - isFromPackageJson: info[0].isFromPackageJson // should be the same for each 'info' - }; - symbols.push(exportInfo.symbol); - symbolToOriginInfoMap[symbolId] = origin; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; - } - }); + exportInfo.forEach((info, key) => { + const [symbolName] = key.split("|"); + if (stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { + // If we don't need to resolve module specifiers, it doesn't matter which SymbolExportInfo + // we use. Each is importable by the same name and resolves to the same declaration. + const { moduleSpecifier, exportInfo } = resolveModuleSpecifiers + ? codefix.getBestImportFixForExports(info, sourceFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, program, host, preferences) + : { moduleSpecifier: undefined, exportInfo: info[0] }; + if (!exportInfo) return; + const isDefaultExport = exportInfo.symbol.name === InternalSymbolName.Default; + const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; + const symbolId = getSymbolId(symbol); + const isAmbientModule = !isExternalModuleNameRelative(stripQuotes(exportInfo.moduleSymbol.name)); + const origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport = { + kind: resolveModuleSpecifiers ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, + moduleSpecifier, + symbolName, + exportName: exportInfo.symbol.name, + fileName: isAmbientModule ? undefined : cast(exportInfo.moduleSymbol.valueDeclaration, isSourceFile).fileName, + isDefaultExport, + moduleSymbol: exportInfo.moduleSymbol, + isFromPackageJson: exportInfo.isFromPackageJson, + }; + symbols.push(symbol); + symbolToOriginInfoMap[symbolId] = origin; + symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + } }); } @@ -1779,84 +1759,84 @@ namespace ts.Completions { * Gathers symbols that can be imported from other files, de-duplicating along the way. Symbols can be "duplicates" * if re-exported from another module by the same name, e.g. `export { foo } from "./a"`. */ - function getSymbolsFromOtherSourceFileExports(target: ScriptTarget, host: LanguageServiceHost): readonly AutoImportSuggestion[] { - const cached = importSuggestionsCache && importSuggestionsCache.get( - sourceFile.fileName, - typeChecker, - detailsEntryId && host.getProjectVersion ? host.getProjectVersion() : undefined); - - if (cached) { - log("getSymbolsFromOtherSourceFileExports: Using cached list"); - return cached; - } - - const startTime = timestamp(); - log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`); - const seenResolvedModules = new Map(); - const results = createMultiMap(); - - 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; - } - - const typeChecker = program.getTypeChecker(); - const resolvedModuleSymbol = typeChecker.resolveExternalModuleSymbol(moduleSymbol); - // resolvedModuleSymbol may be a namespace. A namespace may be `export =` by multiple module declarations, but only keep the first one. - if (!addToSeen(seenResolvedModules, getSymbolId(resolvedModuleSymbol))) { - return; - } - - // 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, InternalSymbolName.ExportEquals, moduleSymbol, file, isFromPackageJson); - } - - for (const symbol of typeChecker.getExportsAndPropertiesOfModule(moduleSymbol)) { - // If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion. - if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) { - continue; - } - - pushSymbol(symbol, symbol.name, moduleSymbol, file, isFromPackageJson); - } - }); - - log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`); - return flatten(arrayFrom(results.values())); - - function pushSymbol(symbol: Symbol, exportName: string, moduleSymbol: Symbol, file: SourceFile | undefined, isFromPackageJson: boolean) { - const isDefaultExport = symbol.escapedName === InternalSymbolName.Default; - const nonLocalSymbol = symbol; - if (isDefaultExport) { - symbol = getLocalSymbolForExportDefault(symbol) || symbol; - } - if (typeChecker.isUndefinedSymbol(symbol)) { - return; - } - const original = skipAlias(nonLocalSymbol, typeChecker); - 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, - symbolName, - isDefaultExport, - isFromPackageJson, - exportName, - fileName: file?.fileName - }; - results.add(getSymbolId(original), { - symbol, - symbolName, - origin, - }); - } - } - } + // function getSymbolsFromOtherSourceFileExports(target: ScriptTarget, host: LanguageServiceHost): readonly AutoImportSuggestion[] { + // const cached = importSuggestionsCache && importSuggestionsCache.get( + // sourceFile.fileName, + // typeChecker, + // detailsEntryId && host.getProjectVersion ? host.getProjectVersion() : undefined); + + // if (cached) { + // log("getSymbolsFromOtherSourceFileExports: Using cached list"); + // return cached; + // } + + // const startTime = timestamp(); + // log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`); + // const seenResolvedModules = new Map(); + // const results = createMultiMap(); + + // 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; + // } + + // const typeChecker = program.getTypeChecker(); + // const resolvedModuleSymbol = typeChecker.resolveExternalModuleSymbol(moduleSymbol); + // // resolvedModuleSymbol may be a namespace. A namespace may be `export =` by multiple module declarations, but only keep the first one. + // if (!addToSeen(seenResolvedModules, getSymbolId(resolvedModuleSymbol))) { + // return; + // } + + // // 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, InternalSymbolName.ExportEquals, moduleSymbol, file, isFromPackageJson); + // } + + // for (const symbol of typeChecker.getExportsAndPropertiesOfModule(moduleSymbol)) { + // // If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion. + // if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) { + // continue; + // } + + // pushSymbol(symbol, symbol.name, moduleSymbol, file, isFromPackageJson); + // } + // }); + + // log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`); + // return flatten(arrayFrom(results.values())); + + // function pushSymbol(symbol: Symbol, exportName: string, moduleSymbol: Symbol, file: SourceFile | undefined, isFromPackageJson: boolean) { + // const isDefaultExport = symbol.escapedName === InternalSymbolName.Default; + // const nonLocalSymbol = symbol; + // if (isDefaultExport) { + // symbol = getLocalSymbolForExportDefault(symbol) || symbol; + // } + // if (typeChecker.isUndefinedSymbol(symbol)) { + // return; + // } + // const original = skipAlias(nonLocalSymbol, typeChecker); + // 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, + // symbolName, + // isDefaultExport, + // isFromPackageJson, + // exportName, + // fileName: file?.fileName + // }; + // results.add(getSymbolId(original), { + // symbol, + // symbolName, + // origin, + // }); + // } + // } + // } /** * Determines whether a module symbol is redundant with another for purposes of offering @@ -1864,11 +1844,11 @@ namespace ts.Completions { * will not be offered from different external modules, but they will be offered from * different ambient modules. */ - function moduleSymbolsAreDuplicateOrigins(a: Symbol, b: Symbol) { - const ambientNameA = pathIsBareSpecifier(stripQuotes(a.name)) ? a.name : undefined; - const ambientNameB = pathIsBareSpecifier(stripQuotes(b.name)) ? b.name : undefined; - return ambientNameA === ambientNameB; - } + // function moduleSymbolsAreDuplicateOrigins(a: Symbol, b: Symbol) { + // const ambientNameA = pathIsBareSpecifier(stripQuotes(a.name)) ? a.name : undefined; + // const ambientNameB = pathIsBareSpecifier(stripQuotes(b.name)) ? b.name : undefined; + // return ambientNameA === ambientNameB; + // } /** * True if you could remove some characters in `a` to get `b`. diff --git a/tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts b/tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts index 5d8b8ba3cf363..898658921361b 100644 --- a/tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts +++ b/tests/cases/fourslash/completionsImport_defaultAndNamedConflict.ts @@ -18,8 +18,8 @@ verify.completions({ name: "someModule", source: "/someModule", sourceDisplay: "./someModule", - text: "const someModule: 0", - kind: "const", + text: "(property) default: 1", + kind: "property", kindModifiers: "export", hasAction: true, sortText: completion.SortText.AutoImportSuggestions @@ -28,8 +28,8 @@ verify.completions({ name: "someModule", source: "/someModule", sourceDisplay: "./someModule", - text: "(property) default: 1", - kind: "property", + text: "const someModule: 0", + kind: "const", kindModifiers: "export", hasAction: true, sortText: completion.SortText.AutoImportSuggestions diff --git a/tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts b/tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts index 16dd9d7fb1dce..137276e0313c4 100644 --- a/tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts +++ b/tests/cases/fourslash/server/completionsImport_defaultAndNamedConflict_server.ts @@ -17,8 +17,8 @@ verify.completions({ name: "someModule", source: "/someModule", sourceDisplay: "./someModule", - text: "const someModule: 0", - kind: "const", + text: "(property) default: 1", + kind: "property", kindModifiers: "export", hasAction: true, sortText: completion.SortText.AutoImportSuggestions @@ -27,8 +27,8 @@ verify.completions({ name: "someModule", source: "/someModule", sourceDisplay: "./someModule", - text: "(property) default: 1", - kind: "property", + text: "const someModule: 0", + kind: "const", kindModifiers: "export", hasAction: true, sortText: completion.SortText.AutoImportSuggestions From 89dba0815cb3486ebbee3cf74bd8d3211b7154a3 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 4 Mar 2021 10:56:03 -0800 Subject: [PATCH 05/35] Fix export= --- src/services/codefixes/importFixes.ts | 35 ++++++++++++++------------- src/services/completions.ts | 2 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index bcd0fc054b7e3..e0a2d8695cc91 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -191,6 +191,7 @@ namespace ts.codefix { interface SymbolExportInfo { readonly symbol: Symbol; readonly moduleSymbol: Symbol; + readonly isExportEquals: boolean readonly importKind: ImportKind; /** If true, can't use an es6 import from a js file. */ readonly exportedSymbolIsTypeOnly: boolean; @@ -246,11 +247,11 @@ namespace ts.codefix { function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined { const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) { - return { symbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: defaultInfo.symbol, moduleSymbol, isExportEquals: defaultInfo.isExportEquals, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } const named = checker.tryGetMemberInModuleExportsAndProperties(symbol.name, moduleSymbol); if (named && skipAlias(named, checker) === symbol) { - return { symbol: named, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: named, moduleSymbol, isExportEquals: false, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } } } @@ -267,12 +268,12 @@ namespace ts.codefix { const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol) { - result.push({ symbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); + result.push({ symbol: defaultInfo.symbol, moduleSymbol, isExportEquals: defaultInfo.isExportEquals, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) { - result.push({ symbol: exported, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + result.push({ symbol: exported, moduleSymbol, isExportEquals: false, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); @@ -292,11 +293,11 @@ namespace ts.codefix { const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); if (defaultInfo) { const name = getNameForExportedSymbol(getLocalSymbolForExportDefault(defaultInfo.symbol) || defaultInfo.symbol, target); - result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), { symbol: defaultInfo.symbol, moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); + result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), { symbol: defaultInfo.symbol, moduleSymbol, isExportEquals: defaultInfo.isExportEquals, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported !== defaultInfo?.symbol) { - result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), { symbol: exported, moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), { symbol: exported, moduleSymbol, isExportEquals: false, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); @@ -530,7 +531,7 @@ namespace ts.codefix { if (!umdSymbol) return undefined; const symbol = checker.getAliasedSymbol(umdSymbol); const symbolName = umdSymbol.name; - const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false, isFromPackageJson: false }]; + const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, isExportEquals: false, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false, isFromPackageJson: false }]; const useRequire = shouldUseRequire(sourceFile, program); const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences); return { fixes, symbolName }; @@ -614,8 +615,8 @@ namespace ts.codefix { // For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once. // Maps symbol id to info for modules providing that symbol (original export + re-exports). const originalSymbolToExportInfos = createMultiMap(); - function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind, checker: TypeChecker, isFromPackageJson: boolean): void { - originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); + function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, isExportEquals: boolean, importKind: ImportKind, checker: TypeChecker, isFromPackageJson: boolean): void { + originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, isExportEquals, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); } forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _, program, isFromPackageJson) => { const checker = program.getTypeChecker(); @@ -624,13 +625,13 @@ namespace ts.codefix { const compilerOptions = program.getCompilerOptions(); const defaultInfo = getDefaultLikeExportInfo(sourceFile, moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) { - addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.kind, checker, isFromPackageJson); + addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.isExportEquals, defaultInfo.kind, checker, isFromPackageJson); } // check exports with the same name const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { - addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ImportKind.Named, checker, isFromPackageJson); + addSymbol(moduleSymbol, exportSymbolWithIdenticalName, /*isExportEquals*/ false, ImportKind.Named, checker, isFromPackageJson); } }); return originalSymbolToExportInfos; @@ -638,19 +639,19 @@ namespace ts.codefix { function getDefaultLikeExportInfo( importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, - ): { readonly symbol: Symbol, readonly symbolForMeaning: Symbol, readonly name: string, readonly kind: ImportKind } | undefined { + ): { readonly symbol: Symbol, readonly symbolForMeaning: Symbol, readonly name: string, readonly kind: ImportKind, isExportEquals: boolean } | undefined { const exported = getDefaultLikeExportWorker(importingFile, moduleSymbol, checker, compilerOptions); if (!exported) return undefined; - const { symbol, kind } = exported; + const { symbol, kind, isExportEquals } = exported; const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions); - return info && { symbol, kind, ...info }; + return info && { symbol, kind, isExportEquals, ...info }; } - function getDefaultLikeExportWorker(importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbol: Symbol, readonly kind: ImportKind } | undefined { + function getDefaultLikeExportWorker(importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbol: Symbol, readonly kind: ImportKind, isExportEquals: boolean } | undefined { const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol); - if (defaultExport) return { symbol: defaultExport, kind: ImportKind.Default }; + if (defaultExport) return { symbol: defaultExport, kind: ImportKind.Default, isExportEquals: false }; const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); - return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, kind: getExportEqualsImportKind(importingFile, compilerOptions) }; + return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, kind: getExportEqualsImportKind(importingFile, compilerOptions), isExportEquals: true }; } function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind { diff --git a/src/services/completions.ts b/src/services/completions.ts index 9a31c83ba823f..0c35a30dc4cb1 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1742,7 +1742,7 @@ namespace ts.Completions { kind: resolveModuleSpecifiers ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, moduleSpecifier, symbolName, - exportName: exportInfo.symbol.name, + exportName: exportInfo.isExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name, fileName: isAmbientModule ? undefined : cast(exportInfo.moduleSymbol.valueDeclaration, isSourceFile).fileName, isDefaultExport, moduleSymbol: exportInfo.moduleSymbol, From 3a4e275acf6291612a605bd87b88c33fa7b567b6 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 4 Mar 2021 11:24:14 -0800 Subject: [PATCH 06/35] Fix completion details for totally misspelled names --- src/services/completions.ts | 3 ++- ...etionsImport_details_withMisspelledName.ts | 23 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 0c35a30dc4cb1..7b1cde5b8c880 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1727,7 +1727,8 @@ namespace ts.Completions { const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*useAutoImportProvider*/ true); exportInfo.forEach((info, key) => { const [symbolName] = key.split("|"); - if (stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { + const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); + if (isCompletionDetailsMatch || stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { // If we don't need to resolve module specifiers, it doesn't matter which SymbolExportInfo // we use. Each is importable by the same name and resolves to the same declaration. const { moduleSpecifier, exportInfo } = resolveModuleSpecifiers diff --git a/tests/cases/fourslash/completionsImport_details_withMisspelledName.ts b/tests/cases/fourslash/completionsImport_details_withMisspelledName.ts index 098f2e5d46e3d..5f5095301a524 100644 --- a/tests/cases/fourslash/completionsImport_details_withMisspelledName.ts +++ b/tests/cases/fourslash/completionsImport_details_withMisspelledName.ts @@ -4,12 +4,29 @@ ////export const abc = 0; // @Filename: /b.ts -////acb/**/; +////acb/*1*/; -goTo.marker(""); -verify.applyCodeActionFromCompletion("", { +// @Filename: /c.ts +////acb/*2*/; + +goTo.marker("1"); +verify.applyCodeActionFromCompletion("1", { + name: "abc", + source: "/a", + description: `Import 'abc' from module "./a"`, + newFileContent: `import { abc } from "./a"; + +acb;`, +}); + +goTo.marker("2"); +verify.applyCodeActionFromCompletion("2", { name: "abc", source: "/a", + data: { + exportName: "abc", + fileName: "/a.ts", + }, description: `Import 'abc' from module "./a"`, newFileContent: `import { abc } from "./a"; From 12bb1fe7ce406149f7450f1937024a038004901a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 4 Mar 2021 17:03:27 -0800 Subject: [PATCH 07/35] Almost fixed duplication... --- src/services/codefixes/importFixes.ts | 3 +- src/services/completions.ts | 137 +++++++++++------- .../unittests/tsserver/completions.ts | 2 +- 3 files changed, 84 insertions(+), 58 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index e0a2d8695cc91..3da902f82c7da 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -295,8 +295,9 @@ namespace ts.codefix { const name = getNameForExportedSymbol(getLocalSymbolForExportDefault(defaultInfo.symbol) || defaultInfo.symbol, target); result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), { symbol: defaultInfo.symbol, moduleSymbol, isExportEquals: defaultInfo.isExportEquals, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } + const seenExports = new Map(); for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { - if (exported !== defaultInfo?.symbol) { + if (exported !== defaultInfo?.symbol && addToSeen(seenExports, exported)) { result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), { symbol: exported, moduleSymbol, isExportEquals: false, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } diff --git a/src/services/completions.ts b/src/services/completions.ts index 7b1cde5b8c880..5de8dc790082c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -42,16 +42,20 @@ namespace ts.Completions { interface SymbolOriginInfo { kind: SymbolOriginInfoKind; + symbolName?: string; + moduleSymbol?: Symbol; + isDefaultExport?: boolean; + isFromPackageJson?: boolean; + exportName?: string; + fileName?: string; + moduleSpecifier?: string; } interface SymbolOriginInfoExport extends SymbolOriginInfo { - kind: SymbolOriginInfoKind; symbolName: string; moduleSymbol: Symbol; isDefaultExport: boolean; - isFromPackageJson?: boolean; exportName: string; - fileName?: string; } interface SymbolOriginInfoResolvedExport extends SymbolOriginInfoExport { @@ -99,7 +103,7 @@ namespace ts.Completions { * Map from symbol id -> SymbolOriginInfo. * Only populated for symbols that come from other modules. */ - type SymbolOriginInfoMap = Record; + type SymbolOriginInfoMap = Record; type SymbolSortTextMap = (SortText | undefined)[]; @@ -580,40 +584,42 @@ namespace ts.Completions { // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. const uniques = new Map(); for (const symbol of symbols) { - const origin = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; - const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected); - if (!info) { - continue; - } - const { name, needsConvertPropertyAccess } = info; - if (uniques.get(name)) { - continue; - } + const origins = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; + for (const origin of toArray(origins)) { + const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected); + if (!info) { + continue; + } + const { name, needsConvertPropertyAccess } = info; + if (uniques.get(name)) { + continue; + } - const entry = createCompletionEntry( - symbol, - symbolToSortTextMap && symbolToSortTextMap[getSymbolId(symbol)] || SortText.LocationPriority, - contextToken, - location, - sourceFile, - typeChecker, - name, - needsConvertPropertyAccess, - origin, - recommendedCompletion, - propertyAccessToConvert, - isJsxInitializer, - preferences - ); - if (!entry) { - continue; - } + const entry = createCompletionEntry( + symbol, + symbolToSortTextMap && symbolToSortTextMap[getSymbolId(symbol)] || SortText.LocationPriority, + contextToken, + location, + sourceFile, + typeChecker, + name, + needsConvertPropertyAccess, + origin, + recommendedCompletion, + propertyAccessToConvert, + isJsxInitializer, + preferences + ); + if (!entry) { + continue; + } - /** True for locals; false for globals, module exports from other files, `this.` completions. */ - const shouldShadowLaterSymbols = !origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile())); - uniques.set(name, shouldShadowLaterSymbols); + /** True for locals; false for globals, module exports from other files, `this.` completions. */ + const shouldShadowLaterSymbols = !origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile())); + uniques.set(name, shouldShadowLaterSymbols); - entries.push(entry); + entries.push(entry); + } } log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start)); @@ -664,7 +670,7 @@ namespace ts.Completions { type: "symbol"; symbol: Symbol; location: Node | undefined; - symbolToOriginInfoMap: SymbolOriginInfoMap; + origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined; previousToken: Node | undefined; readonly isJsxInitializer: IsJsxInitializer; readonly isTypeOnlyLocation: boolean; @@ -688,9 +694,7 @@ namespace ts.Completions { previousToken: findPrecedingToken(position, sourceFile, /*startNode*/ undefined)!, isJsxInitializer: false, isTypeOnlyLocation: false, - symbolToOriginInfoMap: { - [getSymbolId(autoImport.symbol)]: autoImport.origin - } + origin: autoImport.origin, }; } } @@ -714,11 +718,13 @@ namespace ts.Completions { // name against 'entryName' (which is known to be good), not building a new // completion entry. return firstDefined(symbols, (symbol): SymbolCompletion | undefined => { - const origin = symbolToOriginInfoMap[getSymbolId(symbol)]; - const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind, completionData.isJsxIdentifierExpected); - return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source - ? { type: "symbol" as const, symbol, location, symbolToOriginInfoMap, previousToken, isJsxInitializer, isTypeOnlyLocation } - : undefined; + const origins = symbolToOriginInfoMap[getSymbolId(symbol)]; + return firstDefined(toArray(origins), origin => { + const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind, completionData.isJsxIdentifierExpected); + return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source + ? { type: "symbol" as const, symbol, location, origin, previousToken, isJsxInitializer, isTypeOnlyLocation } + : undefined; + }); }) || { type: "none" }; } @@ -765,8 +771,8 @@ namespace ts.Completions { } } case "symbol": { - const { symbol, location, symbolToOriginInfoMap, previousToken } = symbolCompletion; - const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(symbolToOriginInfoMap, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, entryId.data); + const { symbol, location, origin, previousToken } = symbolCompletion; + const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(origin, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, entryId.data); return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } case "literal": { @@ -802,7 +808,7 @@ namespace ts.Completions { readonly sourceDisplay: SymbolDisplayPart[] | undefined; } function getCompletionEntryCodeActionsAndSourceDisplay( - symbolToOriginInfoMap: SymbolOriginInfoMap, + origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined, symbol: Symbol, program: Program, checker: TypeChecker, @@ -819,12 +825,11 @@ namespace ts.Completions { return { codeActions: undefined, sourceDisplay: [textPart(data.moduleSpecifier)] }; } - const symbolOriginInfo = symbolToOriginInfoMap[getSymbolId(symbol)]; - if (!symbolOriginInfo || !originIsExport(symbolOriginInfo)) { + if (!origin || !originIsExport(origin)) { return { codeActions: undefined, sourceDisplay: undefined }; } - const { moduleSymbol } = symbolOriginInfo; + const { moduleSymbol } = origin; const exportedSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker)); const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction( exportedSymbol, @@ -1729,6 +1734,10 @@ namespace ts.Completions { const [symbolName] = key.split("|"); const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); if (isCompletionDetailsMatch || stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { + // if (some(info, i => i.isExportEquals && !every(i.symbol.declarations, isNonGlobalDeclaration))) { + // return; + // } + // If we don't need to resolve module specifiers, it doesn't matter which SymbolExportInfo // we use. Each is importable by the same name and resolves to the same declaration. const { moduleSpecifier, exportInfo } = resolveModuleSpecifiers @@ -1737,9 +1746,8 @@ namespace ts.Completions { if (!exportInfo) return; const isDefaultExport = exportInfo.symbol.name === InternalSymbolName.Default; const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; - const symbolId = getSymbolId(symbol); const isAmbientModule = !isExternalModuleNameRelative(stripQuotes(exportInfo.moduleSymbol.name)); - const origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport = { + pushAutoImportSymbol(symbol, { kind: resolveModuleSpecifiers ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, moduleSpecifier, symbolName, @@ -1748,14 +1756,31 @@ namespace ts.Completions { isDefaultExport, moduleSymbol: exportInfo.moduleSymbol, isFromPackageJson: exportInfo.isFromPackageJson, - }; - symbols.push(symbol); - symbolToOriginInfoMap[symbolId] = origin; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + }); } }); } + function pushAutoImportSymbol(symbol: Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { + const symbolId = getSymbolId(symbol); + if (symbolToSortTextMap[symbolId] === SortText.GlobalsOrKeywords) { + // If an auto-importable symbol is available as a global, don't add the auto import + return; + } + const existingOrigin = symbolToOriginInfoMap[symbolId]; + if (existingOrigin === undefined) { + symbolToOriginInfoMap[symbolId] = origin; + symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + symbols.push(symbol); + } + else if (isArray(existingOrigin)) { + existingOrigin.push(origin); + } + else { + symbolToOriginInfoMap[symbolId] = [existingOrigin, origin]; + } + } + /** * Gathers symbols that can be imported from other files, de-duplicating along the way. Symbols can be "duplicates" * if re-exported from another module by the same name, e.g. `export { foo } from "./a"`. diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts index ccf4bc934611d..490add4a695ba 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -39,7 +39,7 @@ namespace ts.projectSystem { isPackageJsonImport: undefined, sortText: Completions.SortText.AutoImportSuggestions, source: "/a", - data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined } + data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined, moduleSpecifier: undefined } }; assert.deepEqual(response, { isGlobalCompletion: true, From 0ae905cd42c028a81a599cc60d80e6bd225502d4 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 5 Mar 2021 12:12:27 -0800 Subject: [PATCH 08/35] Fix remaining completion tests --- src/services/completions.ts | 5 +---- .../unittests/tsserver/completions.ts | 1 + .../completionsImport_exportEquals_global.ts | 22 ++++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 5de8dc790082c..53213c4ed31d5 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1732,12 +1732,9 @@ namespace ts.Completions { const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*useAutoImportProvider*/ true); exportInfo.forEach((info, key) => { const [symbolName] = key.split("|"); + if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); if (isCompletionDetailsMatch || stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { - // if (some(info, i => i.isExportEquals && !every(i.symbol.declarations, isNonGlobalDeclaration))) { - // return; - // } - // If we don't need to resolve module specifiers, it doesn't matter which SymbolExportInfo // we use. Each is importable by the same name and resolves to the same declaration. const { moduleSpecifier, exportInfo } = resolveModuleSpecifiers diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts index 490add4a695ba..651e79576993c 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -69,6 +69,7 @@ namespace ts.projectSystem { kindModifiers: ScriptElementKindModifier.exportedModifier, name: "foo", source: [{ text: "./a", kind: "text" }], + sourceDisplay: [{ text: "./a", kind: "text" }], tags: undefined, }; assert.deepEqual(detailsResponse, [ diff --git a/tests/cases/fourslash/completionsImport_exportEquals_global.ts b/tests/cases/fourslash/completionsImport_exportEquals_global.ts index 7e11b169579fa..27d26c387b735 100644 --- a/tests/cases/fourslash/completionsImport_exportEquals_global.ts +++ b/tests/cases/fourslash/completionsImport_exportEquals_global.ts @@ -3,26 +3,28 @@ // @module: es6 // @Filename: /console.d.ts -////interface Console {} -////declare var console: Console; -////declare module "console" { -//// export = console; -////} +//// interface Console {} +//// declare var console: Console; +//// declare module "console" { +//// export = console; +//// } // @Filename: /react-native.d.ts //// import 'console'; -////declare global { -//// interface Console {} -//// var console: Console; -////} +//// declare global { +//// interface Console {} +//// var console: Console; +//// } // @Filename: /a.ts ////conso/**/ verify.completions({ + marker: "", exact: completion.globalsPlus([{ hasAction: undefined, // asserts that it does *not* have an action - name: "console" + name: "console", + sortText: completion.SortText.GlobalsOrKeywords, }]), preferences: { includeCompletionsForModuleExports: true, From e9f3ec1b6ced56d06dbb6d788b3e8bf52343a68c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 5 Mar 2021 13:55:39 -0800 Subject: [PATCH 09/35] Refactor to support multiple origins for same symbol --- src/services/completions.ts | 297 ++++++++++++++---------------- src/services/stringCompletions.ts | 7 +- 2 files changed, 146 insertions(+), 158 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 53213c4ed31d5..8b6eef3d4156b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -100,11 +100,12 @@ namespace ts.Completions { } /** - * Map from symbol id -> SymbolOriginInfo. + * Map from symbol index in `symbols` -> SymbolOriginInfo. * Only populated for symbols that come from other modules. */ - type SymbolOriginInfoMap = Record; + type SymbolOriginInfoMap = Record; + /** Map from symbol id -> SortText. */ type SymbolSortTextMap = (SortText | undefined)[]; const enum KeywordCompletionFilters { @@ -278,6 +279,8 @@ namespace ts.Completions { log, completionKind, preferences, + compilerOptions, + completionData.isTypeOnlyLocation, propertyAccessToConvert, completionData.isJsxIdentifierExpected, isJsxInitializer, @@ -285,7 +288,7 @@ namespace ts.Completions { symbolToOriginInfoMap, symbolToSortTextMap ); - getJSCompletionEntries(sourceFile, location!.pos, uniqueNames, compilerOptions.target!, entries); // TODO: GH#18217 + getJSCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target!, entries); // TODO: GH#18217 } else { if (!isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) { @@ -303,6 +306,8 @@ namespace ts.Completions { log, completionKind, preferences, + compilerOptions, + completionData.isTypeOnlyLocation, propertyAccessToConvert, completionData.isJsxIdentifierExpected, isJsxInitializer, @@ -433,7 +438,7 @@ namespace ts.Completions { symbol: Symbol, sortText: SortText, contextToken: Node | undefined, - location: Node | undefined, + location: Node, sourceFile: SourceFile, typeChecker: TypeChecker, name: string, @@ -521,7 +526,7 @@ namespace ts.Completions { // entries (like JavaScript identifier entries). return { name, - kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location!), // TODO: GH#18217 + kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location), // TODO: GH#18217 kindModifiers: SymbolDisplay.getSymbolModifiers(typeChecker, symbol), sortText, source: getSourceFromOrigin(origin), @@ -563,13 +568,15 @@ namespace ts.Completions { symbols: readonly Symbol[], entries: Push, contextToken: Node | undefined, - location: Node | undefined, + location: Node, sourceFile: SourceFile, typeChecker: TypeChecker, target: ScriptTarget, log: Log, kind: CompletionKind, preferences: UserPreferences, + compilerOptions: CompilerOptions, + isTypeOnlyLocation?: boolean, propertyAccessToConvert?: PropertyAccessExpression, jsxIdentifierExpected?: boolean, isJsxInitializer?: IsJsxInitializer, @@ -578,48 +585,44 @@ namespace ts.Completions { symbolToSortTextMap?: SymbolSortTextMap, ): UniqueNameSet { const start = timestamp(); + const variableDeclaration = getVariableDeclaration(location); // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. // So adding a completion for a local will prevent us from adding completions for external module exports sharing the same name. const uniques = new Map(); - for (const symbol of symbols) { - const origins = symbolToOriginInfoMap ? symbolToOriginInfoMap[getSymbolId(symbol)] : undefined; - for (const origin of toArray(origins)) { - const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected); - if (!info) { - continue; - } - const { name, needsConvertPropertyAccess } = info; - if (uniques.get(name)) { - continue; - } - - const entry = createCompletionEntry( - symbol, - symbolToSortTextMap && symbolToSortTextMap[getSymbolId(symbol)] || SortText.LocationPriority, - contextToken, - location, - sourceFile, - typeChecker, - name, - needsConvertPropertyAccess, - origin, - recommendedCompletion, - propertyAccessToConvert, - isJsxInitializer, - preferences - ); - if (!entry) { - continue; - } - - /** True for locals; false for globals, module exports from other files, `this.` completions. */ - const shouldShadowLaterSymbols = !origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location!.getSourceFile())); - uniques.set(name, shouldShadowLaterSymbols); - - entries.push(entry); + for (let i = 0; i < symbols.length; i++) { + const symbol = symbols[i]; + const origin = symbolToOriginInfoMap?.[i]; + const info = getCompletionEntryDisplayNameForSymbol(symbol, target, origin, kind, !!jsxIdentifierExpected); + if (!info || uniques.get(info.name) || kind === CompletionKind.Global && symbolToSortTextMap && !shouldIncludeSymbol(symbol, symbolToSortTextMap)) { + continue; + } + + const { name, needsConvertPropertyAccess } = info; + const entry = createCompletionEntry( + symbol, + symbolToSortTextMap && symbolToSortTextMap[getSymbolId(symbol)] || SortText.LocationPriority, + contextToken, + location, + sourceFile, + typeChecker, + name, + needsConvertPropertyAccess, + origin, + recommendedCompletion, + propertyAccessToConvert, + isJsxInitializer, + preferences + ); + if (!entry) { + continue; } + + /** True for locals; false for globals, module exports from other files, `this.` completions. */ + const shouldShadowLaterSymbols = !origin && !(symbol.parent === undefined && !some(symbol.declarations, d => d.getSourceFile() === location.getSourceFile())); + uniques.set(name, shouldShadowLaterSymbols); + entries.push(entry); } log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start)); @@ -631,8 +634,54 @@ namespace ts.Completions { has: name => uniques.has(name), add: name => uniques.set(name, true), }; + + function shouldIncludeSymbol(symbol: Symbol, symbolToSortTextMap: SymbolSortTextMap): boolean { + if (!isSourceFile(location)) { + // export = /**/ here we want to get all meanings, so any symbol is ok + if (isExportAssignment(location.parent)) { + return true; + } + // Filter out variables from their own initializers + // `const a = /* no 'a' here */` + if (variableDeclaration && symbol.valueDeclaration === variableDeclaration) { + return false; + } + + // External modules can have global export declarations that will be + // available as global keywords in all scopes. But if the external module + // already has an explicit export and user only wants to user explicit + // module imports then the global keywords will be filtered out so auto + // import suggestions will win in the completion + const symbolOrigin = skipAlias(symbol, typeChecker); + // We only want to filter out the global keywords + // Auto Imports are not available for scripts so this conditional is always false + if (!!sourceFile.externalModuleIndicator + && !compilerOptions.allowUmdGlobalAccess + && symbolToSortTextMap[getSymbolId(symbol)] === SortText.GlobalsOrKeywords + && symbolToSortTextMap[getSymbolId(symbolOrigin)] === SortText.AutoImportSuggestions) { + return false; + } + // Continue with origin symbol + symbol = symbolOrigin; + + // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) + if (isInRightSideOfInternalImportEqualsDeclaration(location)) { + return !!(symbol.flags & SymbolFlags.Namespace); + } + + if (isTypeOnlyLocation) { + // It's a type, but you can reach it by namespace.type as well + return symbolCanBeReferencedAtTypeLocation(symbol, typeChecker); + } + } + + // expressions are value space (which includes the value namespaces) + return !!(getCombinedLocalAndExportSymbolFlags(symbol) & SymbolFlags.Value); + } } + + function getLabelCompletionAtPosition(node: BreakOrContinueStatement): CompletionInfo | undefined { const entries = getLabelStatementCompletions(node); if (entries.length) { @@ -669,7 +718,7 @@ namespace ts.Completions { interface SymbolCompletion { type: "symbol"; symbol: Symbol; - location: Node | undefined; + location: Node; origin: SymbolOriginInfo | SymbolOriginInfoExport | SymbolOriginInfoResolvedExport | undefined; previousToken: Node | undefined; readonly isJsxInitializer: IsJsxInitializer; @@ -717,14 +766,12 @@ namespace ts.Completions { // We don't need to perform character checks here because we're only comparing the // name against 'entryName' (which is known to be good), not building a new // completion entry. - return firstDefined(symbols, (symbol): SymbolCompletion | undefined => { - const origins = symbolToOriginInfoMap[getSymbolId(symbol)]; - return firstDefined(toArray(origins), origin => { - const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind, completionData.isJsxIdentifierExpected); - return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source - ? { type: "symbol" as const, symbol, location, origin, previousToken, isJsxInitializer, isTypeOnlyLocation } - : undefined; - }); + return firstDefined(symbols, (symbol, index): SymbolCompletion | undefined => { + const origin = symbolToOriginInfoMap[index]; + const info = getCompletionEntryDisplayNameForSymbol(symbol, compilerOptions.target!, origin, completionKind, completionData.isJsxIdentifierExpected); + return info && info.name === entryId.name && getSourceFromOrigin(origin) === entryId.source + ? { type: "symbol" as const, symbol, location, origin, previousToken, isJsxInitializer, isTypeOnlyLocation } + : undefined; }) || { type: "none" }; } @@ -773,7 +820,7 @@ namespace ts.Completions { case "symbol": { const { symbol, location, origin, previousToken } = symbolCompletion; const { codeActions, sourceDisplay } = getCompletionEntryCodeActionsAndSourceDisplay(origin, symbol, program, typeChecker, host, compilerOptions, sourceFile, position, previousToken, formatContext, preferences, entryId.data); - return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location!, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 + return createCompletionDetailsForSymbol(symbol, typeChecker, sourceFile, location, cancellationToken, codeActions, sourceDisplay); // TODO: GH#18217 } case "literal": { const { literal } = symbolCompletion; @@ -868,7 +915,7 @@ namespace ts.Completions { /** Note that the presence of this alone doesn't mean that we need a conversion. Only do that if the completion is not an ordinary identifier. */ readonly propertyAccessToConvert: PropertyAccessExpression | undefined; readonly isNewIdentifierLocation: boolean; - readonly location: Node | undefined; + readonly location: Node; readonly keywordFilters: KeywordCompletionFilters; readonly literals: readonly (string | number | PseudoBigInt)[]; readonly symbolToOriginInfoMap: SymbolOriginInfoMap; @@ -958,7 +1005,6 @@ namespace ts.Completions { host: LanguageServiceHost ): CompletionData | Request | undefined { const typeChecker = program.getTypeChecker(); - const compilerOptions = program.getCompilerOptions(); let start = timestamp(); let currentToken = getTokenAtPosition(sourceFile, position); // TODO: GH#15853 @@ -1198,6 +1244,7 @@ namespace ts.Completions { let symbols: Symbol[] = []; const symbolToOriginInfoMap: SymbolOriginInfoMap = []; const symbolToSortTextMap: SymbolSortTextMap = []; + const seenPropertySymbols = new Map(); // const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache(); const isTypeOnly = isTypeOnlyCompletion(); @@ -1209,7 +1256,7 @@ namespace ts.Completions { Debug.assertEachIsDefined(tagSymbols, "getJsxIntrinsicTagNames() should all be defined"); tryGetGlobalSymbols(); symbols = tagSymbols.concat(symbols); - completionKind = CompletionKind.MemberLike; + completionKind = CompletionKind.Global; keywordFilters = KeywordCompletionFilters.None; } else if (isStartingCloseTag) { @@ -1218,7 +1265,7 @@ namespace ts.Completions { if (tagSymbol) { symbols = [tagSymbol]; } - completionKind = CompletionKind.MemberLike; + completionKind = CompletionKind.Global; keywordFilters = KeywordCompletionFilters.None; } else { @@ -1292,7 +1339,7 @@ namespace ts.Completions { const exportedSymbols = typeChecker.getExportsOfModule(symbol); Debug.assertEachIsDefined(exportedSymbols, "getExportsOfModule() should all be defined"); const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess(isImportType ? node : (node.parent), symbol.name); - const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); + const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol, typeChecker); const isValidAccess: (symbol: Symbol) => boolean = isNamespaceName // At `namespace N.M/**/`, if this is the only declaration of `M`, don't include `M` as a completion. @@ -1405,11 +1452,12 @@ namespace ts.Completions { const nameSymbol = leftMostName && typeChecker.getSymbolAtLocation(leftMostName); // If this is nested like for `namespace N { export const sym = Symbol(); }`, we'll add the completion for `N`. const firstAccessibleSymbol = nameSymbol && getFirstSymbolInChain(nameSymbol, contextToken, typeChecker); - if (firstAccessibleSymbol && !symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)]) { + if (firstAccessibleSymbol && addToSeen(seenPropertySymbols, getSymbolId(firstAccessibleSymbol))) { + const index = symbols.length; symbols.push(firstAccessibleSymbol); const moduleSymbol = firstAccessibleSymbol.parent; if (!moduleSymbol || !isExternalModuleSymbol(moduleSymbol)) { - symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }; + symbolToOriginInfoMap[index] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }; } else { const origin: SymbolOriginInfoExport = { @@ -1420,7 +1468,7 @@ namespace ts.Completions { exportName: firstAccessibleSymbol.name, fileName: isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? cast(moduleSymbol.declarations![0], isSourceFile).fileName : undefined, }; - symbolToOriginInfoMap[getSymbolId(firstAccessibleSymbol)] = origin; + symbolToOriginInfoMap[index] = origin; } } else if (preferences.includeCompletionsWithInsertText) { @@ -1443,11 +1491,11 @@ namespace ts.Completions { function addSymbolOriginInfo(symbol: Symbol) { if (preferences.includeCompletionsWithInsertText) { - if (insertAwait && !symbolToOriginInfoMap[getSymbolId(symbol)]) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) }; + if (insertAwait && addToSeen(seenPropertySymbols, getSymbolId(symbol))) { + symbolToOriginInfoMap[symbols.length] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.Promise) }; } else if (insertQuestionDot) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.Nullable }; + symbolToOriginInfoMap[symbols.length] = { kind: SymbolOriginInfoKind.Nullable }; } } } @@ -1563,7 +1611,7 @@ namespace ts.Completions { const thisType = typeChecker.tryGetThisTypeAt(scopeNode, /*includeGlobalThis*/ false); if (thisType && !isProbablyGlobalType(thisType, sourceFile, typeChecker)) { for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) { - symbolToOriginInfoMap[getSymbolId(symbol)] = { kind: SymbolOriginInfoKind.ThisType }; + symbolToOriginInfoMap[symbols.length] = { kind: SymbolOriginInfoKind.ThisType }; symbols.push(symbol); symbolToSortTextMap[getSymbolId(symbol)] = SortText.SuggestedClassMembers; } @@ -1578,7 +1626,11 @@ namespace ts.Completions { const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; collectAutoImports(lowerCaseTokenText, resolveModuleSpecifier); } - filterGlobalCompletion(symbols); + if (isTypeOnly) { + keywordFilters = contextToken && isAssertionExpression(contextToken.parent) + ? KeywordCompletionFilters.TypeAssertionKeywords + : KeywordCompletionFilters.TypeKeywords; + } } function shouldOfferImportCompletions(): boolean { @@ -1606,75 +1658,6 @@ namespace ts.Completions { } } - function filterGlobalCompletion(symbols: Symbol[]): void { - const isTypeOnly = isTypeOnlyCompletion(); - if (isTypeOnly) { - keywordFilters = contextToken && isAssertionExpression(contextToken.parent) - ? KeywordCompletionFilters.TypeAssertionKeywords - : KeywordCompletionFilters.TypeKeywords; - } - - const variableDeclaration = getVariableDeclaration(location); - - filterMutate(symbols, symbol => { - if (!isSourceFile(location)) { - // export = /**/ here we want to get all meanings, so any symbol is ok - if (isExportAssignment(location.parent)) { - return true; - } - - // Filter out variables from their own initializers - // `const a = /* no 'a' here */` - if (variableDeclaration && symbol.valueDeclaration === variableDeclaration) { - return false; - } - - // External modules can have global export declarations that will be - // available as global keywords in all scopes. But if the external module - // already has an explicit export and user only wants to user explicit - // module imports then the global keywords will be filtered out so auto - // import suggestions will win in the completion - const symbolOrigin = skipAlias(symbol, typeChecker); - // We only want to filter out the global keywords - // Auto Imports are not available for scripts so this conditional is always false - if (!!sourceFile.externalModuleIndicator - && !compilerOptions.allowUmdGlobalAccess - && symbolToSortTextMap[getSymbolId(symbol)] === SortText.GlobalsOrKeywords - && symbolToSortTextMap[getSymbolId(symbolOrigin)] === SortText.AutoImportSuggestions) { - return false; - } - // Continue with origin symbol - symbol = symbolOrigin; - - // import m = /**/ <-- It can only access namespace (if typing import = x. this would get member symbols and not namespace) - if (isInRightSideOfInternalImportEqualsDeclaration(location)) { - return !!(symbol.flags & SymbolFlags.Namespace); - } - - if (isTypeOnly) { - // It's a type, but you can reach it by namespace.type as well - return symbolCanBeReferencedAtTypeLocation(symbol); - } - } - - // expressions are value space (which includes the value namespaces) - return !!(getCombinedLocalAndExportSymbolFlags(symbol) & SymbolFlags.Value); - }); - } - - function getVariableDeclaration(property: Node): VariableDeclaration | undefined { - const variableDeclaration = findAncestor(property, node => - isFunctionBlock(node) || isArrowFunctionBody(node) || isBindingPattern(node) - ? "quit" - : isVariableDeclaration(node)); - - return variableDeclaration as VariableDeclaration | undefined; - } - - function isArrowFunctionBody(node: Node) { - return node.parent && isArrowFunction(node.parent) && node.parent.body === node; - }; - function isTypeOnlyCompletion(): boolean { return insideJsDocTagTypeExpression || !isContextTokenValueLocation(contextToken) && @@ -1717,15 +1700,6 @@ namespace ts.Completions { return false; } - /** True if symbol is a type or a module containing at least one type. */ - function symbolCanBeReferencedAtTypeLocation(symbol: Symbol, seenModules = new Map()): boolean { - const sym = skipAlias(symbol.exportSymbol || symbol, typeChecker); - return !!(sym.flags & SymbolFlags.Type) || - !!(sym.flags & SymbolFlags.Module) && - addToSeen(seenModules, getSymbolId(sym)) && - typeChecker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, seenModules)); - } - /** Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` */ function collectAutoImports(lowerCaseTokenText: string, resolveModuleSpecifiers: boolean) { Debug.assert(!detailsEntryId?.data); @@ -1764,18 +1738,9 @@ namespace ts.Completions { // If an auto-importable symbol is available as a global, don't add the auto import return; } - const existingOrigin = symbolToOriginInfoMap[symbolId]; - if (existingOrigin === undefined) { - symbolToOriginInfoMap[symbolId] = origin; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; - symbols.push(symbol); - } - else if (isArray(existingOrigin)) { - existingOrigin.push(origin); - } - else { - symbolToOriginInfoMap[symbolId] = [existingOrigin, origin]; - } + symbolToOriginInfoMap[symbols.length] = origin; + symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + symbols.push(symbol); } /** @@ -3032,4 +2997,26 @@ namespace ts.Completions { } return undefined; } + + function getVariableDeclaration(property: Node): VariableDeclaration | undefined { + const variableDeclaration = findAncestor(property, node => + isFunctionBlock(node) || isArrowFunctionBody(node) || isBindingPattern(node) + ? "quit" + : isVariableDeclaration(node)); + + return variableDeclaration as VariableDeclaration | undefined; + } + + function isArrowFunctionBody(node: Node) { + return node.parent && isArrowFunction(node.parent) && node.parent.body === node; + }; + + /** True if symbol is a type or a module containing at least one type. */ + function symbolCanBeReferencedAtTypeLocation(symbol: Symbol, checker: TypeChecker, seenModules = new Map()): boolean { + const sym = skipAlias(symbol.exportSymbol || symbol, checker); + return !!(sym.flags & SymbolFlags.Type) || + !!(sym.flags & SymbolFlags.Module) && + addToSeen(seenModules, getSymbolId(sym)) && + checker.getExportsOfModule(sym).some(e => symbolCanBeReferencedAtTypeLocation(e, checker, seenModules)); + } } diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index de823ad951d0e..64f3814d22dc1 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -8,11 +8,11 @@ namespace ts.Completions.StringCompletions { if (isInString(sourceFile, position, contextToken)) { if (!contextToken || !isStringLiteralLike(contextToken)) return undefined; const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host); - return convertStringLiteralCompletions(entries, contextToken, sourceFile, checker, log, preferences); + return convertStringLiteralCompletions(entries, contextToken, sourceFile, checker, log, options, preferences); } } - function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined { + function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, checker: TypeChecker, log: Log, options: CompilerOptions, preferences: UserPreferences): CompletionInfo | undefined { if (completion === undefined) { return undefined; } @@ -33,7 +33,8 @@ namespace ts.Completions.StringCompletions { ScriptTarget.ESNext, log, CompletionKind.String, - preferences + preferences, + options, ); // Target will not be used, so arbitrary return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries }; } From b5b58f8a6a44395d143decc70ac1e1d70009a114 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 5 Mar 2021 15:43:34 -0800 Subject: [PATCH 10/35] Make import fixes make slightly more sense --- src/services/codefixes/importFixes.ts | 82 ++++++++++-------- src/services/completions.ts | 115 ++------------------------ 2 files changed, 56 insertions(+), 141 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 3da902f82c7da..26c8165c0697d 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -187,12 +187,18 @@ namespace ts.codefix { CommonJS, } + export const enum ExportKind { + Named, + Default, + ExportEquals, + UMD, + } + /** Information about how a symbol is exported from a module. */ - interface SymbolExportInfo { + export interface SymbolExportInfo { readonly symbol: Symbol; readonly moduleSymbol: Symbol; - readonly isExportEquals: boolean - readonly importKind: ImportKind; + readonly exportKind: ExportKind; /** If true, can't use an es6 import from a js file. */ readonly exportedSymbolIsTypeOnly: boolean; /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ @@ -218,7 +224,7 @@ namespace ts.codefix { ): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } { const compilerOptions = program.getCompilerOptions(); const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name)) - ? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, sourceFile, program, host)] + ? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, program, host)] : getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, /*useAutoImportProvider*/ true); const useRequire = shouldUseRequire(sourceFile, program); const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position)); @@ -235,7 +241,7 @@ namespace ts.codefix { return { description, changes, commands }; } - function getSymbolExportInfoForSymbol(symbol: Symbol, moduleSymbol: Symbol, importingFile: SourceFile, program: Program, host: LanguageServiceHost): SymbolExportInfo { + function getSymbolExportInfoForSymbol(symbol: Symbol, moduleSymbol: Symbol, program: Program, host: LanguageServiceHost): SymbolExportInfo { const compilerOptions = program.getCompilerOptions(); const mainProgramInfo = getInfoWithChecker(program.getTypeChecker(), /*isFromPackageJson*/ false); if (mainProgramInfo) { @@ -245,13 +251,13 @@ namespace ts.codefix { return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider, /*isFromPackageJson*/ true), `Could not find symbol in specified module for code actions`); function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined { - const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) { - return { symbol: defaultInfo.symbol, moduleSymbol, isExportEquals: defaultInfo.isExportEquals, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } const named = checker.tryGetMemberInModuleExportsAndProperties(symbol.name, moduleSymbol); if (named && skipAlias(named, checker) === symbol) { - return { symbol: named, moduleSymbol, isExportEquals: false, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: named, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } } } @@ -266,14 +272,14 @@ namespace ts.codefix { return; } - const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol) { - result.push({ symbol: defaultInfo.symbol, moduleSymbol, isExportEquals: defaultInfo.isExportEquals, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); + result.push({ symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) { - result.push({ symbol: exported, moduleSymbol, isExportEquals: false, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + result.push({ symbol: exported, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); @@ -290,15 +296,15 @@ namespace ts.codefix { const target = getEmitScriptTarget(compilerOptions); forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); - const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions); + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo) { const name = getNameForExportedSymbol(getLocalSymbolForExportDefault(defaultInfo.symbol) || defaultInfo.symbol, target); - result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), { symbol: defaultInfo.symbol, moduleSymbol, isExportEquals: defaultInfo.isExportEquals, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); + result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), { symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } const seenExports = new Map(); for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported !== defaultInfo?.symbol && addToSeen(seenExports, exported)) { - result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), { symbol: exported, moduleSymbol, isExportEquals: false, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), { symbol: exported, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); @@ -332,7 +338,7 @@ namespace ts.codefix { preferences: UserPreferences, ): readonly ImportFix[] { const checker = program.getTypeChecker(); - const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile)); + const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile, program.getCompilerOptions())); const useNamespace = position === undefined ? undefined : tryUseExistingNamespaceImport(existingImports, symbolName, position, checker); const addToExisting = tryAddToExistingImport(existingImports, position !== undefined && isTypeOnlyPosition(sourceFile, position)); // Don't bother providing an action to add a new import if we can add to an existing one. @@ -409,9 +415,11 @@ namespace ts.codefix { }); } - function getExistingImportDeclarations({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, sourceFile: SourceFile): readonly FixAddToExistingImportInfo[] { + function getExistingImportDeclarations({ moduleSymbol, exportKind, exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] { // Can't use an es6 import for a type in JS. - return exportedSymbolIsTypeOnly && isSourceFileJS(sourceFile) ? emptyArray : mapDefined(sourceFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => { + if (exportedSymbolIsTypeOnly && isSourceFileJS(importingFile)) return emptyArray; + const importKind = getImportKind(importingFile, exportKind, compilerOptions); + return mapDefined(importingFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => { const i = importFromModuleSpecifier(moduleSpecifier); if (isRequireVariableDeclaration(i.parent)) { return checker.resolveExternalModuleName(moduleSpecifier) === moduleSymbol ? { declaration: i.parent, importKind } : undefined; @@ -468,7 +476,7 @@ namespace ts.codefix { // `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types. exportInfo.exportedSymbolIsTypeOnly && isJs && position !== undefined ? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo } - : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind: exportInfo.importKind, useRequire, typeOnly: preferTypeOnlyImport, exportInfo })); + : { kind: ImportFixKind.AddNew, moduleSpecifier, importKind: getImportKind(sourceFile, exportInfo.exportKind, compilerOptions), useRequire, typeOnly: preferTypeOnlyImport, exportInfo })); } function getFixesForAddImport( @@ -532,7 +540,7 @@ namespace ts.codefix { if (!umdSymbol) return undefined; const symbol = checker.getAliasedSymbol(umdSymbol); const symbolName = umdSymbol.name; - const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, isExportEquals: false, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false, isFromPackageJson: false }]; + const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, exportKind: ExportKind.UMD, exportedSymbolIsTypeOnly: false, isFromPackageJson: false }]; const useRequire = shouldUseRequire(sourceFile, program); const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences); return { fixes, symbolName }; @@ -549,6 +557,16 @@ namespace ts.codefix { : undefined; } + function getImportKind(importingFile: SourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions): ImportKind { + switch (exportKind) { + case ExportKind.Named: return ImportKind.Named; + case ExportKind.Default: return ImportKind.Default; + case ExportKind.ExportEquals: return getExportEqualsImportKind(importingFile, compilerOptions); + case ExportKind.UMD: return getUmdImportKind(importingFile, compilerOptions); + default: return Debug.assertNever(exportKind); + } + } + function getUmdImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind { // Import a synthetic `default` if enabled. if (getAllowSyntheticDefaultImports(compilerOptions)) { @@ -616,43 +634,41 @@ namespace ts.codefix { // For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once. // Maps symbol id to info for modules providing that symbol (original export + re-exports). const originalSymbolToExportInfos = createMultiMap(); - function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, isExportEquals: boolean, importKind: ImportKind, checker: TypeChecker, isFromPackageJson: boolean): void { - originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, isExportEquals, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); + function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker, isFromPackageJson: boolean): void { + originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); } forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _, program, isFromPackageJson) => { const checker = program.getTypeChecker(); cancellationToken.throwIfCancellationRequested(); const compilerOptions = program.getCompilerOptions(); - const defaultInfo = getDefaultLikeExportInfo(sourceFile, moduleSymbol, checker, compilerOptions); + const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) { - addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.isExportEquals, defaultInfo.kind, checker, isFromPackageJson); + addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.exportKind, checker, isFromPackageJson); } // check exports with the same name const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { - addSymbol(moduleSymbol, exportSymbolWithIdenticalName, /*isExportEquals*/ false, ImportKind.Named, checker, isFromPackageJson); + addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ExportKind.Named, checker, isFromPackageJson); } }); return originalSymbolToExportInfos; } - function getDefaultLikeExportInfo( - importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions, - ): { readonly symbol: Symbol, readonly symbolForMeaning: Symbol, readonly name: string, readonly kind: ImportKind, isExportEquals: boolean } | undefined { - const exported = getDefaultLikeExportWorker(importingFile, moduleSymbol, checker, compilerOptions); + function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) { + const exported = getDefaultLikeExportWorker(moduleSymbol, checker); if (!exported) return undefined; - const { symbol, kind, isExportEquals } = exported; + const { symbol, exportKind } = exported; const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions); - return info && { symbol, kind, isExportEquals, ...info }; + return info && { symbol, exportKind, ...info }; } - function getDefaultLikeExportWorker(importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbol: Symbol, readonly kind: ImportKind, isExportEquals: boolean } | undefined { + function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined { const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol); - if (defaultExport) return { symbol: defaultExport, kind: ImportKind.Default, isExportEquals: false }; + if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default }; const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); - return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, kind: getExportEqualsImportKind(importingFile, compilerOptions), isExportEquals: true }; + return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; } function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind { diff --git a/src/services/completions.ts b/src/services/completions.ts index 8b6eef3d4156b..3a4c4a729d4b0 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1549,7 +1549,7 @@ namespace ts.Completions { function tryGetImportCompletionSymbols(): GlobalsSearch { if (!importCompletionNode) return GlobalsSearch.Continue; if (!shouldOfferImportCompletions()) return GlobalsSearch.Fail; - collectAndFilterAutoImportCompletions(!!importCompletionNode); + collectAutoImports(!!importCompletionNode); return GlobalsSearch.Success; } @@ -1617,15 +1617,7 @@ namespace ts.Completions { } } } - collectAndFilterAutoImportCompletions(/*resolveModuleSpecifier*/ false); - } - - function collectAndFilterAutoImportCompletions(resolveModuleSpecifier: boolean) { - Debug.assert(!detailsEntryId?.data); - if (shouldOfferImportCompletions()) { - const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; - collectAutoImports(lowerCaseTokenText, resolveModuleSpecifier); - } + collectAutoImports(/*resolveModuleSpecifier*/ false); if (isTypeOnly) { keywordFilters = contextToken && isAssertionExpression(contextToken.parent) ? KeywordCompletionFilters.TypeAssertionKeywords @@ -1701,8 +1693,10 @@ namespace ts.Completions { } /** Mutates `symbols`, `symbolToOriginInfoMap`, and `symbolToSortTextMap` */ - function collectAutoImports(lowerCaseTokenText: string, resolveModuleSpecifiers: boolean) { + function collectAutoImports(resolveModuleSpecifiers: boolean) { + if (!shouldOfferImportCompletions()) return; Debug.assert(!detailsEntryId?.data); + const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*useAutoImportProvider*/ true); exportInfo.forEach((info, key) => { const [symbolName] = key.split("|"); @@ -1715,14 +1709,14 @@ namespace ts.Completions { ? codefix.getBestImportFixForExports(info, sourceFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, program, host, preferences) : { moduleSpecifier: undefined, exportInfo: info[0] }; if (!exportInfo) return; - const isDefaultExport = exportInfo.symbol.name === InternalSymbolName.Default; + const isDefaultExport = exportInfo.exportKind === codefix.ExportKind.Default; const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; const isAmbientModule = !isExternalModuleNameRelative(stripQuotes(exportInfo.moduleSymbol.name)); pushAutoImportSymbol(symbol, { kind: resolveModuleSpecifiers ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, moduleSpecifier, symbolName, - exportName: exportInfo.isExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name, + exportName: exportInfo.exportKind === codefix.ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name, fileName: isAmbientModule ? undefined : cast(exportInfo.moduleSymbol.valueDeclaration, isSourceFile).fileName, isDefaultExport, moduleSymbol: exportInfo.moduleSymbol, @@ -1743,101 +1737,6 @@ namespace ts.Completions { symbols.push(symbol); } - /** - * Gathers symbols that can be imported from other files, de-duplicating along the way. Symbols can be "duplicates" - * if re-exported from another module by the same name, e.g. `export { foo } from "./a"`. - */ - // function getSymbolsFromOtherSourceFileExports(target: ScriptTarget, host: LanguageServiceHost): readonly AutoImportSuggestion[] { - // const cached = importSuggestionsCache && importSuggestionsCache.get( - // sourceFile.fileName, - // typeChecker, - // detailsEntryId && host.getProjectVersion ? host.getProjectVersion() : undefined); - - // if (cached) { - // log("getSymbolsFromOtherSourceFileExports: Using cached list"); - // return cached; - // } - - // const startTime = timestamp(); - // log(`getSymbolsFromOtherSourceFileExports: Recomputing list${detailsEntryId ? " for details entry" : ""}`); - // const seenResolvedModules = new Map(); - // const results = createMultiMap(); - - // 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; - // } - - // const typeChecker = program.getTypeChecker(); - // const resolvedModuleSymbol = typeChecker.resolveExternalModuleSymbol(moduleSymbol); - // // resolvedModuleSymbol may be a namespace. A namespace may be `export =` by multiple module declarations, but only keep the first one. - // if (!addToSeen(seenResolvedModules, getSymbolId(resolvedModuleSymbol))) { - // return; - // } - - // // 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, InternalSymbolName.ExportEquals, moduleSymbol, file, isFromPackageJson); - // } - - // for (const symbol of typeChecker.getExportsAndPropertiesOfModule(moduleSymbol)) { - // // If this is `export { _break as break };` (a keyword) -- skip this and prefer the keyword completion. - // if (some(symbol.declarations, d => isExportSpecifier(d) && !!d.propertyName && isIdentifierANonContextualKeyword(d.name))) { - // continue; - // } - - // pushSymbol(symbol, symbol.name, moduleSymbol, file, isFromPackageJson); - // } - // }); - - // log(`getSymbolsFromOtherSourceFileExports: ${timestamp() - startTime}`); - // return flatten(arrayFrom(results.values())); - - // function pushSymbol(symbol: Symbol, exportName: string, moduleSymbol: Symbol, file: SourceFile | undefined, isFromPackageJson: boolean) { - // const isDefaultExport = symbol.escapedName === InternalSymbolName.Default; - // const nonLocalSymbol = symbol; - // if (isDefaultExport) { - // symbol = getLocalSymbolForExportDefault(symbol) || symbol; - // } - // if (typeChecker.isUndefinedSymbol(symbol)) { - // return; - // } - // const original = skipAlias(nonLocalSymbol, typeChecker); - // 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, - // symbolName, - // isDefaultExport, - // isFromPackageJson, - // exportName, - // fileName: file?.fileName - // }; - // results.add(getSymbolId(original), { - // symbol, - // symbolName, - // origin, - // }); - // } - // } - // } - - /** - * 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 - * will not be offered from different external modules, but they will be offered from - * different ambient modules. - */ - // function moduleSymbolsAreDuplicateOrigins(a: Symbol, b: Symbol) { - // const ambientNameA = pathIsBareSpecifier(stripQuotes(a.name)) ? a.name : undefined; - // const ambientNameB = pathIsBareSpecifier(stripQuotes(b.name)) ? b.name : undefined; - // return ambientNameA === ambientNameB; - // } - /** * True if you could remove some characters in `a` to get `b`. * E.g., true for "abcdef" and "bdf". From c7e9bc7686c22b5f42dc9f704654ac42d2fa6aa4 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 8 Mar 2021 11:00:18 -0800 Subject: [PATCH 11/35] Add cache back in --- src/server/project.ts | 2 +- src/services/codefixes/importFixes.ts | 87 ++++++++++++++++++++++++--- src/services/completions.ts | 66 ++------------------ src/services/types.ts | 2 +- 4 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 9220d134fa234..256ddcb071493 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -245,7 +245,7 @@ namespace ts.server { public readonly getCanonicalFileName: GetCanonicalFileName; /*@internal*/ - private importSuggestionsCache = Completions.createImportSuggestionsForFileCache(); + private importSuggestionsCache = codefix.createImportSuggestionsForFileCache(); /*@internal*/ private dirtyFilesForSuggestions: Set | undefined; /*@internal*/ diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 26c8165c0697d..816adab20cf4f 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -196,13 +196,13 @@ namespace ts.codefix { /** Information about how a symbol is exported from a module. */ export interface SymbolExportInfo { - readonly symbol: Symbol; - readonly moduleSymbol: Symbol; - readonly exportKind: ExportKind; + symbol: Symbol; + moduleSymbol: Symbol; + exportKind: ExportKind; /** If true, can't use an es6 import from a js file. */ - readonly exportedSymbolIsTypeOnly: boolean; + exportedSymbolIsTypeOnly: boolean; /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ - readonly isFromPackageJson: boolean; + isFromPackageJson: boolean; } /** Information needed to augment an existing import declaration. */ @@ -211,6 +211,59 @@ namespace ts.codefix { readonly importKind: ImportKind; } + export interface ImportSuggestionsForFileCache { + clear(): void; + get(fileName: string, checker: TypeChecker, projectVersion?: string): MultiMap | undefined; + set(fileName: string, suggestions: MultiMap, projectVersion?: string): void; + isEmpty(): boolean; + } + export function createImportSuggestionsForFileCache(): ImportSuggestionsForFileCache { + let cache: MultiMap | undefined; + let projectVersion: string | undefined; + let fileName: string | undefined; + return { + isEmpty() { + return !cache; + }, + clear: () => { + cache = undefined; + fileName = undefined; + projectVersion = undefined; + }, + set: (file, suggestions, version) => { + cache = suggestions; + fileName = file; + if (version) { + projectVersion = version; + } + }, + get: (file, checker, version) => { + if (file !== fileName) { + return undefined; + } + if (version) { + return projectVersion === version ? cache : undefined; + } + cache?.forEach(infos => { + for (const info of infos) { + // If the symbol/moduleSymbol was a merged symbol, it will have a new identity + // in the checker, even though the symbols to merge are the same (guaranteed by + // cache invalidation in synchronizeHostData). + if (info.symbol.declarations?.length) { + info.symbol = checker.getMergedSymbol(info.exportKind === ExportKind.Default + ? info.symbol.declarations[0].localSymbol ?? info.symbol.declarations[0].symbol + : info.symbol.declarations[0].symbol); + } + if (info.moduleSymbol.declarations?.length) { + info.moduleSymbol = checker.getMergedSymbol(info.moduleSymbol.declarations[0].symbol); + } + } + }); + return cache; + }, + }; + } + export function getImportCompletionAction( exportedSymbol: Symbol, moduleSymbol: Symbol, @@ -290,11 +343,25 @@ namespace ts.codefix { return getBestFix(getNewImportFixes(program, importingFile, position, preferTypeOnlyImport, useRequire, exportInfo, host, preferences), importingFile, program, host); } - export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean) { + export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, filterByPackageJson: boolean) { + host.log?.(`getSymbolToExportInfoMap: starting for ${importingFile.fileName}`); + const start = timestamp(); + const cache = host.getImportSuggestionsCache?.(); + if (cache) { + const cached = cache.get(importingFile.fileName, program.getTypeChecker(), host.getProjectVersion?.()); + if (cached) { + host.log?.("getSymbolToExportInfoMap: cache hit"); + return cached; + } + else { + host.log?.("getSymbolToExportInfoMap: cache miss or empty; calculating new results"); + } + } + const result: MultiMap = createMultiMap(); const compilerOptions = program.getCompilerOptions(); const target = getEmitScriptTarget(compilerOptions); - forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { + forEachExternalModuleToImportFrom(program, host, importingFile, filterByPackageJson, /*useAutoImportProvider*/ true, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo) { @@ -308,6 +375,12 @@ namespace ts.codefix { } } }); + + if (cache && filterByPackageJson) { + host.log?.("getSymbolToExportInfoMap: caching results"); + cache.set(importingFile.fileName, result, host.getProjectVersion?.()); + } + host.log?.(`getSymbolToExportInfoMap: done in ${timestamp() - start} ms`); return result; function key(name: string, alias: Symbol, moduleSymbol: Symbol, checker: TypeChecker) { diff --git a/src/services/completions.ts b/src/services/completions.ts index 3a4c4a729d4b0..4a9f5538a432c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -122,67 +122,6 @@ namespace ts.Completions { const enum GlobalsSearch { Continue, Success, Fail } - export interface AutoImportSuggestion { - symbol: Symbol; - symbolName: string; - origin: SymbolOriginInfoExport; - } - export interface AutoImportSuggestionWithModuleSpecifier { - symbol: Symbol; - symbolName: string; - moduleSpecifier: string; - } - export interface ImportSuggestionsForFileCache { - clear(): void; - get(fileName: string, checker: TypeChecker, projectVersion?: string): readonly AutoImportSuggestion[] | undefined; - set(fileName: string, suggestions: readonly AutoImportSuggestion[], projectVersion?: string): void; - isEmpty(): boolean; - } - export function createImportSuggestionsForFileCache(): ImportSuggestionsForFileCache { - let cache: readonly AutoImportSuggestion[] | undefined; - let projectVersion: string | undefined; - let fileName: string | undefined; - return { - isEmpty() { - return !cache; - }, - clear: () => { - cache = undefined; - fileName = undefined; - projectVersion = undefined; - }, - set: (file, suggestions, version) => { - cache = suggestions; - fileName = file; - if (version) { - projectVersion = version; - } - }, - get: (file, checker, version) => { - if (file !== fileName) { - return undefined; - } - if (version) { - return projectVersion === version ? cache : undefined; - } - forEach(cache, suggestion => { - // If the symbol/moduleSymbol was a merged symbol, it will have a new identity - // in the checker, even though the symbols to merge are the same (guaranteed by - // cache invalidation in synchronizeHostData). - if (suggestion.symbol.declarations?.length) { - suggestion.symbol = checker.getMergedSymbol(suggestion.origin.isDefaultExport - ? suggestion.symbol.declarations[0].localSymbol ?? suggestion.symbol.declarations[0].symbol - : suggestion.symbol.declarations[0].symbol); - } - if (suggestion.origin.moduleSymbol.declarations?.length) { - suggestion.origin.moduleSymbol = checker.getMergedSymbol(suggestion.origin.moduleSymbol.declarations[0].symbol); - } - }); - return cache; - }, - }; - } - export function getCompletionsAtPosition( host: LanguageServiceHost, program: Program, @@ -1696,8 +1635,10 @@ namespace ts.Completions { function collectAutoImports(resolveModuleSpecifiers: boolean) { if (!shouldOfferImportCompletions()) return; Debug.assert(!detailsEntryId?.data); + const start = timestamp(); + host.log?.(`collectAutoImports: starting, ${resolveModuleSpecifiers ? "" : "not "}resolving module specifiers`); const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; - const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*useAutoImportProvider*/ true); + const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*filterByPackageJson*/ !detailsEntryId); exportInfo.forEach((info, key) => { const [symbolName] = key.split("|"); if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; @@ -1724,6 +1665,7 @@ namespace ts.Completions { }); } }); + host.log?.(`collectAutoImports: done in ${timestamp() - start} ms`); } function pushAutoImportSymbol(symbol: Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { diff --git a/src/services/types.ts b/src/services/types.ts index f92ac6d560ecf..a76cb3afc2257 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -305,7 +305,7 @@ namespace ts { /* @internal */ getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[]; /* @internal */ - getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache; + getImportSuggestionsCache?(): codefix.ImportSuggestionsForFileCache; /* @internal */ setCompilerHost?(host: CompilerHost): void; /* @internal */ From 393fce5a0e4532e542fe708b2b0bc32a8288c824 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 8 Mar 2021 13:27:42 -0800 Subject: [PATCH 12/35] Set insertText based on import kind --- src/services/codefixes/importFixes.ts | 4 +- src/services/completions.ts | 56 +++++++++++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 816adab20cf4f..f31964c3b586e 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -180,7 +180,7 @@ namespace ts.codefix { readonly exportInfo?: SymbolExportInfo; } - const enum ImportKind { + export const enum ImportKind { Named, Default, Namespace, @@ -630,7 +630,7 @@ namespace ts.codefix { : undefined; } - function getImportKind(importingFile: SourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions): ImportKind { + export function getImportKind(importingFile: SourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions): ImportKind { switch (exportKind) { case ExportKind.Named: return ImportKind.Named; case ExportKind.Default: return ImportKind.Default; diff --git a/src/services/completions.ts b/src/services/completions.ts index 4a9f5538a432c..7b289f8e822ea 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -192,6 +192,9 @@ namespace ts.Completions { symbolToOriginInfoMap, recommendedCompletion, isJsxInitializer, + isTypeOnlyLocation, + isJsxIdentifierExpected, + importCompletionNode, insideJsDocTagTypeExpression, symbolToSortTextMap, } = completionData; @@ -219,10 +222,11 @@ namespace ts.Completions { completionKind, preferences, compilerOptions, - completionData.isTypeOnlyLocation, + isTypeOnlyLocation, propertyAccessToConvert, - completionData.isJsxIdentifierExpected, + isJsxIdentifierExpected, isJsxInitializer, + importCompletionNode, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap @@ -246,10 +250,11 @@ namespace ts.Completions { completionKind, preferences, compilerOptions, - completionData.isTypeOnlyLocation, + isTypeOnlyLocation, propertyAccessToConvert, - completionData.isJsxIdentifierExpected, + isJsxIdentifierExpected, isJsxInitializer, + importCompletionNode, recommendedCompletion, symbolToOriginInfoMap, symbolToSortTextMap @@ -386,6 +391,8 @@ namespace ts.Completions { recommendedCompletion: Symbol | undefined, propertyAccessToConvert: PropertyAccessExpression | undefined, isJsxInitializer: IsJsxInitializer | undefined, + importCompletionNode: Node | undefined, + options: CompilerOptions, preferences: UserPreferences, ): CompletionEntry | undefined { let insertText: string | undefined; @@ -438,7 +445,8 @@ namespace ts.Completions { } if (originIsResolvedExport(origin)) { - insertText = `${origin.symbolName} from "${origin.moduleSpecifier}"`; + Debug.assertIsDefined(importCompletionNode); + ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, origin, options, preferences)); } if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { @@ -478,6 +486,23 @@ namespace ts.Completions { }; } + function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, origin: SymbolOriginInfoResolvedExport, options: CompilerOptions, preferences: UserPreferences) { + const sourceFile = importCompletionNode.getSourceFile(); + const replacementSpan = createTextSpanFromNode(importCompletionNode, sourceFile); + const quotedModuleSpecifier = quote(sourceFile, preferences, origin.moduleSpecifier); + const exportKind = + origin.isDefaultExport ? codefix.ExportKind.Default : + origin.exportName === InternalSymbolName.ExportEquals ? codefix.ExportKind.ExportEquals : + codefix.ExportKind.Named; + const importKind = codefix.getImportKind(sourceFile, exportKind, options); + switch (importKind) { + case codefix.ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name} = require(${quotedModuleSpecifier})` }; + case codefix.ImportKind.Default: return { replacementSpan, insertText: `import ${name} from ${quotedModuleSpecifier}` }; + case codefix.ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name} from ${quotedModuleSpecifier}` }; + case codefix.ImportKind.Named: return { replacementSpan, insertText: `import { ${name} } from ${quotedModuleSpecifier}` }; + } + } + function quotePropertyName(sourceFile: SourceFile, preferences: UserPreferences, name: string,): string { if (/^\d+$/.test(name)) { return name; @@ -519,6 +544,7 @@ namespace ts.Completions { propertyAccessToConvert?: PropertyAccessExpression, jsxIdentifierExpected?: boolean, isJsxInitializer?: IsJsxInitializer, + importCompletionNode?: Node, recommendedCompletion?: Symbol, symbolToOriginInfoMap?: SymbolOriginInfoMap, symbolToSortTextMap?: SymbolSortTextMap, @@ -552,6 +578,8 @@ namespace ts.Completions { recommendedCompletion, propertyAccessToConvert, isJsxInitializer, + importCompletionNode, + compilerOptions, preferences ); if (!entry) { @@ -866,6 +894,7 @@ namespace ts.Completions { readonly isTypeOnlyLocation: boolean; /** In JSX tag name and attribute names, identifiers like "my-tag" or "aria-name" is valid identifier. */ readonly isJsxIdentifierExpected: boolean; + readonly importCompletionNode?: Node; } type Request = { readonly kind: CompletionDataKind.JsDocTagName | CompletionDataKind.JsDocTag } | { readonly kind: CompletionDataKind.JsDocParameterName, tag: JSDocParameterTag }; @@ -1046,18 +1075,18 @@ namespace ts.Completions { let isStartingCloseTag = false; let isJsxInitializer: IsJsxInitializer = false; let isJsxIdentifierExpected = false; - let importCompletionNode: ImportEqualsDeclaration | ImportDeclaration | Token | undefined; + let importCompletionNode: Node | undefined; let location = getTouchingPropertyName(sourceFile, position); if (contextToken) { + importCompletionNode = getImportCompletionNode(contextToken); // Bail out if this is a known invalid completion location - if (isCompletionListBlocker(contextToken)) { + if (!importCompletionNode && isCompletionListBlocker(contextToken)) { log("Returning an empty list because completion was requested in an invalid position."); return undefined; } let parent = contextToken.parent; - importCompletionNode = getImportCompletionNode(contextToken); if (contextToken.kind === SyntaxKind.DotToken || contextToken.kind === SyntaxKind.QuestionDotToken) { isRightOfDot = contextToken.kind === SyntaxKind.DotToken; isRightOfQuestionDot = contextToken.kind === SyntaxKind.QuestionDotToken; @@ -1239,6 +1268,7 @@ namespace ts.Completions { symbolToSortTextMap, isTypeOnlyLocation: isTypeOnly, isJsxIdentifierExpected, + importCompletionNode, }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; @@ -1451,12 +1481,12 @@ namespace ts.Completions { function tryGetGlobalSymbols(): boolean { const result: GlobalsSearch = tryGetObjectLikeCompletionSymbols() + || tryGetImportCompletionSymbols() || tryGetImportOrExportClauseCompletionSymbols() || tryGetLocalNamedExportCompletionSymbols() || tryGetConstructorCompletion() || tryGetClassLikeCompletionSymbols() || tryGetJsxCompletionSymbols() - || tryGetImportCompletionSymbols() || (getGlobalCompletions(), GlobalsSearch.Success); return result === GlobalsSearch.Success; } @@ -1488,7 +1518,7 @@ namespace ts.Completions { function tryGetImportCompletionSymbols(): GlobalsSearch { if (!importCompletionNode) return GlobalsSearch.Continue; if (!shouldOfferImportCompletions()) return GlobalsSearch.Fail; - collectAutoImports(!!importCompletionNode); + collectAutoImports(/*resolveModuleSpecifiers*/ true); return GlobalsSearch.Success; } @@ -2830,11 +2860,13 @@ namespace ts.Completions { return nodeIsMissing(parent.moduleReference) ? parent : undefined; } if (isNamedImports(parent) || isNamespaceImport(parent)) { - return nodeIsMissing(parent.parent.parent.moduleSpecifier) ? parent.parent.parent : undefined; + return nodeIsMissing(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) + ? parent.parent.parent + : undefined; } if (isImportKeyword(contextToken) && isSourceFile(parent)) { // A lone import keyword with nothing following it does not parse as a statement at all - return contextToken; + return contextToken as Token; } return undefined; } From 8999db9faef8def47e27fb4cdddc6578fe5d0d07 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 8 Mar 2021 13:29:34 -0800 Subject: [PATCH 13/35] Update API baselines --- .../reference/api/tsserverlibrary.d.ts | 17 ++++++++++++++++- tests/baselines/reference/api/typescript.d.ts | 7 +++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 71c0361b07752..a5003aacef1ab 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -6107,6 +6107,10 @@ declare namespace ts { * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. */ exportName: string; + /** + * Set for auto imports with eagerly resolved module specifiers. + */ + moduleSpecifier?: string; } interface CompletionEntry { name: string; @@ -6122,6 +6126,7 @@ declare namespace ts { replacementSpan?: TextSpan; hasAction?: true; source?: string; + sourceDisplay?: SymbolDisplayPart[]; isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; @@ -6143,7 +6148,9 @@ declare namespace ts { documentation?: SymbolDisplayPart[]; tags?: JSDocTagInfo[]; codeActions?: CodeAction[]; + /** @deprecated Use `sourceDisplay` instead. */ source?: SymbolDisplayPart[]; + sourceDisplay?: SymbolDisplayPart[]; } interface OutliningSpan { /** The span of the document to actually collapse. */ @@ -8259,6 +8266,10 @@ declare namespace ts.server.protocol { * Identifier (not necessarily human-readable) identifying where this completion came from. */ source?: string; + /** + * Human-readable description of the `source`. + */ + sourceDisplay?: SymbolDisplayPart[]; /** * If true, this completion should be highlighted as recommended. There will only be one of these. * This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class. @@ -8315,9 +8326,13 @@ declare namespace ts.server.protocol { */ codeActions?: CodeAction[]; /** - * Human-readable description of the `source` from the CompletionEntry. + * @deprecated Use `sourceDisplay` instead. */ source?: SymbolDisplayPart[]; + /** + * Human-readable description of the `source` from the CompletionEntry. + */ + sourceDisplay?: SymbolDisplayPart[]; } /** @deprecated Prefer CompletionInfoResponse, which supports several top-level fields in addition to the array of entries. */ interface CompletionsResponse extends Response { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 6847f968ab894..75ef6dd2ca583 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6107,6 +6107,10 @@ declare namespace ts { * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. */ exportName: string; + /** + * Set for auto imports with eagerly resolved module specifiers. + */ + moduleSpecifier?: string; } interface CompletionEntry { name: string; @@ -6122,6 +6126,7 @@ declare namespace ts { replacementSpan?: TextSpan; hasAction?: true; source?: string; + sourceDisplay?: SymbolDisplayPart[]; isRecommended?: true; isFromUncheckedFile?: true; isPackageJsonImport?: true; @@ -6143,7 +6148,9 @@ declare namespace ts { documentation?: SymbolDisplayPart[]; tags?: JSDocTagInfo[]; codeActions?: CodeAction[]; + /** @deprecated Use `sourceDisplay` instead. */ source?: SymbolDisplayPart[]; + sourceDisplay?: SymbolDisplayPart[]; } interface OutliningSpan { /** The span of the document to actually collapse. */ From 6c8457b10feedacbdaebf813f3887b0b1978d614 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Mar 2021 12:11:11 -0800 Subject: [PATCH 14/35] Add semicolons, snippet support, and sourceDisplay --- src/server/project.ts | 4 ++-- src/server/protocol.ts | 4 ++++ src/server/session.ts | 4 ++-- src/services/codefixes/importFixes.ts | 3 ++- src/services/completions.ts | 22 ++++++++++++++++------ src/services/types.ts | 1 + 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 256ddcb071493..3244bf075d35d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1168,8 +1168,8 @@ namespace ts.server { } else if (this.dirtyFilesForSuggestions && oldProgram && this.program) { forEachKey(this.dirtyFilesForSuggestions, fileName => { - const oldSourceFile = oldProgram.getSourceFile(fileName); - const sourceFile = this.program!.getSourceFile(fileName); + const oldSourceFile = oldProgram.getSourceFileByPath(fileName); + const sourceFile = this.program!.getSourceFileByPath(fileName); if (this.sourceFileHasChangedOwnImportSuggestions(oldSourceFile, sourceFile)) { this.importSuggestionsCache.clear(); return true; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 69472b9da81fd..3cc0b65f0f3da 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2232,6 +2232,10 @@ namespace ts.server.protocol { * coupled with `replacementSpan` to replace a dotted access with a bracket access. */ insertText?: string; + /** + * `insertText` should be interpreted as a snippet if true. + */ + isSnippet?: true; /** * An optional span that indicates the text to be replaced by this completion item. * If present, this span should be used instead of the default one. diff --git a/src/server/session.ts b/src/server/session.ts index aeb695bf1477b..d10d470da9142 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1810,10 +1810,10 @@ namespace ts.server { const prefix = args.prefix || ""; 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 { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, sourceDisplay, isSnippet, 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 }; + return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, isSnippet, hasAction: hasAction || undefined, source, sourceDisplay, isRecommended, isPackageJsonImport, data }; } }), (a, b) => compareStringsCaseSensitiveUI(a.name, b.name)); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index f31964c3b586e..e36a0f181f88f 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -760,7 +760,8 @@ namespace ts.codefix { // really hate that, so look to see if the importing file has any precedent // on how to handle it. for (const statement of importingFile.statements) { - if (isImportEqualsDeclaration(statement)) { + // `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration + if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) { return ImportKind.CommonJS; } } diff --git a/src/services/completions.ts b/src/services/completions.ts index 7b289f8e822ea..d2fd3b0529cc6 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -392,12 +392,15 @@ namespace ts.Completions { propertyAccessToConvert: PropertyAccessExpression | undefined, isJsxInitializer: IsJsxInitializer | undefined, importCompletionNode: Node | undefined, + useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences, ): CompletionEntry | undefined { let insertText: string | undefined; let replacementSpan = getReplacementSpanForContextToken(contextToken); let data: CompletionEntryData | undefined; + let isSnippet: true | undefined; + let sourceDisplay; const insertQuestionDot = origin && originIsNullableMember(origin); const useBraces = origin && originIsSymbolMember(origin) || needsConvertPropertyAccess; @@ -446,7 +449,9 @@ namespace ts.Completions { if (originIsResolvedExport(origin)) { Debug.assertIsDefined(importCompletionNode); - ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, origin, options, preferences)); + ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, origin, useSemicolons, options, preferences)); + sourceDisplay = [textPart(origin.moduleSpecifier)]; + isSnippet = true; } if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { @@ -481,12 +486,14 @@ namespace ts.Completions { isRecommended: isRecommendedCompletionMatch(symbol, recommendedCompletion, typeChecker) || undefined, insertText, replacementSpan, + sourceDisplay, + isSnippet, isPackageJsonImport: originIsPackageJsonImport(origin) || undefined, data, }; } - function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, origin: SymbolOriginInfoResolvedExport, options: CompilerOptions, preferences: UserPreferences) { + function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences) { const sourceFile = importCompletionNode.getSourceFile(); const replacementSpan = createTextSpanFromNode(importCompletionNode, sourceFile); const quotedModuleSpecifier = quote(sourceFile, preferences, origin.moduleSpecifier); @@ -495,11 +502,12 @@ namespace ts.Completions { origin.exportName === InternalSymbolName.ExportEquals ? codefix.ExportKind.ExportEquals : codefix.ExportKind.Named; const importKind = codefix.getImportKind(sourceFile, exportKind, options); + const suffix = useSemicolons ? ";" : ""; switch (importKind) { - case codefix.ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name} = require(${quotedModuleSpecifier})` }; - case codefix.ImportKind.Default: return { replacementSpan, insertText: `import ${name} from ${quotedModuleSpecifier}` }; - case codefix.ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name} from ${quotedModuleSpecifier}` }; - case codefix.ImportKind.Named: return { replacementSpan, insertText: `import { ${name} } from ${quotedModuleSpecifier}` }; + case codefix.ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name}$1 = require(${quotedModuleSpecifier})${suffix}` }; + case codefix.ImportKind.Default: return { replacementSpan, insertText: `import ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; + case codefix.ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; + case codefix.ImportKind.Named: return { replacementSpan, insertText: `import { ${name}$1 } from ${quotedModuleSpecifier}${suffix}` }; } } @@ -551,6 +559,7 @@ namespace ts.Completions { ): UniqueNameSet { const start = timestamp(); const variableDeclaration = getVariableDeclaration(location); + const useSemicolons = probablyUsesSemicolons(sourceFile); // Tracks unique names. // Value is set to false for global variables or completions from external module exports, because we can have multiple of those; // true otherwise. Based on the order we add things we will always see locals first, then globals, then module exports. @@ -579,6 +588,7 @@ namespace ts.Completions { propertyAccessToConvert, isJsxInitializer, importCompletionNode, + useSemicolons, compilerOptions, preferences ); diff --git a/src/services/types.ts b/src/services/types.ts index a76cb3afc2257..0226cd1f47565 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1164,6 +1164,7 @@ namespace ts { kindModifiers?: string; // see ScriptElementKindModifier, comma separated sortText: string; insertText?: string; + isSnippet?: true; /** * An optional span that indicates the text to be replaced by this completion item. * If present, this span should be used instead of the default one. From db75d56e201bebd9e5d21dcbed182ce120e6741b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Mar 2021 16:30:09 -0800 Subject: [PATCH 15/35] Add some tests --- src/compiler/checker.ts | 17 +++-- src/compiler/types.ts | 1 + src/harness/fourslashImpl.ts | 9 ++- src/harness/fourslashInterfaceImpl.ts | 1 + src/server/protocol.ts | 5 ++ src/services/completions.ts | 32 +++++++-- tests/cases/fourslash/fourslash.ts | 2 + .../fourslash/importStatementCompletions1.ts | 66 +++++++++++++++++++ ...rtStatementCompletions_esModuleInterop1.ts | 26 ++++++++ ...rtStatementCompletions_esModuleInterop2.ts | 26 ++++++++ .../importStatementCompletions_quotes.ts | 24 +++++++ .../importStatementCompletions_semicolons.ts | 24 +++++++ 12 files changed, 218 insertions(+), 15 deletions(-) create mode 100644 tests/cases/fourslash/importStatementCompletions1.ts create mode 100644 tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts create mode 100644 tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts create mode 100644 tests/cases/fourslash/importStatementCompletions_quotes.ts create mode 100644 tests/cases/fourslash/importStatementCompletions_semicolons.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 592efb9e4f60a..1631d1e95ec9b 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3493,7 +3493,10 @@ namespace ts { const exports = getExportsOfModuleAsArray(moduleSymbol); const exportEquals = resolveExternalModuleSymbol(moduleSymbol); if (exportEquals !== moduleSymbol) { - addRange(exports, getPropertiesOfType(getTypeOfSymbol(exportEquals))); + const type = getTypeOfSymbol(exportEquals); + if (shouldTreatPropertiesOfExternalModuleAsExports(type)) { + addRange(exports, getPropertiesOfType(type)); + } } return exports; } @@ -3517,11 +3520,13 @@ namespace ts { } const type = getTypeOfSymbol(exportEquals); - return type.flags & TypeFlags.Primitive || - getObjectFlags(type) & ObjectFlags.Class || - isArrayOrTupleLikeType(type) - ? undefined - : getPropertyOfType(type, memberName); + return shouldTreatPropertiesOfExternalModuleAsExports(type) ? getPropertyOfType(type, memberName) : undefined; + } + + function shouldTreatPropertiesOfExternalModuleAsExports(resolvedExternalModuleType: Type) { + return !(resolvedExternalModuleType.flags & TypeFlags.Primitive || + getObjectFlags(resolvedExternalModuleType) & ObjectFlags.Class || + isArrayOrTupleLikeType(resolvedExternalModuleType)); } function getExportsOfSymbol(symbol: Symbol): SymbolTable { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8cfddadc7acaa..02ef982eda0f2 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8253,6 +8253,7 @@ namespace ts { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsForImportStatements?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 73d83a1fb3510..212e25bd612ce 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -930,8 +930,12 @@ namespace FourSlash { assert.equal(actual.hasAction, expected.hasAction, `Expected 'hasAction' properties to match`); assert.equal(actual.isRecommended, expected.isRecommended, `Expected 'isRecommended' properties to match'`); + assert.equal(actual.isSnippet, expected.isSnippet, `Expected 'isSnippet' properties to match`); assert.equal(actual.source, expected.source, `Expected 'source' values to match`); - assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, this.messageAtLastKnownMarker(`Actual entry: ${JSON.stringify(actual)}`)); + assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, `Expected 'sortText' properties to match`); + if (expected.sourceDisplay) { + assert.equal(ts.displayPartsToString(actual.sourceDisplay), expected.sourceDisplay, `Expected 'sourceDisplay' properties to match`); + } if (expected.text !== undefined) { 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}'`); @@ -941,10 +945,11 @@ namespace FourSlash { // assert.equal(actualDetails.kind, actual.kind); assert.equal(actualDetails.kindModifiers, actual.kindModifiers, "Expected 'kindModifiers' properties to match"); assert.equal(actualDetails.source && ts.displayPartsToString(actualDetails.source), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'source' display parts string"); + assert.equal(actualDetails.sourceDisplay && ts.displayPartsToString(actualDetails.sourceDisplay), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'sourceDisplay' display parts string"); assert.deepEqual(actualDetails.tags, expected.tags); } else { - assert(expected.documentation === undefined && expected.tags === undefined && expected.sourceDisplay === undefined, "If specifying completion details, should specify 'text'"); + assert(expected.documentation === undefined && expected.tags === undefined, "If specifying completion details, should specify 'text'"); } } diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index a909dd1c08027..3ff59726e9506 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -1591,6 +1591,7 @@ namespace FourSlashInterface { readonly isFromUncheckedFile?: boolean; // If not specified, won't assert about this readonly kind?: string; // If not specified, won't assert about this readonly isPackageJsonImport?: boolean; // If not specified, won't assert about this + readonly isSnippet?: boolean; readonly kindModifiers?: string; // Must be paired with 'kind' readonly text?: string; readonly documentation?: string; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3cc0b65f0f3da..8fe1c6e956962 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3290,6 +3290,11 @@ namespace ts.server.protocol { * This affects lone identifier completions but not completions on the right hand side of `obj.`. */ readonly includeCompletionsForModuleExports?: boolean; + /** + * Enables auto-import-style completions on partially-typed import statements. E.g., allows + * `import write|` to be completed to `import { writeFile } from "fs"`. + */ + readonly includeCompletionsForImportStatements?: boolean; /** * If enabled, the completion list will include completions with invalid identifier names. * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. diff --git a/src/services/completions.ts b/src/services/completions.ts index d2fd3b0529cc6..e16b062b7839a 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -635,7 +635,8 @@ namespace ts.Completions { if (!!sourceFile.externalModuleIndicator && !compilerOptions.allowUmdGlobalAccess && symbolToSortTextMap[getSymbolId(symbol)] === SortText.GlobalsOrKeywords - && symbolToSortTextMap[getSymbolId(symbolOrigin)] === SortText.AutoImportSuggestions) { + && (symbolToSortTextMap[getSymbolId(symbolOrigin)] === SortText.AutoImportSuggestions + || symbolToSortTextMap[getSymbolId(symbolOrigin)] === SortText.LocationPriority)) { return false; } // Continue with origin symbol @@ -978,7 +979,7 @@ namespace ts.Completions { sourceFile: SourceFile, isUncheckedFile: boolean, position: number, - preferences: Pick, + preferences: UserPreferences, detailsEntryId: CompletionEntryIdentifier | undefined, host: LanguageServiceHost ): CompletionData | Request | undefined { @@ -1089,7 +1090,13 @@ namespace ts.Completions { let location = getTouchingPropertyName(sourceFile, position); if (contextToken) { - importCompletionNode = getImportCompletionNode(contextToken); + // Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier` + // added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature + // is not backward compatible with older clients, the language service defaults to disabling it, allowing newer clients + // to opt in with the `includeCompletionsForImportStatements` user preference. + importCompletionNode = preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText + ? getImportCompletionNode(contextToken) + : undefined; // Bail out if this is a known invalid completion location if (!importCompletionNode && isCompletionListBlocker(contextToken)) { log("Returning an empty list because completion was requested in an invalid position."); @@ -1527,7 +1534,6 @@ namespace ts.Completions { function tryGetImportCompletionSymbols(): GlobalsSearch { if (!importCompletionNode) return GlobalsSearch.Continue; - if (!shouldOfferImportCompletions()) return GlobalsSearch.Fail; collectAutoImports(/*resolveModuleSpecifiers*/ true); return GlobalsSearch.Success; } @@ -1605,6 +1611,8 @@ namespace ts.Completions { } function shouldOfferImportCompletions(): boolean { + // If already typing an import statement, provide completions for it. + if (importCompletionNode) return true; // If current completion is for non-contextual Object literal shortahands, ignore auto-import symbols if (isNonContextualObjectLiteral) return false; // If not already a module, must have modules enabled. @@ -1715,7 +1723,7 @@ namespace ts.Completions { return; } symbolToOriginInfoMap[symbols.length] = origin; - symbolToSortTextMap[symbolId] = SortText.AutoImportSuggestions; + symbolToSortTextMap[symbolId] = importCompletionNode ? SortText.LocationPriority : SortText.AutoImportSuggestions; symbols.push(symbol); } @@ -2263,6 +2271,7 @@ namespace ts.Completions { case SyntaxKind.InterfaceKeyword: case SyntaxKind.FunctionKeyword: case SyntaxKind.VarKeyword: + case SyntaxKind.ImportKeyword: case SyntaxKind.LetKeyword: case SyntaxKind.ConstKeyword: case SyntaxKind.InferKeyword: @@ -2867,10 +2876,10 @@ namespace ts.Completions { function getImportCompletionNode(contextToken: Node) { const parent = contextToken.parent; if (isImportEqualsDeclaration(parent)) { - return nodeIsMissing(parent.moduleReference) ? parent : undefined; + return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined; } if (isNamedImports(parent) || isNamespaceImport(parent)) { - return nodeIsMissing(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) + return isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name ? parent.parent.parent : undefined; } @@ -2878,9 +2887,18 @@ namespace ts.Completions { // A lone import keyword with nothing following it does not parse as a statement at all return contextToken as Token; } + if (isImportKeyword(contextToken) && isImportDeclaration(parent)) { + // `import s| from` + return isModuleSpecifierMissingOrEmpty(parent.moduleSpecifier) ? parent : undefined; + } return undefined; } + function isModuleSpecifierMissingOrEmpty(specifier: ModuleReference | Expression) { + if (nodeIsMissing(specifier)) return true; + return !tryCast(isExternalModuleReference(specifier) ? specifier.expression : specifier, isStringLiteralLike)?.text; + } + function getVariableDeclaration(property: Node): VariableDeclaration | undefined { const variableDeclaration = findAncestor(property, node => isFunctionBlock(node) || isArrowFunctionBody(node) || isBindingPattern(node) diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 845c0674a9830..cac43c638f9a8 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -619,6 +619,7 @@ declare namespace FourSlashInterface { interface UserPreferences { readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsForImportStatements?: boolean; readonly includeInsertTextCompletions?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; @@ -648,6 +649,7 @@ declare namespace FourSlashInterface { readonly kindModifiers?: string; readonly sortText?: completion.SortText; readonly isPackageJsonImport?: boolean; + readonly isSnippet?: boolean; // details readonly text?: string; diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts new file mode 100644 index 0000000000000..5352e7afbbbcd --- /dev/null +++ b/tests/cases/fourslash/importStatementCompletions1.ts @@ -0,0 +1,66 @@ +/// + +// @Filename: /mod.ts +//// export const foo = 0; + +// @Filename: /index0.ts +//// [|import f/*0*/|] + +// @Filename: /index1.ts +//// [|import { f/*1*/}|] + +// @Filename: /index2.ts +//// [|import * as f/*2*/|] + +// @Filename: /index3.ts +//// [|import f/*3*/ from|] + +// @Filename: /index4.ts +//// [|import f/*4*/ =|] + +// @Filename: /index5.ts +//// import f/*5*/ from ""; + +[0, 1, 2, 3, 4, 5].forEach(marker => { + verify.completions({ + marker: "" + marker, + exact: [{ + name: "foo", + source: "./mod", + insertText: `import { foo$1 } from "./mod";`, + isSnippet: true, + replacementSpan: test.ranges()[marker], + sourceDisplay: "./mod", + }], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } + }); +}); + +// @Filename: /index6.ts +//// import f/*6*/ from "nope"; + +// @Filename: /index7.ts +//// import { f/*7*/, bar } + +// @Filename: /index8.ts +//// import foo, { f/*8*/ } + +// @Filename: /index9.ts +//// import g/*9*/ + +// @Filename: /index10.ts +//// import f/*10*/ from "./mod"; + +[6, 7, 8, 9, 10].forEach(marker => { + verify.completions({ + marker: "" + marker, + exact: [], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } + }); +}); diff --git a/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts b/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts new file mode 100644 index 0000000000000..8b1e1cfe5287c --- /dev/null +++ b/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts @@ -0,0 +1,26 @@ +/// + +// @esModuleInterop: false + +// @Filename: /mod.ts +//// const foo = 0; +//// export = foo; + +// @Filename: /importExportEquals.ts +//// [|import f/**/|] + +verify.completions({ + marker: "", + exact: [{ + name: "foo", + source: "./mod", + insertText: `import foo$1 = require("./mod");`, + isSnippet: true, + replacementSpan: test.ranges()[0], + sourceDisplay: "./mod", + }], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } +}); diff --git a/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts b/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts new file mode 100644 index 0000000000000..aad113cff99f5 --- /dev/null +++ b/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts @@ -0,0 +1,26 @@ +/// + +// @esModuleInterop: true + +// @Filename: /mod.ts +//// const foo = 0; +//// export = foo; + +// @Filename: /importExportEquals.ts +//// [|import f/**/|] + +verify.completions({ + marker: "", + exact: [{ + name: "foo", + source: "./mod", + insertText: `import foo$1 from "./mod";`, // <-- default import + isSnippet: true, + replacementSpan: test.ranges()[0], + sourceDisplay: "./mod", + }], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } +}); diff --git a/tests/cases/fourslash/importStatementCompletions_quotes.ts b/tests/cases/fourslash/importStatementCompletions_quotes.ts new file mode 100644 index 0000000000000..626953ec987bb --- /dev/null +++ b/tests/cases/fourslash/importStatementCompletions_quotes.ts @@ -0,0 +1,24 @@ +/// + +// @Filename: /mod.ts +//// export const foo = 0; + +// @Filename: /single.ts +//// import * as fs from 'fs'; +//// [|import f/**/|] + +verify.completions({ + marker: "", + exact: [{ + name: "foo", + source: "./mod", + insertText: `import { foo$1 } from './mod';`, // <-- single quotes + isSnippet: true, + replacementSpan: test.ranges()[0], + sourceDisplay: "./mod", + }], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } +}); diff --git a/tests/cases/fourslash/importStatementCompletions_semicolons.ts b/tests/cases/fourslash/importStatementCompletions_semicolons.ts new file mode 100644 index 0000000000000..96f00751149ed --- /dev/null +++ b/tests/cases/fourslash/importStatementCompletions_semicolons.ts @@ -0,0 +1,24 @@ +/// + +// @Filename: /mod.ts +//// export const foo = 0; + +// @Filename: /noSemicolons.ts +//// import * as fs from "fs" +//// [|import f/**/|] + +verify.completions({ + marker: "", + exact: [{ + name: "foo", + source: "./mod", + insertText: `import { foo$1 } from "./mod"`, // <-- no semicolon + isSnippet: true, + replacementSpan: test.ranges()[0], + sourceDisplay: "./mod", + }], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } +}); From 6e6c8ff30b64fbfcb9f7a73ffae77132487e9c02 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 11 Mar 2021 16:30:40 -0800 Subject: [PATCH 16/35] Update baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 11 +++++++++++ tests/baselines/reference/api/typescript.d.ts | 2 ++ 2 files changed, 13 insertions(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a5003aacef1ab..9bf82b1b6bdee 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3874,6 +3874,7 @@ declare namespace ts { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsForImportStatements?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; @@ -6118,6 +6119,7 @@ declare namespace ts { kindModifiers?: string; sortText: string; insertText?: string; + isSnippet?: true; /** * An optional span that indicates the text to be replaced by this completion item. * If present, this span should be used instead of the default one. @@ -8251,6 +8253,10 @@ declare namespace ts.server.protocol { * coupled with `replacementSpan` to replace a dotted access with a bracket access. */ insertText?: string; + /** + * `insertText` should be interpreted as a snippet if true. + */ + isSnippet?: true; /** * An optional span that indicates the text to be replaced by this completion item. * If present, this span should be used instead of the default one. @@ -9133,6 +9139,11 @@ declare namespace ts.server.protocol { * This affects lone identifier completions but not completions on the right hand side of `obj.`. */ readonly includeCompletionsForModuleExports?: boolean; + /** + * Enables auto-import-style completions on partially-typed import statements. E.g., allows + * `import write|` to be completed to `import { writeFile } from "fs"`. + */ + readonly includeCompletionsForImportStatements?: boolean; /** * If enabled, the completion list will include completions with invalid identifier names. * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 75ef6dd2ca583..addfad8567772 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3874,6 +3874,7 @@ declare namespace ts { readonly disableSuggestions?: boolean; readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsForImportStatements?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; @@ -6118,6 +6119,7 @@ declare namespace ts { kindModifiers?: string; sortText: string; insertText?: string; + isSnippet?: true; /** * An optional span that indicates the text to be replaced by this completion item. * If present, this span should be used instead of the default one. From 6382ece96bd7f34fc69f37eea5fc181d28e80990 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 12 Mar 2021 14:35:02 -0800 Subject: [PATCH 17/35] Fix pattern ambient modules appearing in auto imports --- src/services/codefixes/importFixes.ts | 4 +++- ...rtStatementCompletions_noPatternAmbient.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index e36a0f181f88f..281805c27c8bc 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1055,7 +1055,9 @@ namespace ts.codefix { function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) { for (const ambient of checker.getAmbientModules()) { - cb(ambient, /*sourceFile*/ undefined); + if (!stringContains(ambient.name, "*")) { + cb(ambient, /*sourceFile*/ undefined); + } } for (const sourceFile of allSourceFiles) { if (isExternalOrCommonJsModule(sourceFile)) { diff --git a/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts b/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts new file mode 100644 index 0000000000000..5189d080ab02e --- /dev/null +++ b/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts @@ -0,0 +1,19 @@ +/// + +// @Filename: /types.d.ts +//// declare module "*.css" { +//// const styles: any; +//// export = styles; +//// } + +// @Filename: /index.ts +//// import style/**/ + +verify.completions({ + marker: "", + exact: [], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + } +}); From 409fa900da5745e9c07254f8e5ad540ac23fab58 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 12 Mar 2021 16:16:58 -0800 Subject: [PATCH 18/35] Fix tests --- src/harness/fourslashImpl.ts | 6 ++++-- src/server/protocol.ts | 3 ++- src/services/completions.ts | 12 +++++++++++- src/services/types.ts | 7 +++++-- src/testRunner/unittests/tsserver/completions.ts | 2 ++ .../unittests/tsserver/partialSemanticServer.ts | 2 ++ tests/baselines/reference/api/tsserverlibrary.d.ts | 9 +++++++-- tests/baselines/reference/api/typescript.d.ts | 6 +++++- 8 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 212e25bd612ce..9eaaa5e911f83 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -933,7 +933,7 @@ namespace FourSlash { assert.equal(actual.isSnippet, expected.isSnippet, `Expected 'isSnippet' properties to match`); assert.equal(actual.source, expected.source, `Expected 'source' values to match`); assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, `Expected 'sortText' properties to match`); - if (expected.sourceDisplay) { + if (expected.sourceDisplay && actual.sourceDisplay) { assert.equal(ts.displayPartsToString(actual.sourceDisplay), expected.sourceDisplay, `Expected 'sourceDisplay' properties to match`); } @@ -945,7 +945,9 @@ namespace FourSlash { // assert.equal(actualDetails.kind, actual.kind); assert.equal(actualDetails.kindModifiers, actual.kindModifiers, "Expected 'kindModifiers' properties to match"); assert.equal(actualDetails.source && ts.displayPartsToString(actualDetails.source), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'source' display parts string"); - assert.equal(actualDetails.sourceDisplay && ts.displayPartsToString(actualDetails.sourceDisplay), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'sourceDisplay' display parts string"); + if (!actual.sourceDisplay) { + assert.equal(actualDetails.sourceDisplay && ts.displayPartsToString(actualDetails.sourceDisplay), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'sourceDisplay' display parts string"); + } assert.deepEqual(actualDetails.tags, expected.tags); } else { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 8fe1c6e956962..daa411831ed46 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2127,7 +2127,7 @@ namespace ts.server.protocol { arguments: FormatOnKeyRequestArgs; } - export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#"; + export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " "; /** * Arguments for completions messages. @@ -2345,6 +2345,7 @@ namespace ts.server.protocol { * must be used to commit that completion entry. */ readonly optionalReplacementSpan?: TextSpan; + readonly isIncomplete?: boolean; readonly entries: readonly CompletionEntry[]; } diff --git a/src/services/completions.ts b/src/services/completions.ts index e16b062b7839a..befd60e474485 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -139,6 +139,14 @@ namespace ts.Completions { return undefined; } + if (triggerCharacter === " ") { + // `isValidTrigger` ensures we are at `import |` + if (!(preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText)) { + return undefined; + } + return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; + } + const stringCompletions = StringCompletions.getStringLiteralCompletions(sourceFile, position, contextToken, typeChecker, compilerOptions, host, log, preferences); if (stringCompletions) { return stringCompletions; @@ -1087,8 +1095,8 @@ namespace ts.Completions { let isJsxInitializer: IsJsxInitializer = false; let isJsxIdentifierExpected = false; let importCompletionNode: Node | undefined; - let location = getTouchingPropertyName(sourceFile, position); + if (contextToken) { // Import statement completions use `insertText`, and also require the `data` property of `CompletionEntryIdentifier` // added in TypeScript 4.3 to be sent back from the client during `getCompletionEntryDetails`. Since this feature @@ -2830,6 +2838,8 @@ namespace ts.Completions { return !!contextToken && (isStringLiteralLike(contextToken) ? !!tryGetImportFromModuleSpecifier(contextToken) : contextToken.kind === SyntaxKind.SlashToken && isJsxClosingElement(contextToken.parent)); + case " ": + return !!contextToken && isImportKeyword(contextToken) && contextToken.parent.kind === SyntaxKind.SourceFile; default: return Debug.assertNever(triggerCharacter); } diff --git a/src/services/types.ts b/src/services/types.ts index 0226cd1f47565..9f2ee7449612d 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -550,7 +550,7 @@ namespace ts { export type OrganizeImportsScope = CombinedCodeFixScope; - export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#"; + export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " "; export interface GetCompletionsAtPositionOptions extends UserPreferences { /** @@ -1131,11 +1131,14 @@ namespace ts { * must be used to commit that completion entry. */ optionalReplacementSpan?: TextSpan; - /** * true when the current location also allows for a new identifier */ isNewIdentifierLocation: boolean; + /** + * Indicates to client to continue requesting completions on subsequent keystrokes. + */ + isIncomplete?: true; entries: CompletionEntry[]; } diff --git a/src/testRunner/unittests/tsserver/completions.ts b/src/testRunner/unittests/tsserver/completions.ts index 651e79576993c..2b9d543a09236 100644 --- a/src/testRunner/unittests/tsserver/completions.ts +++ b/src/testRunner/unittests/tsserver/completions.ts @@ -39,6 +39,8 @@ namespace ts.projectSystem { isPackageJsonImport: undefined, sortText: Completions.SortText.AutoImportSuggestions, source: "/a", + sourceDisplay: undefined, + isSnippet: undefined, data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined, moduleSpecifier: undefined } }; assert.deepEqual(response, { diff --git a/src/testRunner/unittests/tsserver/partialSemanticServer.ts b/src/testRunner/unittests/tsserver/partialSemanticServer.ts index d7af24e3167bd..edede23aee3b0 100644 --- a/src/testRunner/unittests/tsserver/partialSemanticServer.ts +++ b/src/testRunner/unittests/tsserver/partialSemanticServer.ts @@ -72,6 +72,8 @@ import { something } from "something"; replacementSpan: undefined, source: undefined, data: undefined, + sourceDisplay: undefined, + isSnippet: undefined, }; } }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 9bf82b1b6bdee..fb41b1757aee8 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -5614,7 +5614,7 @@ declare namespace ts { fileName: string; } type OrganizeImportsScope = CombinedCodeFixScope; - type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#"; + type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " "; interface GetCompletionsAtPositionOptions extends UserPreferences { /** * If the editor is asking for completions because a certain character was typed @@ -6094,6 +6094,10 @@ declare namespace ts { * true when the current location also allows for a new identifier */ isNewIdentifierLocation: boolean; + /** + * Indicates to client to continue requesting completions on subsequent keystrokes. + */ + isIncomplete?: true; entries: CompletionEntry[]; } interface CompletionEntryData { @@ -8156,7 +8160,7 @@ declare namespace ts.server.protocol { command: CommandTypes.Formatonkey; arguments: FormatOnKeyRequestArgs; } - type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#"; + type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " "; /** * Arguments for completions messages. */ @@ -8357,6 +8361,7 @@ declare namespace ts.server.protocol { * must be used to commit that completion entry. */ readonly optionalReplacementSpan?: TextSpan; + readonly isIncomplete?: boolean; readonly entries: readonly CompletionEntry[]; } interface CompletionDetailsResponse extends Response { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index addfad8567772..0199506618c98 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -5614,7 +5614,7 @@ declare namespace ts { fileName: string; } type OrganizeImportsScope = CombinedCodeFixScope; - type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#"; + type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " "; interface GetCompletionsAtPositionOptions extends UserPreferences { /** * If the editor is asking for completions because a certain character was typed @@ -6094,6 +6094,10 @@ declare namespace ts { * true when the current location also allows for a new identifier */ isNewIdentifierLocation: boolean; + /** + * Indicates to client to continue requesting completions on subsequent keystrokes. + */ + isIncomplete?: true; entries: CompletionEntry[]; } interface CompletionEntryData { From 2ae85af6fa3c5a48782277012bb9fd29a2aabc15 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 15 Mar 2021 11:03:59 -0700 Subject: [PATCH 19/35] Remove commented code --- src/services/completions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index befd60e474485..713452f9c2f49 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1238,7 +1238,6 @@ namespace ts.Completions { const symbolToOriginInfoMap: SymbolOriginInfoMap = []; const symbolToSortTextMap: SymbolSortTextMap = []; const seenPropertySymbols = new Map(); - // const importSuggestionsCache = host.getImportSuggestionsCache && host.getImportSuggestionsCache(); const isTypeOnly = isTypeOnlyCompletion(); if (isRightOfDot || isRightOfQuestionDot) { From 77b6e3d4e02a59c39493c8ce657836b23321769b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 15 Mar 2021 11:05:12 -0700 Subject: [PATCH 20/35] Switch to valueDeclaration for getting module source file --- src/services/completions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 713452f9c2f49..5441c8e07f5f1 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1459,7 +1459,7 @@ namespace ts.Completions { isDefaultExport: false, symbolName: firstAccessibleSymbol.name, exportName: firstAccessibleSymbol.name, - fileName: isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? cast(moduleSymbol.declarations![0], isSourceFile).fileName : undefined, + fileName: isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? cast(moduleSymbol.valueDeclaration, isSourceFile).fileName : undefined, }; symbolToOriginInfoMap[index] = origin; } From 5388b3078647f29df5aa821c4d72e5df90eaa3a3 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 16 Mar 2021 16:06:18 -0700 Subject: [PATCH 21/35] Small optimizations --- src/compiler/checker.ts | 4 +++- src/services/codefixes/importFixes.ts | 8 ++++---- src/services/completions.ts | 5 +++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1631d1e95ec9b..ec48a960a2ee8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3526,7 +3526,9 @@ namespace ts { function shouldTreatPropertiesOfExternalModuleAsExports(resolvedExternalModuleType: Type) { return !(resolvedExternalModuleType.flags & TypeFlags.Primitive || getObjectFlags(resolvedExternalModuleType) & ObjectFlags.Class || - isArrayOrTupleLikeType(resolvedExternalModuleType)); + // `isArrayOrTupleLikeType` is too expensive to use in this auto-imports hot path + isArrayType(resolvedExternalModuleType) || + isTupleType(resolvedExternalModuleType)); } function getExportsOfSymbol(symbol: Symbol): SymbolTable { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 281805c27c8bc..8cc4cd6ac8bd3 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -241,8 +241,8 @@ namespace ts.codefix { if (file !== fileName) { return undefined; } - if (version) { - return projectVersion === version ? cache : undefined; + if (version && projectVersion === version) { + return cache; } cache?.forEach(infos => { for (const info of infos) { @@ -738,10 +738,10 @@ namespace ts.codefix { } function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined { + const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); + if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol); if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default }; - const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol); - return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, exportKind: ExportKind.ExportEquals }; } function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind { diff --git a/src/services/completions.ts b/src/services/completions.ts index 5441c8e07f5f1..3302603b6839f 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1695,7 +1695,7 @@ namespace ts.Completions { const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*filterByPackageJson*/ !detailsEntryId); exportInfo.forEach((info, key) => { - const [symbolName] = key.split("|"); + const symbolName = key.substring(0, key.indexOf("|")); if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); if (isCompletionDetailsMatch || stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { @@ -1745,7 +1745,8 @@ namespace ts.Completions { } let characterIndex = 0; - for (let strIndex = 0; strIndex < str.length; strIndex++) { + const len = str.length; + for (let strIndex = 0; strIndex < len; strIndex++) { if (str.charCodeAt(strIndex) === characters.charCodeAt(characterIndex)) { characterIndex++; if (characterIndex === characters.length) { From 99ec664a130000e6944daf530bf0d29cffdc1b40 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 19 Mar 2021 12:21:49 -0700 Subject: [PATCH 22/35] Cache module specifiers / importableness and export map separately --- src/compiler/moduleSpecifiers.ts | 20 +- src/compiler/types.ts | 16 + src/server/project.ts | 102 ++---- src/server/scriptInfo.ts | 10 +- src/services/codefixes/importFixes.ts | 338 +++--------------- src/services/completions.ts | 58 ++- src/services/types.ts | 4 +- src/services/utilities.ts | 328 ++++++++++++++++- .../tsserver/importSuggestionsCache.ts | 12 +- 9 files changed, 499 insertions(+), 389 deletions(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 6d4eb03719ca1..7b2e6edff610c 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -324,19 +324,17 @@ namespace ts.moduleSpecifiers { : undefined); } - interface ModulePath { - path: string; - isInNodeModules: boolean; - isRedirect: boolean; - } - /** * Looks for existing imports that use symlinks to this module. * Symlinks will be returned first so they are preferred over the real path. */ - function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly ModulePath[] { - const cwd = host.getCurrentDirectory(); + function getAllModulePaths(importingFileName: Path, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly ModulePath[] { + const cache = host.getModuleSpecifierCache?.(); const getCanonicalFileName = hostGetCanonicalFileName(host); + if (cache) { + const cached = cache.get(importingFileName, toPath(importedFileName, host.getCurrentDirectory(), getCanonicalFileName)); + if (typeof cached === "object") return cached; + } const allFileNames = new Map(); let importedFileFromNodeModules = false; forEachFileNameOfModule( @@ -355,7 +353,7 @@ namespace ts.moduleSpecifiers { // Sort by paths closest to importing file Name directory const sortedPaths: ModulePath[] = []; for ( - let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName)); + let directory = getDirectoryPath(importingFileName); allFileNames.size !== 0; ) { const directoryStart = ensureTrailingDirectorySeparator(directory); @@ -381,6 +379,10 @@ namespace ts.moduleSpecifiers { if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators); sortedPaths.push(...remainingPaths); } + + if (cache) { + cache.set(importingFileName, toPath(importedFileName, host.getCurrentDirectory(), getCanonicalFileName), sortedPaths); + } return sortedPaths; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 02ef982eda0f2..533fd42e9b1ee 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -7954,6 +7954,7 @@ namespace ts { readFile?(path: string): string | undefined; realpath?(path: string): string; getSymlinkCache?(): SymlinkCache; + getModuleSpecifierCache?(): ModuleSpecifierCache; getGlobalTypingsCacheLocation?(): string | undefined; getNearestAncestorDirectoryWithPackageJson?(fileName: string, rootDir?: string): string | undefined; @@ -7964,6 +7965,21 @@ namespace ts { getFileIncludeReasons(): MultiMap; } + /* @internal */ + export interface ModulePath { + path: string; + isInNodeModules: boolean; + isRedirect: boolean; + } + + /* @internal */ + export interface ModuleSpecifierCache { + get(fromFileName: Path, toFileName: Path): boolean | readonly ModulePath[] | undefined; + set(fromFileName: Path, toFileName: Path, moduleSpecifiers: boolean | readonly ModulePath[]): void; + clear(): void; + count(): number; + } + // Note: this used to be deprecated in our public API, but is still used internally /* @internal */ export interface SymbolTracker { diff --git a/src/server/project.ts b/src/server/project.ts index 3244bf075d35d..b273e37183bf3 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -146,6 +146,8 @@ namespace ts.server { lastCachedUnresolvedImportsList: SortedReadonlyArray | undefined; /*@internal*/ private hasAddedorRemovedFiles = false; + /*@internal*/ + private hasAddedOrRemovedSymlinks = false; /*@internal*/ lastFileExceededProgramSize: string | undefined; @@ -245,9 +247,11 @@ namespace ts.server { public readonly getCanonicalFileName: GetCanonicalFileName; /*@internal*/ - private importSuggestionsCache = codefix.createImportSuggestionsForFileCache(); + private exportMapCache = createExportMapCache(); + /*@internal*/ + private changedFilesForExportMapCache: Set | undefined; /*@internal*/ - private dirtyFilesForSuggestions: Set | undefined; + private moduleSpecifierCache = createModuleSpecifierCache(); /*@internal*/ private symlinks: SymlinkCache | undefined; /*@internal*/ @@ -976,8 +980,8 @@ namespace ts.server { /*@internal*/ markFileAsDirty(changedFile: Path) { this.markAsDirty(); - if (!this.importSuggestionsCache.isEmpty()) { - (this.dirtyFilesForSuggestions || (this.dirtyFilesForSuggestions = new Set())).add(changedFile); + if (!this.exportMapCache.isEmpty()) { + (this.changedFilesForExportMapCache || (this.changedFilesForExportMapCache = new Set())).add(changedFile); } } @@ -994,7 +998,7 @@ namespace ts.server { this.autoImportProviderHost = undefined; } this.autoImportProviderHost?.markAsDirty(); - this.importSuggestionsCache.clear(); + this.exportMapCache.clear(); } /* @internal */ @@ -1002,6 +1006,11 @@ namespace ts.server { this.hasAddedorRemovedFiles = true; } + /* @internal */ + onSymlinkAddedOrRemoved() { + this.hasAddedOrRemovedSymlinks = true; + } + /** * Updates set of files that contribute to this project * @returns: true if set of files in the project stays the same and false - otherwise. @@ -1013,6 +1022,7 @@ namespace ts.server { const hasNewProgram = this.updateGraphWorker(); const hasAddedorRemovedFiles = this.hasAddedorRemovedFiles; this.hasAddedorRemovedFiles = false; + this.hasAddedOrRemovedSymlinks = false; const changedFiles: readonly Path[] = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray; @@ -1162,27 +1172,32 @@ namespace ts.server { } } - if (!this.importSuggestionsCache.isEmpty()) { + if (!this.exportMapCache.isEmpty()) { if (this.hasAddedorRemovedFiles || oldProgram && !this.program.structureIsReused) { - this.importSuggestionsCache.clear(); + this.exportMapCache.clear(); } - else if (this.dirtyFilesForSuggestions && oldProgram && this.program) { - forEachKey(this.dirtyFilesForSuggestions, fileName => { + else if (this.changedFilesForExportMapCache && oldProgram && this.program) { + forEachKey(this.changedFilesForExportMapCache, fileName => { const oldSourceFile = oldProgram.getSourceFileByPath(fileName); const sourceFile = this.program!.getSourceFileByPath(fileName); - if (this.sourceFileHasChangedOwnImportSuggestions(oldSourceFile, sourceFile)) { - this.importSuggestionsCache.clear(); + if (!oldSourceFile || !sourceFile) { + this.exportMapCache.clear(); return true; } + return this.exportMapCache.onFileChanged(oldSourceFile, sourceFile, !!this.getTypeAcquisition().enable); }); } } - if (this.dirtyFilesForSuggestions) { - this.dirtyFilesForSuggestions.clear(); + if (this.changedFilesForExportMapCache) { + this.changedFilesForExportMapCache.clear(); } if (this.hasAddedorRemovedFiles) { - this.symlinks = undefined; + const symlinksInvalidated = this.hasAddedOrRemovedSymlinks || this.getCompilerOptions().preserveSymlinks; + if (symlinksInvalidated) { + this.symlinks = undefined; + this.moduleSpecifierCache.clear(); + } } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; @@ -1214,54 +1229,6 @@ namespace ts.server { this.projectService.sendPerformanceEvent(kind, durationMs); } - /*@internal*/ - private sourceFileHasChangedOwnImportSuggestions(oldSourceFile: SourceFile | undefined, newSourceFile: SourceFile | undefined) { - if (!oldSourceFile && !newSourceFile) { - return false; - } - // Probably shouldn’t get this far, but on the off chance the file was added or removed, - // we can’t reliably tell anything about it. - if (!oldSourceFile || !newSourceFile) { - return true; - } - - Debug.assertEqual(oldSourceFile.fileName, newSourceFile.fileName); - // If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node. - // Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list. - if (this.getTypeAcquisition().enable && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile)) { - return true; - } - - // Module agumentation and ambient module changes can add or remove exports available to be auto-imported. - // Changes elsewhere in the file can change the *type* of an export in a module augmentation, - // but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache. - if ( - !arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) || - !this.ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile) - ) { - return true; - } - return false; - } - - /*@internal*/ - private ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) { - if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) { - return false; - } - let oldFileStatementIndex = -1; - let newFileStatementIndex = -1; - for (const ambientModuleName of newSourceFile.ambientModuleNames) { - const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName; - oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1); - newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1); - if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) { - return false; - } - } - return true; - } - private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean) { const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName); if (scriptInfoToDetach) { @@ -1674,8 +1641,13 @@ namespace ts.server { } /*@internal*/ - getImportSuggestionsCache() { - return this.importSuggestionsCache; + getExportMapCache() { + return this.exportMapCache; + } + + /*@internal*/ + getModuleSpecifierCache() { + return this.moduleSpecifierCache; } /*@internal*/ @@ -2005,7 +1977,7 @@ namespace ts.server { this.projectService.setFileNamesOfAutoImportProviderProject(this, rootFileNames); this.rootFileNames = rootFileNames; - this.hostProject.getImportSuggestionsCache().clear(); + this.hostProject.getExportMapCache().clear(); return super.updateGraph(); } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index fb3928cec9470..9cc588780839b 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -391,7 +391,8 @@ namespace ts.server { return this.textStorage.getSnapshot(); } - private ensureRealPath() { + /** @returns Whether the file was a symlink */ + private ensureRealPath(): boolean { if (this.realpath === undefined) { // Default is just the path this.realpath = this.path; @@ -404,10 +405,12 @@ namespace ts.server { // If it is different from this.path, add to the map if (this.realpath !== this.path) { project.projectService.realpathToScriptInfos!.add(this.realpath, this); // TODO: GH#18217 + return true; } } } } + return false; } /*@internal*/ @@ -424,7 +427,10 @@ namespace ts.server { this.containingProjects.push(project); project.onFileAddedOrRemoved(); if (!project.getCompilerOptions().preserveSymlinks) { - this.ensureRealPath(); + const isSymlink = this.ensureRealPath(); + if (isSymlink) { + project.onSymlinkAddedOrRemoved(); + } } } return isNew; diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 8cc4cd6ac8bd3..eee24a3d9c746 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -180,90 +180,12 @@ namespace ts.codefix { readonly exportInfo?: SymbolExportInfo; } - export const enum ImportKind { - Named, - Default, - Namespace, - CommonJS, - } - - export const enum ExportKind { - Named, - Default, - ExportEquals, - UMD, - } - - /** Information about how a symbol is exported from a module. */ - export interface SymbolExportInfo { - symbol: Symbol; - moduleSymbol: Symbol; - exportKind: ExportKind; - /** If true, can't use an es6 import from a js file. */ - exportedSymbolIsTypeOnly: boolean; - /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ - isFromPackageJson: boolean; - } - /** Information needed to augment an existing import declaration. */ interface FixAddToExistingImportInfo { readonly declaration: AnyImportOrRequire; readonly importKind: ImportKind; } - export interface ImportSuggestionsForFileCache { - clear(): void; - get(fileName: string, checker: TypeChecker, projectVersion?: string): MultiMap | undefined; - set(fileName: string, suggestions: MultiMap, projectVersion?: string): void; - isEmpty(): boolean; - } - export function createImportSuggestionsForFileCache(): ImportSuggestionsForFileCache { - let cache: MultiMap | undefined; - let projectVersion: string | undefined; - let fileName: string | undefined; - return { - isEmpty() { - return !cache; - }, - clear: () => { - cache = undefined; - fileName = undefined; - projectVersion = undefined; - }, - set: (file, suggestions, version) => { - cache = suggestions; - fileName = file; - if (version) { - projectVersion = version; - } - }, - get: (file, checker, version) => { - if (file !== fileName) { - return undefined; - } - if (version && projectVersion === version) { - return cache; - } - cache?.forEach(infos => { - for (const info of infos) { - // If the symbol/moduleSymbol was a merged symbol, it will have a new identity - // in the checker, even though the symbols to merge are the same (guaranteed by - // cache invalidation in synchronizeHostData). - if (info.symbol.declarations?.length) { - info.symbol = checker.getMergedSymbol(info.exportKind === ExportKind.Default - ? info.symbol.declarations[0].localSymbol ?? info.symbol.declarations[0].symbol - : info.symbol.declarations[0].symbol); - } - if (info.moduleSymbol.declarations?.length) { - info.moduleSymbol = checker.getMergedSymbol(info.moduleSymbol.declarations[0].symbol); - } - } - }); - return cache; - }, - }; - } - export function getImportCompletionAction( exportedSymbol: Symbol, moduleSymbol: Symbol, @@ -287,7 +209,7 @@ namespace ts.codefix { function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) { Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol"); - return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, program, host); + return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, host); } function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction { @@ -318,7 +240,11 @@ namespace ts.codefix { function getAllReExportingModules(importingFile: SourceFile, exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean): readonly SymbolExportInfo[] { const result: SymbolExportInfo[] = []; const compilerOptions = program.getCompilerOptions(); - forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ false, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => { + const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => { + return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host); + }); + + forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); // Don't import from a re-export when looking "up" like to `./index` or `../index`. if (moduleFile && moduleSymbol !== exportingModuleSymbol && startsWith(importingFile.fileName, getDirectoryPath(moduleFile.fileName))) { @@ -326,29 +252,37 @@ namespace ts.codefix { } const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); - if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol) { + if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) { result.push({ symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson }); } for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { - if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) { + if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) { result.push({ symbol: exported, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); return result; + + function isImportable(program: Program, moduleFile: SourceFile | undefined, isFromPackageJson: boolean) { + return !moduleFile || isImportableFile(program, importingFile, moduleFile, /*packageJsonFilter*/ undefined, getModuleSpecifierResolutionHost(isFromPackageJson), host.getModuleSpecifierCache?.()); + } } - export function getBestImportFixForExports(exportInfo: readonly SymbolExportInfo[], importingFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, program: Program, host: LanguageServiceHost, preferences: UserPreferences) { - return getBestFix(getNewImportFixes(program, importingFile, position, preferTypeOnlyImport, useRequire, exportInfo, host, preferences), importingFile, program, host); + export function getModuleSpecifierForBestExportInfo(exportInfo: readonly SymbolExportInfo[], + importingFile: SourceFile, + program: Program, + host: LanguageServiceHost, + preferences: UserPreferences + ): { exportInfo?: SymbolExportInfo, moduleSpecifier: string } { + return getBestFix(getNewImportFixes(program, importingFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, exportInfo, host, preferences), importingFile, host); } - export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program, filterByPackageJson: boolean) { - host.log?.(`getSymbolToExportInfoMap: starting for ${importingFile.fileName}`); + export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program) { const start = timestamp(); - const cache = host.getImportSuggestionsCache?.(); + const cache = host.getExportMapCache?.(); if (cache) { - const cached = cache.get(importingFile.fileName, program.getTypeChecker(), host.getProjectVersion?.()); + const cached = cache.get(importingFile.path, program.getTypeChecker(), host.getProjectVersion?.()); if (cached) { host.log?.("getSymbolToExportInfoMap: cache hit"); return cached; @@ -361,7 +295,7 @@ namespace ts.codefix { const result: MultiMap = createMultiMap(); const compilerOptions = program.getCompilerOptions(); const target = getEmitScriptTarget(compilerOptions); - forEachExternalModuleToImportFrom(program, host, importingFile, filterByPackageJson, /*useAutoImportProvider*/ true, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { + forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, _moduleFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo) { @@ -376,9 +310,9 @@ namespace ts.codefix { } }); - if (cache && filterByPackageJson) { + if (cache) { host.log?.("getSymbolToExportInfoMap: caching results"); - cache.set(importingFile.fileName, result, host.getProjectVersion?.()); + cache.set(result, host.getProjectVersion?.()); } host.log?.(`getSymbolToExportInfoMap: done in ${timestamp() - start} ms`); return result; @@ -580,20 +514,20 @@ namespace ts.codefix { const info = errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code ? getFixesInfoForUMDImport(context, symbolToken) : isIdentifier(symbolToken) ? getFixesInfoForNonUMDImport(context, symbolToken, useAutoImportProvider) : undefined; - return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.program, context.host) }; + return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.host) }; } - function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, program: Program, host: LanguageServiceHost): readonly ImportFix[] { - const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host); + function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, host: LanguageServiceHost): readonly ImportFix[] { + const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, host); return sort(fixes, (a, b) => compareValues(a.kind, b.kind) || compareModuleSpecifiers(a, b, allowsImportingSpecifier)); } - function getBestFix(fixes: readonly T[], sourceFile: SourceFile, program: Program, host: LanguageServiceHost): T { + function getBestFix(fixes: readonly T[], sourceFile: SourceFile, host: LanguageServiceHost): T { // These will always be placed first if available, and are better than other kinds if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) { return fixes[0]; } - const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host); + const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, host); return fixes.reduce((best, fix) => compareModuleSpecifiers(fix, best, allowsImportingSpecifier) === Comparison.LessThan ? fix : best ); @@ -699,7 +633,7 @@ namespace ts.codefix { symbolName: string, currentTokenMeaning: SemanticMeaning, cancellationToken: CancellationToken, - sourceFile: SourceFile, + fromFile: SourceFile, program: Program, useAutoImportProvider: boolean, host: LanguageServiceHost @@ -707,23 +641,34 @@ namespace ts.codefix { // For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once. // Maps symbol id to info for modules providing that symbol (original export + re-exports). const originalSymbolToExportInfos = createMultiMap(); - function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, exportKind: ExportKind, checker: TypeChecker, isFromPackageJson: boolean): void { - originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); + const packageJsonFilter = createPackageJsonImportFilter(fromFile, host); + const moduleSpecifierCache = host.getModuleSpecifierCache?.(); + const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => { + return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host); + }); + function addSymbol(moduleSymbol: Symbol, toFile: SourceFile | undefined, exportedSymbol: Symbol, exportKind: ExportKind, program: Program, isFromPackageJson: boolean): void { + const moduleSpecifierResolutionHost = getModuleSpecifierResolutionHost(isFromPackageJson); + if (toFile && isImportableFile(program, fromFile, toFile, packageJsonFilter, moduleSpecifierResolutionHost, moduleSpecifierCache) || + !toFile && packageJsonFilter.allowsImportingAmbientModule(moduleSymbol, moduleSpecifierResolutionHost) + ) { + const checker = program.getTypeChecker(); + originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); + } } - forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _, program, isFromPackageJson) => { + forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => { const checker = program.getTypeChecker(); cancellationToken.throwIfCancellationRequested(); const compilerOptions = program.getCompilerOptions(); const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions); if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) { - addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.exportKind, checker, isFromPackageJson); + addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson); } // check exports with the same name const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { - addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ExportKind.Named, checker, isFromPackageJson); + addSymbol(moduleSymbol, sourceFile, exportSymbolWithIdenticalName, ExportKind.Named, program, isFromPackageJson); } }); return originalSymbolToExportInfos; @@ -1005,54 +950,18 @@ namespace ts.codefix { export function forEachExternalModuleToImportFrom( program: Program, host: LanguageServiceHost, - from: SourceFile, - filterByPackageJson: boolean, useAutoImportProvider: boolean, cb: (module: Symbol, moduleFile: SourceFile | undefined, program: Program, isFromPackageJson: boolean) => void, ) { - forEachExternalModuleToImportFromInProgram(program, host, from, filterByPackageJson, (module, file) => cb(module, file, program, /*isFromPackageJson*/ false)); + forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false)); const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.(); if (autoImportProvider) { const start = timestamp(); - forEachExternalModuleToImportFromInProgram(autoImportProvider, host, from, filterByPackageJson, (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true)); + forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true)); host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`); } } - function forEachExternalModuleToImportFromInProgram( - program: Program, - host: LanguageServiceHost, - from: SourceFile, - filterByPackageJson: boolean, - cb: (module: Symbol, moduleFile: SourceFile | undefined) => void, - ) { - let filteredCount = 0; - const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); - const packageJson = filterByPackageJson && createAutoImportFilter(from, program, host, moduleSpecifierResolutionHost); - forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, sourceFile) => { - if (sourceFile === undefined) { - if (!packageJson || packageJson.allowsImportingAmbientModule(module)) { - cb(module, sourceFile); - } - else if (packageJson) { - filteredCount++; - } - } - else if (sourceFile && - sourceFile !== from && - isImportableFile(program, from, sourceFile, moduleSpecifierResolutionHost) - ) { - if (!packageJson || packageJson.allowsImportingSourceFile(sourceFile)) { - cb(module, sourceFile); - } - else if (packageJson) { - filteredCount++; - } - } - }); - host.log?.(`forEachExternalModuleToImportFrom: filtered out ${filteredCount} modules by package.json contents`); - } - function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) { for (const ambient of checker.getAmbientModules()) { if (!stringContains(ambient.name, "*")) { @@ -1066,42 +975,6 @@ namespace ts.codefix { } } - function isImportableFile( - program: Program, - from: SourceFile, - to: SourceFile, - moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost - ) { - const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost); - const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.(); - return !!moduleSpecifiers.forEachFileNameOfModule( - from.fileName, - to.fileName, - moduleSpecifierResolutionHost, - /*preferSymlinks*/ false, - toPath => { - const toFile = program.getSourceFile(toPath); - // Determine to import using toPath only if toPath is what we were looking at - // or there doesnt exist the file in the program by the symlink - return (toFile === to || !toFile) && - isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache); - } - ); - } - - /** - * Don't include something from a `node_modules` that isn't actually reachable by a global import. - * A relative import to node_modules is usually a bad idea. - */ - function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: GetCanonicalFileName, globalCachePath?: string): boolean { - // If it's in a `node_modules` but is not reachable from here via a global import, don't bother. - const toNodeModules = forEachAncestorDirectory(toPath, ancestor => getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined); - const toNodeModulesParent = toNodeModules && getDirectoryPath(getCanonicalFileName(toNodeModules)); - return toNodeModulesParent === undefined - || startsWith(getCanonicalFileName(fromPath), toNodeModulesParent) - || (!!globalCachePath && startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent)); - } - export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined): string { return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target); } @@ -1132,117 +1005,4 @@ namespace ts.codefix { // Need `|| "_"` to ensure result isn't empty. return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`; } - - function createAutoImportFilter(fromFile: SourceFile, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host)) { - const packageJsons = ( - (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) - ).filter(p => p.parseable); - - let usesNodeCoreModules: boolean | undefined; - return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier, moduleSpecifierResolutionHost }; - - function moduleSpecifierIsCoveredByPackageJson(specifier: string) { - const packageName = getNodeModuleRootSpecifier(specifier); - for (const packageJson of packageJsons) { - if (packageJson.has(packageName) || packageJson.has(getTypesPackageName(packageName))) { - return true; - } - } - return false; - } - - function allowsImportingAmbientModule(moduleSymbol: Symbol): boolean { - if (!packageJsons.length) { - return true; - } - - const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile(); - const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName); - if (typeof declaringNodeModuleName === "undefined") { - return true; - } - - const declaredModuleSpecifier = stripQuotes(moduleSymbol.getName()); - if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) { - return true; - } - - return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName) - || moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier); - } - - function allowsImportingSourceFile(sourceFile: SourceFile): boolean { - if (!packageJsons.length) { - return true; - } - - const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName); - if (!moduleSpecifier) { - return true; - } - - return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); - } - - /** - * Use for a specific module specifier that has already been resolved. - * Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve - * the best module specifier for a given module _and_ determine if it’s importable. - */ - function allowsImportingSpecifier(moduleSpecifier: string) { - if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) { - return true; - } - if (pathIsRelative(moduleSpecifier) || isRootedDiskPath(moduleSpecifier)) { - return true; - } - return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); - } - - function isAllowedCoreNodeModulesImport(moduleSpecifier: string) { - // If we’re in JavaScript, it can be difficult to tell whether the user wants to import - // from Node core modules or not. We can start by seeing if the user is actually using - // any node core modules, as opposed to simply having @types/node accidentally as a - // dependency of a dependency. - if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { - if (usesNodeCoreModules === undefined) { - usesNodeCoreModules = consumesNodeCoreModules(fromFile); - } - if (usesNodeCoreModules) { - return true; - } - } - return false; - } - - function getNodeModulesPackageNameFromFileName(importedFileName: string): string | undefined { - if (!stringContains(importedFileName, "node_modules")) { - return undefined; - } - const specifier = moduleSpecifiers.getNodeModulesPackageName( - host.getCompilationSettings(), - fromFile.path, - importedFileName, - moduleSpecifierResolutionHost, - ); - - if (!specifier) { - return undefined; - } - // Paths here are not node_modules, so we don’t care about them; - // returning anything will trigger a lookup in package.json. - if (!pathIsRelative(specifier) && !isRootedDiskPath(specifier)) { - return getNodeModuleRootSpecifier(specifier); - } - } - - function getNodeModuleRootSpecifier(fullSpecifier: string): string { - const components = getPathComponents(getPackageNameFromTypesPackageName(fullSpecifier)).slice(1); - // Scoped packages - if (startsWith(components[0], "@")) { - return `${components[0]}/${components[1]}`; - } - return components[0]; - } - } } diff --git a/src/services/completions.ts b/src/services/completions.ts index 3302603b6839f..c29ec35bdf10d 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -506,16 +506,16 @@ namespace ts.Completions { const replacementSpan = createTextSpanFromNode(importCompletionNode, sourceFile); const quotedModuleSpecifier = quote(sourceFile, preferences, origin.moduleSpecifier); const exportKind = - origin.isDefaultExport ? codefix.ExportKind.Default : - origin.exportName === InternalSymbolName.ExportEquals ? codefix.ExportKind.ExportEquals : - codefix.ExportKind.Named; + origin.isDefaultExport ? ExportKind.Default : + origin.exportName === InternalSymbolName.ExportEquals ? ExportKind.ExportEquals : + ExportKind.Named; const importKind = codefix.getImportKind(sourceFile, exportKind, options); const suffix = useSemicolons ? ";" : ""; switch (importKind) { - case codefix.ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name}$1 = require(${quotedModuleSpecifier})${suffix}` }; - case codefix.ImportKind.Default: return { replacementSpan, insertText: `import ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; - case codefix.ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; - case codefix.ImportKind.Named: return { replacementSpan, insertText: `import { ${name}$1 } from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name}$1 = require(${quotedModuleSpecifier})${suffix}` }; + case ImportKind.Default: return { replacementSpan, insertText: `import ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.Named: return { replacementSpan, insertText: `import { ${name}$1 } from ${quotedModuleSpecifier}${suffix}` }; } } @@ -1239,6 +1239,9 @@ namespace ts.Completions { const symbolToSortTextMap: SymbolSortTextMap = []; const seenPropertySymbols = new Map(); const isTypeOnly = isTypeOnlyCompletion(); + const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => { + return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host); + }); if (isRightOfDot || isRightOfQuestionDot) { getTypeScriptMemberSymbols(); @@ -1691,29 +1694,35 @@ namespace ts.Completions { if (!shouldOfferImportCompletions()) return; Debug.assert(!detailsEntryId?.data); const start = timestamp(); + const moduleSpecifierCache = host.getModuleSpecifierCache?.(); host.log?.(`collectAutoImports: starting, ${resolveModuleSpecifiers ? "" : "not "}resolving module specifiers`); + if (moduleSpecifierCache) { + host.log?.(`collectAutoImports: module specifier cache size: ${moduleSpecifierCache.count()}`); + } const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : ""; - const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program, /*filterByPackageJson*/ !detailsEntryId); + const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program); + const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.(); + const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, host); exportInfo.forEach((info, key) => { const symbolName = key.substring(0, key.indexOf("|")); if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); if (isCompletionDetailsMatch || stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { - // If we don't need to resolve module specifiers, it doesn't matter which SymbolExportInfo - // we use. Each is importable by the same name and resolves to the same declaration. + // If we don't need to resolve module specifiers, we can use any re-export that is importable at all + // (We need to ensure that at least one is importable to show a completion.) const { moduleSpecifier, exportInfo } = resolveModuleSpecifiers - ? codefix.getBestImportFixForExports(info, sourceFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, program, host, preferences) - : { moduleSpecifier: undefined, exportInfo: info[0] }; + ? codefix.getModuleSpecifierForBestExportInfo(info, sourceFile, program, host, preferences) + : { moduleSpecifier: undefined, exportInfo: find(info, isImportableExportInfo) }; if (!exportInfo) return; - const isDefaultExport = exportInfo.exportKind === codefix.ExportKind.Default; + const moduleFile = tryCast(exportInfo.moduleSymbol.valueDeclaration, isSourceFile); + const isDefaultExport = exportInfo.exportKind === ExportKind.Default; const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; - const isAmbientModule = !isExternalModuleNameRelative(stripQuotes(exportInfo.moduleSymbol.name)); pushAutoImportSymbol(symbol, { kind: resolveModuleSpecifiers ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, moduleSpecifier, symbolName, - exportName: exportInfo.exportKind === codefix.ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name, - fileName: isAmbientModule ? undefined : cast(exportInfo.moduleSymbol.valueDeclaration, isSourceFile).fileName, + exportName: exportInfo.exportKind === ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name, + fileName: moduleFile?.fileName, isDefaultExport, moduleSymbol: exportInfo.moduleSymbol, isFromPackageJson: exportInfo.isFromPackageJson, @@ -1721,8 +1730,25 @@ namespace ts.Completions { } }); host.log?.(`collectAutoImports: done in ${timestamp() - start} ms`); + + function isImportableExportInfo(info: SymbolExportInfo) { + const moduleFile = tryCast(info.moduleSymbol.valueDeclaration, isSourceFile); + if (!moduleFile) { + return packageJsonFilter + ? packageJsonFilter.allowsImportingAmbientModule(info.moduleSymbol, getModuleSpecifierResolutionHost(info.isFromPackageJson)) + : true; + } + return isImportableFile( + info.isFromPackageJson ? packageJsonAutoImportProvider! : program, + sourceFile, + moduleFile, + packageJsonFilter, + getModuleSpecifierResolutionHost(info.isFromPackageJson), + moduleSpecifierCache); + } } + function pushAutoImportSymbol(symbol: Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { const symbolId = getSymbolId(symbol); if (symbolToSortTextMap[symbolId] === SortText.GlobalsOrKeywords) { diff --git a/src/services/types.ts b/src/services/types.ts index 9f2ee7449612d..0b41fb8f30fa5 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -305,7 +305,9 @@ namespace ts { /* @internal */ getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[]; /* @internal */ - getImportSuggestionsCache?(): codefix.ImportSuggestionsForFileCache; + getExportMapCache?(): ExportMapCache; + /* @internal */ + getModuleSpecifierCache?(): ModuleSpecifierCache; /* @internal */ setCompilerHost?(host: CompilerHost): void; /* @internal */ diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 74315895927ed..e3247c2644628 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1821,7 +1821,7 @@ namespace ts { } export function createModuleSpecifierResolutionHost(program: Program, host: LanguageServiceHost): ModuleSpecifierResolutionHost { - // Mix in `getProbableSymlinks` from Program when host doesn't have it + // Mix in `getSymlinkCache` from Program when host doesn't have it // in order for non-Project hosts to have a symlinks cache. return { fileExists: fileName => program.fileExists(fileName), @@ -1829,6 +1829,7 @@ namespace ts { readFile: maybeBind(host, host.readFile), useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames), getSymlinkCache: maybeBind(host, host.getSymlinkCache) || program.getSymlinkCache, + getModuleSpecifierCache: maybeBind(host, host.getModuleSpecifierCache), getGlobalTypingsCacheLocation: maybeBind(host, host.getGlobalTypingsCacheLocation), getSourceFiles: () => program.getSourceFiles(), redirectTargetsMap: program.redirectTargetsMap, @@ -2808,6 +2809,125 @@ namespace ts { } } + export interface PackageJsonImportFilter { + allowsImportingAmbientModule: (moduleSymbol: Symbol, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost) => boolean; + allowsImportingSourceFile: (sourceFile: SourceFile, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost) => boolean; + /** + * Use for a specific module specifier that has already been resolved. + * Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve + * the best module specifier for a given module _and_ determine if it’s importable. + */ + allowsImportingSpecifier: (moduleSpecifier: string) => boolean; + } + + export function createPackageJsonImportFilter(fromFile: SourceFile, host: LanguageServiceHost): PackageJsonImportFilter { + const packageJsons = ( + (host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host) + ).filter(p => p.parseable); + + let usesNodeCoreModules: boolean | undefined; + return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier }; + + function moduleSpecifierIsCoveredByPackageJson(specifier: string) { + const packageName = getNodeModuleRootSpecifier(specifier); + for (const packageJson of packageJsons) { + if (packageJson.has(packageName) || packageJson.has(getTypesPackageName(packageName))) { + return true; + } + } + return false; + } + + function allowsImportingAmbientModule(moduleSymbol: Symbol, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost): boolean { + if (!packageJsons.length) { + return true; + } + + const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile(); + const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName, moduleSpecifierResolutionHost); + if (typeof declaringNodeModuleName === "undefined") { + return true; + } + + const declaredModuleSpecifier = stripQuotes(moduleSymbol.getName()); + if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) { + return true; + } + + return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName) + || moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier); + } + + function allowsImportingSourceFile(sourceFile: SourceFile, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost): boolean { + if (!packageJsons.length) { + return true; + } + + const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName, moduleSpecifierResolutionHost); + if (!moduleSpecifier) { + return true; + } + + return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); + } + + function allowsImportingSpecifier(moduleSpecifier: string) { + if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) { + return true; + } + if (pathIsRelative(moduleSpecifier) || isRootedDiskPath(moduleSpecifier)) { + return true; + } + return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier); + } + + function isAllowedCoreNodeModulesImport(moduleSpecifier: string) { + // If we’re in JavaScript, it can be difficult to tell whether the user wants to import + // from Node core modules or not. We can start by seeing if the user is actually using + // any node core modules, as opposed to simply having @types/node accidentally as a + // dependency of a dependency. + if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) { + if (usesNodeCoreModules === undefined) { + usesNodeCoreModules = consumesNodeCoreModules(fromFile); + } + if (usesNodeCoreModules) { + return true; + } + } + return false; + } + + function getNodeModulesPackageNameFromFileName(importedFileName: string, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost): string | undefined { + if (!stringContains(importedFileName, "node_modules")) { + return undefined; + } + const specifier = moduleSpecifiers.getNodeModulesPackageName( + host.getCompilationSettings(), + fromFile.path, + importedFileName, + moduleSpecifierResolutionHost, + ); + + if (!specifier) { + return undefined; + } + // Paths here are not node_modules, so we don’t care about them; + // returning anything will trigger a lookup in package.json. + if (!pathIsRelative(specifier) && !isRootedDiskPath(specifier)) { + return getNodeModuleRootSpecifier(specifier); + } + } + + function getNodeModuleRootSpecifier(fullSpecifier: string): string { + const components = getPathComponents(getPackageNameFromTypesPackageName(fullSpecifier)).slice(1); + // Scoped packages + if (startsWith(components[0], "@")) { + return `${components[0]}/${components[1]}`; + } + return components[0]; + } + } + function tryParseJson(text: string) { try { return JSON.parse(text); @@ -2956,5 +3076,211 @@ namespace ts { return isInJSFile(declaration) || !findAncestor(declaration, isGlobalScopeAugmentation); } + export const enum ImportKind { + Named, + Default, + Namespace, + CommonJS, + } + + export const enum ExportKind { + Named, + Default, + ExportEquals, + UMD, + } + + /** Information about how a symbol is exported from a module. */ + export interface SymbolExportInfo { + symbol: Symbol; + moduleSymbol: Symbol; + exportKind: ExportKind; + /** If true, can't use an es6 import from a js file. */ + exportedSymbolIsTypeOnly: boolean; + /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ + isFromPackageJson: boolean; + } + + export interface ExportMapCache { + clear(): void; + get(file: Path, checker: TypeChecker, projectVersion?: string): MultiMap | undefined; + set(suggestions: MultiMap, projectVersion?: string): void; + isEmpty(): boolean; + /** @returns Whether the change resulted in the cache being cleared */ + onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean): boolean; + } + export function createExportMapCache(): ExportMapCache { + let cache: MultiMap | undefined; + let projectVersion: string | undefined; + let usableByFileName: Path | undefined; + const wrapped: ExportMapCache = { + isEmpty() { + return !cache; + }, + clear() { + cache = undefined; + projectVersion = undefined; + }, + set(suggestions, version) { + cache = suggestions; + if (version) { + projectVersion = version; + } + }, + get: (file, checker, version) => { + if (file !== usableByFileName) { + return undefined; + } + if (version && projectVersion === version) { + return cache; + } + cache?.forEach(infos => { + for (const info of infos) { + // If the symbol/moduleSymbol was a merged symbol, it will have a new identity + // in the checker, even though the symbols to merge are the same (guaranteed by + // cache invalidation in synchronizeHostData). + if (info.symbol.declarations?.length) { + info.symbol = checker.getMergedSymbol(info.exportKind === ExportKind.Default + ? info.symbol.declarations[0].localSymbol ?? info.symbol.declarations[0].symbol + : info.symbol.declarations[0].symbol); + } + if (info.moduleSymbol.declarations?.length) { + info.moduleSymbol = checker.getMergedSymbol(info.moduleSymbol.declarations[0].symbol); + } + } + }); + return cache; + }, + onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) { + if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) { + // File is purely global; doesn't affect export map + return false; + } + if ( + usableByFileName && usableByFileName !== newSourceFile.path || + // If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node. + // Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list. + typeAcquisitionEnabled && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile) || + // Module agumentation and ambient module changes can add or remove exports available to be auto-imported. + // Changes elsewhere in the file can change the *type* of an export in a module augmentation, + // but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache. + !arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) || + !ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile) + ) { + this.clear(); + return true; + } + usableByFileName = newSourceFile.path; + return false; + }, + }; + if (Debug.isDebugging) { + (wrapped as any).__cache = cache; + } + return wrapped; + + function fileIsGlobalOnly(file: SourceFile) { + return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames; + } + + function ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) { + if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) { + return false; + } + let oldFileStatementIndex = -1; + let newFileStatementIndex = -1; + for (const ambientModuleName of newSourceFile.ambientModuleNames) { + const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName; + oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1); + newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1); + if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) { + return false; + } + } + return true; + } + } + + export function createModuleSpecifierCache(): ModuleSpecifierCache { + let cache: ESMap | undefined; + let importingFileName: Path | undefined; + const wrapped: ModuleSpecifierCache = { + get(fromFileName, toFileName) { + if (!cache || fromFileName !== importingFileName) return undefined; + return cache.get(toFileName); + }, + set(fromFileName, toFileName, moduleSpecifiers) { + if (cache && fromFileName !== importingFileName) { + cache.clear(); + } + importingFileName = fromFileName; + (cache ||= new Map()).set(toFileName, moduleSpecifiers); + }, + clear() { + cache = undefined; + importingFileName = undefined; + }, + count() { + return cache ? cache.size : 0; + } + }; + if (Debug.isDebugging) { + (wrapped as any).__cache = cache; + } + return wrapped; + } + + export function isImportableFile( + program: Program, + from: SourceFile, + to: SourceFile, + packageJsonFilter: PackageJsonImportFilter | undefined, + moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost, + moduleSpecifierCache: ModuleSpecifierCache | undefined, + ): boolean { + if (from === to) return false; + const cachedResult = moduleSpecifierCache?.get(from.path, to.path); + if (cachedResult !== undefined) { + return !!cachedResult; + } + + const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost); + const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.(); + const hasImportablePath = !!moduleSpecifiers.forEachFileNameOfModule( + from.fileName, + to.fileName, + moduleSpecifierResolutionHost, + /*preferSymlinks*/ false, + toPath => { + const toFile = program.getSourceFile(toPath); + // Determine to import using toPath only if toPath is what we were looking at + // or there doesnt exist the file in the program by the symlink + return (toFile === to || !toFile) && + isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache); + } + ); + + if (packageJsonFilter) { + const isImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost); + moduleSpecifierCache?.set(from.path, to.path, isImportable); + return isImportable; + } + + return hasImportablePath; + } + + /** + * Don't include something from a `node_modules` that isn't actually reachable by a global import. + * A relative import to node_modules is usually a bad idea. + */ + function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: GetCanonicalFileName, globalCachePath?: string): boolean { + // If it's in a `node_modules` but is not reachable from here via a global import, don't bother. + const toNodeModules = forEachAncestorDirectory(toPath, ancestor => getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined); + const toNodeModulesParent = toNodeModules && getDirectoryPath(getCanonicalFileName(toNodeModules)); + return toNodeModulesParent === undefined + || startsWith(getCanonicalFileName(fromPath), toNodeModulesParent) + || (!!globalCachePath && startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent)); + } + // #endregion } diff --git a/src/testRunner/unittests/tsserver/importSuggestionsCache.ts b/src/testRunner/unittests/tsserver/importSuggestionsCache.ts index 982eece3d36ad..feeeb57ac9e12 100644 --- a/src/testRunner/unittests/tsserver/importSuggestionsCache.ts +++ b/src/testRunner/unittests/tsserver/importSuggestionsCache.ts @@ -27,14 +27,14 @@ namespace ts.projectSystem { describe("unittests:: tsserver:: importSuggestionsCache", () => { it("caches auto-imports in the same file", () => { const { importSuggestionsCache, checker } = setup(); - assert.ok(importSuggestionsCache.get(bTs.path, checker)); + assert.ok(importSuggestionsCache.get(bTs.path as Path, checker)); }); it("invalidates the cache when new files are added", () => { const { host, importSuggestionsCache, checker } = setup(); host.writeFile("/src/a2.ts", aTs.content); host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path, checker)); + assert.isUndefined(importSuggestionsCache.get(bTs.path as Path, checker)); }); it("invalidates the cache when files are deleted", () => { @@ -42,14 +42,14 @@ namespace ts.projectSystem { projectService.closeClientFile(aTs.path); host.deleteFile(aTs.path); host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path, checker)); + assert.isUndefined(importSuggestionsCache.get(bTs.path as Path, checker)); }); - it("invalidates the cache when package.json is changed", () => { + it("does not invalidate the cache when package.json is changed", () => { const { host, importSuggestionsCache, checker } = setup(); host.writeFile("/package.json", "{}"); host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path, checker)); + assert.isUndefined(importSuggestionsCache.get(bTs.path as Path, checker)); }); }); @@ -70,6 +70,6 @@ namespace ts.projectSystem { prefix: "foo", }); const checker = project.getLanguageService().getProgram()!.getTypeChecker(); - return { host, project, projectService, importSuggestionsCache: project.getImportSuggestionsCache(), checker }; + return { host, project, projectService, importSuggestionsCache: project.getExportMapCache(), checker }; } } From 630f2004994c8e0b63b4f8606a31a5a1ba05558e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sun, 21 Mar 2021 14:29:05 -0700 Subject: [PATCH 23/35] Fix and test cache invalidation logic --- src/server/editorServices.ts | 25 +++--- src/server/project.ts | 44 +++++++--- src/services/codefixes/importFixes.ts | 4 + src/services/utilities.ts | 6 +- src/testRunner/tsconfig.json | 3 +- .../unittests/tsserver/exportMapCache.ts | 88 +++++++++++++++++++ .../tsserver/importSuggestionsCache.ts | 75 ---------------- .../tsserver/moduleSpecifierCache.ts | 81 +++++++++++++++++ 8 files changed, 221 insertions(+), 105 deletions(-) create mode 100644 src/testRunner/unittests/tsserver/exportMapCache.ts delete mode 100644 src/testRunner/unittests/tsserver/importSuggestionsCache.ts create mode 100644 src/testRunner/unittests/tsserver/moduleSpecifierCache.ts diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2689d4e7cadd0..48860715f622b 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2845,7 +2845,7 @@ namespace ts.server { }); } if (includePackageJsonAutoImports !== args.preferences.includePackageJsonAutoImports) { - this.invalidateProjectAutoImports(/*packageJsonPath*/ undefined); + this.invalidateProjectPackageJson(/*packageJsonPath*/ undefined); } } if (args.extraFileExtensions) { @@ -3933,7 +3933,7 @@ namespace ts.server { private watchPackageJsonFile(path: Path) { const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = new Map()); if (!watchers.has(path)) { - this.invalidateProjectAutoImports(path); + this.invalidateProjectPackageJson(path); watchers.set(path, this.watchFactory.watchFile( path, (fileName, eventKind) => { @@ -3943,11 +3943,11 @@ namespace ts.server { return Debug.fail(); case FileWatcherEventKind.Changed: this.packageJsonCache.addOrUpdate(path); - this.invalidateProjectAutoImports(path); + this.invalidateProjectPackageJson(path); break; case FileWatcherEventKind.Deleted: this.packageJsonCache.delete(path); - this.invalidateProjectAutoImports(path); + this.invalidateProjectPackageJson(path); watchers.get(path)!.close(); watchers.delete(path); } @@ -3975,15 +3975,16 @@ namespace ts.server { } /*@internal*/ - private invalidateProjectAutoImports(packageJsonPath: Path | undefined) { - if (this.includePackageJsonAutoImports()) { - this.configuredProjects.forEach(invalidate); - this.inferredProjects.forEach(invalidate); - this.externalProjects.forEach(invalidate); - } + private invalidateProjectPackageJson(packageJsonPath: Path | undefined) { + this.configuredProjects.forEach(invalidate); + this.inferredProjects.forEach(invalidate); + this.externalProjects.forEach(invalidate); function invalidate(project: Project) { - if (!packageJsonPath || project.packageJsonsForAutoImport?.has(packageJsonPath)) { - project.markAutoImportProviderAsDirty(); + if (packageJsonPath) { + project.onPackageJsonChange(packageJsonPath); + } + else { + project.onAutoImportProviderSettingsChanged(); } } } diff --git a/src/server/project.ts b/src/server/project.ts index b273e37183bf3..dd3d21cbd3510 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -206,7 +206,7 @@ namespace ts.server { originalConfiguredProjects: Set | undefined; /*@internal*/ - packageJsonsForAutoImport: Set | undefined; + private packageJsonsForAutoImport: Set | undefined; /*@internal*/ getResolvedProjectReferenceToRedirect(_fileName: string): ResolvedProjectReference | undefined { @@ -970,6 +970,10 @@ namespace ts.server { info.detachFromProject(this); } + if (info.getRealpathIfDifferent()) { + this.hasAddedOrRemovedSymlinks = true; + } + this.markAsDirty(); } @@ -993,12 +997,23 @@ namespace ts.server { } /*@internal*/ - markAutoImportProviderAsDirty() { + onAutoImportProviderSettingsChanged() { if (this.autoImportProviderHost === false) { this.autoImportProviderHost = undefined; } - this.autoImportProviderHost?.markAsDirty(); - this.exportMapCache.clear(); + else { + this.autoImportProviderHost?.markAsDirty(); + } + } + + /*@internal*/ + onPackageJsonChange(packageJsonPath: Path) { + if (this.packageJsonsForAutoImport?.has(packageJsonPath)) { + this.moduleSpecifierCache.clear(); + if (this.autoImportProviderHost) { + this.autoImportProviderHost.markAsDirty(); + } + } } /* @internal */ @@ -1173,7 +1188,7 @@ namespace ts.server { } if (!this.exportMapCache.isEmpty()) { - if (this.hasAddedorRemovedFiles || oldProgram && !this.program.structureIsReused) { + if (this.hasAddedorRemovedFiles || !this.program.structureIsReused) { this.exportMapCache.clear(); } else if (this.changedFilesForExportMapCache && oldProgram && this.program) { @@ -1192,12 +1207,10 @@ namespace ts.server { this.changedFilesForExportMapCache.clear(); } - if (this.hasAddedorRemovedFiles) { - const symlinksInvalidated = this.hasAddedOrRemovedSymlinks || this.getCompilerOptions().preserveSymlinks; - if (symlinksInvalidated) { - this.symlinks = undefined; - this.moduleSpecifierCache.clear(); - } + if (this.hasAddedOrRemovedSymlinks || !this.program.structureIsReused && this.getCompilerOptions().preserveSymlinks) { + // With --preserveSymlinks, we may not determine that a file is a symlink, so we never set `hasAddedOrRemovedSymlinks` + this.symlinks = undefined; + this.moduleSpecifierCache.clear(); } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; @@ -1977,8 +1990,11 @@ namespace ts.server { this.projectService.setFileNamesOfAutoImportProviderProject(this, rootFileNames); this.rootFileNames = rootFileNames; - this.hostProject.getExportMapCache().clear(); - return super.updateGraph(); + const isSameSetOfFiles = super.updateGraph(); + if (!isSameSetOfFiles) { + this.hostProject.getExportMapCache().clear(); + } + return isSameSetOfFiles; } hasRoots() { @@ -1998,7 +2014,7 @@ namespace ts.server { throw new Error("AutoImportProviderProject language service should never be used. To get the program, use `project.getCurrentProgram()`."); } - markAutoImportProviderAsDirty(): never { + onAutoImportProviderSettingsChanged(): never { throw new Error("AutoImportProviderProject is an auto import provider; use `markAsDirty()` instead."); } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index eee24a3d9c746..364c8c3c7f638 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -280,6 +280,10 @@ namespace ts.codefix { export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program) { const start = timestamp(); + // Pulling the AutoImportProvider project will trigger its updateGraph if pending, + // which will invalidate the export map cache if things change, so pull it before + // checking the cache. + host.getPackageJsonAutoImportProvider?.(); const cache = host.getExportMapCache?.(); if (cache) { const cached = cache.get(importingFile.path, program.getTypeChecker(), host.getProjectVersion?.()); diff --git a/src/services/utilities.ts b/src/services/utilities.ts index e3247c2644628..30d0ae57916ae 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3128,7 +3128,7 @@ namespace ts { } }, get: (file, checker, version) => { - if (file !== usableByFileName) { + if (usableByFileName && file !== usableByFileName) { return undefined; } if (version && projectVersion === version) { @@ -3175,7 +3175,7 @@ namespace ts { }, }; if (Debug.isDebugging) { - (wrapped as any).__cache = cache; + Object.defineProperty(wrapped, "__cache", { get: () => cache }); } return wrapped; @@ -3225,7 +3225,7 @@ namespace ts { } }; if (Debug.isDebugging) { - (wrapped as any).__cache = cache; + Object.defineProperty(wrapped, "__cache", { get: () => cache }); } return wrapped; } diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 185d3074081bb..143c035b5ea34 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -168,6 +168,7 @@ "unittests/tsserver/events/projectLanguageServiceState.ts", "unittests/tsserver/events/projectLoading.ts", "unittests/tsserver/events/projectUpdatedInBackground.ts", + "unittests/tsserver/exportMapCache.ts", "unittests/tsserver/externalProjects.ts", "unittests/tsserver/forceConsistentCasingInFileNames.ts", "unittests/tsserver/formatSettings.ts", @@ -176,11 +177,11 @@ "unittests/tsserver/getExportReferences.ts", "unittests/tsserver/getFileReferences.ts", "unittests/tsserver/importHelpers.ts", - "unittests/tsserver/importSuggestionsCache.ts", "unittests/tsserver/inferredProjects.ts", "unittests/tsserver/languageService.ts", "unittests/tsserver/maxNodeModuleJsDepth.ts", "unittests/tsserver/metadataInResponse.ts", + "unittests/tsserver/moduleSpecifierCache.ts", "unittests/tsserver/navTo.ts", "unittests/tsserver/occurences.ts", "unittests/tsserver/openFile.ts", diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts new file mode 100644 index 0000000000000..803d8b9d62234 --- /dev/null +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -0,0 +1,88 @@ +namespace ts.projectSystem { + const packageJson: File = { + path: "/package.json", + content: `{ "dependencies": { "mobx": "*" } }` + }; + const aTs: File = { + path: "/a.ts", + content: "export const foo = 0;", + }; + const bTs: File = { + path: "/b.ts", + content: "foo", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + const ambientDeclaration: File = { + path: "/ambient.d.ts", + content: "declare module 'ambient' {}" + }; + const mobxDts: File = { + path: "/node_modules/mobx/index.d.ts", + content: "export declare function observable(): unknown;" + }; + + describe("unittests:: tsserver:: exportMapCache", () => { + it("caches auto-imports in the same file", () => { + const { exportMapCache, checker } = setup(); + assert.ok(exportMapCache.get(bTs.path as Path, checker)); + }); + + it("invalidates the cache when new files are added", () => { + const { host, exportMapCache, checker } = setup(); + host.writeFile("/src/a2.ts", aTs.content); + host.runQueuedTimeoutCallbacks(); + assert.isUndefined(exportMapCache.get(bTs.path as Path, checker)); + }); + + it("invalidates the cache when files are deleted", () => { + const { host, projectService, exportMapCache, checker } = setup(); + projectService.closeClientFile(aTs.path); + host.deleteFile(aTs.path); + host.runQueuedTimeoutCallbacks(); + assert.isUndefined(exportMapCache.get(bTs.path as Path, checker)); + }); + + it("does not invalidate the cache when package.json is changed inconsequentially", () => { + const { host, exportMapCache, checker, project } = setup(); + host.writeFile("/package.json", `{ "name": "blah", "dependencies": { "mobx": "*" } }`); + host.runQueuedTimeoutCallbacks(); + project.getPackageJsonAutoImportProvider(); + assert.ok(exportMapCache.get(bTs.path as Path, checker)); + }); + + it("invalidates the cache when package.json change results in AutoImportProvider change", () => { + const { host, exportMapCache, checker, project } = setup(); + host.writeFile("/package.json", `{}`); + host.runQueuedTimeoutCallbacks(); + project.getPackageJsonAutoImportProvider(); + assert.isUndefined(exportMapCache.get(bTs.path as Path, checker)); + }); + }); + + function setup() { + const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts]); + const session = createSession(host); + openFilesForSession([aTs, bTs], session); + const projectService = session.getProjectService(); + const project = configuredProjectAt(projectService, 0); + triggerCompletions(); + const checker = project.getLanguageService().getProgram()!.getTypeChecker(); + return { host, project, projectService, exportMapCache: project.getExportMapCache(), checker, triggerCompletions }; + + function triggerCompletions() { + const requestLocation: protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, + }; + executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", + }); + } + } +} diff --git a/src/testRunner/unittests/tsserver/importSuggestionsCache.ts b/src/testRunner/unittests/tsserver/importSuggestionsCache.ts deleted file mode 100644 index feeeb57ac9e12..0000000000000 --- a/src/testRunner/unittests/tsserver/importSuggestionsCache.ts +++ /dev/null @@ -1,75 +0,0 @@ -namespace ts.projectSystem { - const packageJson: File = { - path: "/package.json", - content: `{ "dependencies": { "mobx": "*" } }` - }; - const aTs: File = { - path: "/a.ts", - content: "export const foo = 0;", - }; - const bTs: File = { - path: "/b.ts", - content: "foo", - }; - const tsconfig: File = { - path: "/tsconfig.json", - content: "{}", - }; - const ambientDeclaration: File = { - path: "/ambient.d.ts", - content: "declare module 'ambient' {}" - }; - const mobxDts: File = { - path: "/node_modules/mobx/index.d.ts", - content: "export declare function observable(): unknown;" - }; - - describe("unittests:: tsserver:: importSuggestionsCache", () => { - it("caches auto-imports in the same file", () => { - const { importSuggestionsCache, checker } = setup(); - assert.ok(importSuggestionsCache.get(bTs.path as Path, checker)); - }); - - it("invalidates the cache when new files are added", () => { - const { host, importSuggestionsCache, checker } = setup(); - host.writeFile("/src/a2.ts", aTs.content); - host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path as Path, checker)); - }); - - it("invalidates the cache when files are deleted", () => { - const { host, projectService, importSuggestionsCache, checker } = setup(); - projectService.closeClientFile(aTs.path); - host.deleteFile(aTs.path); - host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path as Path, checker)); - }); - - it("does not invalidate the cache when package.json is changed", () => { - const { host, importSuggestionsCache, checker } = setup(); - host.writeFile("/package.json", "{}"); - host.runQueuedTimeoutCallbacks(); - assert.isUndefined(importSuggestionsCache.get(bTs.path as Path, checker)); - }); - }); - - function setup() { - const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts]); - const session = createSession(host); - openFilesForSession([aTs, bTs], session); - const projectService = session.getProjectService(); - const project = configuredProjectAt(projectService, 0); - const requestLocation: protocol.FileLocationRequestArgs = { - file: bTs.path, - line: 1, - offset: 3, - }; - executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { - ...requestLocation, - includeExternalModuleExports: true, - prefix: "foo", - }); - const checker = project.getLanguageService().getProgram()!.getTypeChecker(); - return { host, project, projectService, importSuggestionsCache: project.getExportMapCache(), checker }; - } -} diff --git a/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts new file mode 100644 index 0000000000000..f109cab9d1f6e --- /dev/null +++ b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts @@ -0,0 +1,81 @@ +namespace ts.projectSystem { + const packageJson: File = { + path: "/package.json", + content: `{ "dependencies": { "mobx": "*" } }` + }; + const aTs: File = { + path: "/a.ts", + content: "export const foo = 0;", + }; + const bTs: File = { + path: "/b.ts", + content: "foo", + }; + const bSymlink: SymLink = { + path: "/b-link.ts", + symLink: "./b.ts", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; + const ambientDeclaration: File = { + path: "/ambient.d.ts", + content: "declare module 'ambient' {}" + }; + const mobxDts: File = { + path: "/node_modules/mobx/index.d.ts", + content: "export declare function observable(): unknown;" + }; + + describe("unittests:: tsserver:: moduleSpecifierCache", () => { + it("caches importability within a file", () => { + const { moduleSpecifierCache } = setup(); + assert.isTrue(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path)); + }); + + it("does not invalidate the cache when new files are added", () => { + const { host, moduleSpecifierCache } = setup(); + host.writeFile("/src/a2.ts", aTs.content); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path)); + }); + + it("invalidates the cache when symlinks are added or removed", () => { + const { host, moduleSpecifierCache } = setup(); + host.renameFile(bSymlink.path, "/b-link2.ts"); + host.runQueuedTimeoutCallbacks(); + assert.equal(moduleSpecifierCache.count(), 0); + }); + + it("invalidates the cache when package.json changes", () => { + const { host, moduleSpecifierCache } = setup(); + host.writeFile("/package.json", `{}`); + host.runQueuedTimeoutCallbacks(); + assert.isUndefined(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path)); + }); + }); + + function setup() { + const host = createServerHost([aTs, bTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxDts]); + const session = createSession(host); + openFilesForSession([aTs, bTs], session); + const projectService = session.getProjectService(); + const project = configuredProjectAt(projectService, 0); + triggerCompletions(); + return { host, project, projectService, moduleSpecifierCache: project.getModuleSpecifierCache(), triggerCompletions }; + + function triggerCompletions() { + const requestLocation: protocol.FileLocationRequestArgs = { + file: bTs.path, + line: 1, + offset: 3, + }; + executeSessionRequest(session, protocol.CommandTypes.CompletionInfo, { + ...requestLocation, + includeExternalModuleExports: true, + prefix: "foo", + }); + } + } +} From f60ac89d3d46b7e45b80295da3dc7ba0eed27bf8 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sun, 21 Mar 2021 14:41:14 -0700 Subject: [PATCH 24/35] Update API baselines --- src/server/project.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/server/project.ts b/src/server/project.ts index dd3d21cbd3510..f669a92cfe9ec 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1207,7 +1207,7 @@ namespace ts.server { this.changedFilesForExportMapCache.clear(); } - if (this.hasAddedOrRemovedSymlinks || !this.program.structureIsReused && this.getCompilerOptions().preserveSymlinks) { + if (this.hasAddedOrRemovedSymlinks || this.program && !this.program.structureIsReused && this.getCompilerOptions().preserveSymlinks) { // With --preserveSymlinks, we may not determine that a file is a symlink, so we never set `hasAddedOrRemovedSymlinks` this.symlinks = undefined; this.moduleSpecifierCache.clear(); @@ -2014,10 +2014,16 @@ namespace ts.server { throw new Error("AutoImportProviderProject language service should never be used. To get the program, use `project.getCurrentProgram()`."); } + /*@internal*/ onAutoImportProviderSettingsChanged(): never { throw new Error("AutoImportProviderProject is an auto import provider; use `markAsDirty()` instead."); } + /*@internal*/ + onPackageJsonChange(): never { + throw new Error("package.json changes should be notified on an AutoImportProvider's host project"); + } + getModuleResolutionHostForAutoImportProvider(): never { throw new Error("AutoImportProviderProject cannot provide its own host; use `hostProject.getModuleResolutionHostForAutomImportProvider()` instead."); } From 32fccbb49f433e50949b36bb94009fe31593001a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Sun, 21 Mar 2021 15:35:28 -0700 Subject: [PATCH 25/35] Add separate user preference for snippet-formatted completions --- src/compiler/types.ts | 1 + src/server/protocol.ts | 4 ++++ src/services/completions.ts | 11 +++++---- .../reference/api/tsserverlibrary.d.ts | 7 +++++- tests/baselines/reference/api/typescript.d.ts | 1 + tests/cases/fourslash/fourslash.ts | 1 + .../fourslash/importStatementCompletions1.ts | 1 + ...rtStatementCompletions_esModuleInterop1.ts | 1 + ...rtStatementCompletions_esModuleInterop2.ts | 1 + ...rtStatementCompletions_noPatternAmbient.ts | 1 + .../importStatementCompletions_noSnippet.ts | 24 +++++++++++++++++++ .../importStatementCompletions_quotes.ts | 1 + .../importStatementCompletions_semicolons.ts | 1 + 13 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 tests/cases/fourslash/importStatementCompletions_noSnippet.ts diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 533fd42e9b1ee..031aa29f07a66 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -8270,6 +8270,7 @@ namespace ts { readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsForImportStatements?: boolean; + readonly includeCompletionsWithSnippetText?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index daa411831ed46..45e71e5ee89b8 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3296,6 +3296,10 @@ namespace ts.server.protocol { * `import write|` to be completed to `import { writeFile } from "fs"`. */ readonly includeCompletionsForImportStatements?: boolean; + /** + * Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. + */ + readonly includeCompletionsWithSnippetText?: boolean; /** * If enabled, the completion list will include completions with invalid identifier names. * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. diff --git a/src/services/completions.ts b/src/services/completions.ts index c29ec35bdf10d..ac6269cee1057 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -459,7 +459,7 @@ namespace ts.Completions { Debug.assertIsDefined(importCompletionNode); ({ insertText, replacementSpan } = getInsertTextAndReplacementSpanForImportCompletion(name, importCompletionNode, origin, useSemicolons, options, preferences)); sourceDisplay = [textPart(origin.moduleSpecifier)]; - isSnippet = true; + isSnippet = preferences.includeCompletionsWithSnippetText ? true : undefined; } if (insertText !== undefined && !preferences.includeCompletionsWithInsertText) { @@ -509,13 +509,14 @@ namespace ts.Completions { origin.isDefaultExport ? ExportKind.Default : origin.exportName === InternalSymbolName.ExportEquals ? ExportKind.ExportEquals : ExportKind.Named; + const tabStop = preferences.includeCompletionsWithSnippetText ? "$1" : ""; const importKind = codefix.getImportKind(sourceFile, exportKind, options); const suffix = useSemicolons ? ";" : ""; switch (importKind) { - case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name}$1 = require(${quotedModuleSpecifier})${suffix}` }; - case ImportKind.Default: return { replacementSpan, insertText: `import ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; - case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name}$1 from ${quotedModuleSpecifier}${suffix}` }; - case ImportKind.Named: return { replacementSpan, insertText: `import { ${name}$1 } from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.CommonJS: return { replacementSpan, insertText: `import ${name}${tabStop} = require(${quotedModuleSpecifier})${suffix}` }; + case ImportKind.Default: return { replacementSpan, insertText: `import ${name}${tabStop} from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.Namespace: return { replacementSpan, insertText: `import * as ${name}${tabStop} from ${quotedModuleSpecifier}${suffix}` }; + case ImportKind.Named: return { replacementSpan, insertText: `import { ${name}${tabStop} } from ${quotedModuleSpecifier}${suffix}` }; } } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index fb41b1757aee8..b80b9ecc2c2b8 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3875,6 +3875,7 @@ declare namespace ts { readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsForImportStatements?: boolean; + readonly includeCompletionsWithSnippetText?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; @@ -9149,6 +9150,10 @@ declare namespace ts.server.protocol { * `import write|` to be completed to `import { writeFile } from "fs"`. */ readonly includeCompletionsForImportStatements?: boolean; + /** + * Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. + */ + readonly includeCompletionsWithSnippetText?: boolean; /** * If enabled, the completion list will include completions with invalid identifier names. * For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`. @@ -9331,6 +9336,7 @@ declare namespace ts.server { open(newText: string): void; close(fileExists?: boolean): void; getSnapshot(): IScriptSnapshot; + /** @returns Whether the file was a symlink */ private ensureRealPath; getFormatCodeSettings(): FormatCodeSettings | undefined; getPreferences(): protocol.UserPreferences | undefined; @@ -9574,7 +9580,6 @@ declare namespace ts.server { markAsDirty(): void; getScriptFileNames(): string[]; getLanguageService(): never; - markAutoImportProviderAsDirty(): never; getModuleResolutionHostForAutoImportProvider(): never; getProjectReferences(): readonly ProjectReference[] | undefined; useSourceOfProjectReferenceRedirect(): boolean; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 0199506618c98..1246ec63a634b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3875,6 +3875,7 @@ declare namespace ts { readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsForImportStatements?: boolean; + readonly includeCompletionsWithSnippetText?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly includeCompletionsWithInsertText?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index cac43c638f9a8..88d6719cf6d14 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -620,6 +620,7 @@ declare namespace FourSlashInterface { readonly quotePreference?: "auto" | "double" | "single"; readonly includeCompletionsForModuleExports?: boolean; readonly includeCompletionsForImportStatements?: boolean; + readonly includeCompletionsWithSnippetText?: boolean; readonly includeInsertTextCompletions?: boolean; readonly includeAutomaticOptionalChainCompletions?: boolean; readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative"; diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts index 5352e7afbbbcd..dacc26c22e490 100644 --- a/tests/cases/fourslash/importStatementCompletions1.ts +++ b/tests/cases/fourslash/importStatementCompletions1.ts @@ -35,6 +35,7 @@ preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, + includeCompletionsWithSnippetText: true, } }); }); diff --git a/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts b/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts index 8b1e1cfe5287c..a6d2083489b89 100644 --- a/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts +++ b/tests/cases/fourslash/importStatementCompletions_esModuleInterop1.ts @@ -22,5 +22,6 @@ verify.completions({ preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, + includeCompletionsWithSnippetText: true, } }); diff --git a/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts b/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts index aad113cff99f5..c6a5b8e25e1f6 100644 --- a/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts +++ b/tests/cases/fourslash/importStatementCompletions_esModuleInterop2.ts @@ -22,5 +22,6 @@ verify.completions({ preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, + includeCompletionsWithSnippetText: true, } }); diff --git a/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts b/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts index 5189d080ab02e..78bad2241b0c8 100644 --- a/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts +++ b/tests/cases/fourslash/importStatementCompletions_noPatternAmbient.ts @@ -15,5 +15,6 @@ verify.completions({ preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, + includeCompletionsWithSnippetText: true, } }); diff --git a/tests/cases/fourslash/importStatementCompletions_noSnippet.ts b/tests/cases/fourslash/importStatementCompletions_noSnippet.ts new file mode 100644 index 0000000000000..8f87dba53bb39 --- /dev/null +++ b/tests/cases/fourslash/importStatementCompletions_noSnippet.ts @@ -0,0 +1,24 @@ +/// + +// @Filename: /mod.ts +//// export const foo = 0; + +// @Filename: /index0.ts +//// [|import f/**/|] + +verify.completions({ + marker: "", + exact: [{ + name: "foo", + source: "./mod", + insertText: `import { foo } from "./mod";`, // <-- no `$1` tab stop + isSnippet: undefined, // <-- undefined + replacementSpan: test.ranges()[0], + sourceDisplay: "./mod", + }], + preferences: { + includeCompletionsForImportStatements: true, + includeInsertTextCompletions: true, + includeCompletionsWithSnippetText: false, // <-- false + } +}); diff --git a/tests/cases/fourslash/importStatementCompletions_quotes.ts b/tests/cases/fourslash/importStatementCompletions_quotes.ts index 626953ec987bb..845dd0a205caf 100644 --- a/tests/cases/fourslash/importStatementCompletions_quotes.ts +++ b/tests/cases/fourslash/importStatementCompletions_quotes.ts @@ -20,5 +20,6 @@ verify.completions({ preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, + includeCompletionsWithSnippetText: true, } }); diff --git a/tests/cases/fourslash/importStatementCompletions_semicolons.ts b/tests/cases/fourslash/importStatementCompletions_semicolons.ts index 96f00751149ed..fb61be82d790a 100644 --- a/tests/cases/fourslash/importStatementCompletions_semicolons.ts +++ b/tests/cases/fourslash/importStatementCompletions_semicolons.ts @@ -20,5 +20,6 @@ verify.completions({ preferences: { includeCompletionsForImportStatements: true, includeInsertTextCompletions: true, + includeCompletionsWithSnippetText: true, } }); From eb421c04d83ccf35bd4cb8dc80ec2ec743cd4f5e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 22 Mar 2021 09:37:28 -0700 Subject: [PATCH 26/35] Require first character to match when resolving module specifiers --- src/services/completions.ts | 11 ++++++++++- tests/cases/fourslash/importStatementCompletions1.ts | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index ac6269cee1057..9c2380936499e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1708,7 +1708,7 @@ namespace ts.Completions { const symbolName = key.substring(0, key.indexOf("|")); if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); - if (isCompletionDetailsMatch || stringContainsCharactersInOrder(symbolName.toLowerCase(), lowerCaseTokenText)) { + if (isCompletionDetailsMatch || isNameMatch(symbolName)) { // If we don't need to resolve module specifiers, we can use any re-export that is importable at all // (We need to ensure that at least one is importable to show a completion.) const { moduleSpecifier, exportInfo } = resolveModuleSpecifiers @@ -1732,6 +1732,15 @@ namespace ts.Completions { }); host.log?.(`collectAutoImports: done in ${timestamp() - start} ms`); + function isNameMatch(symbolName: string) { + const lowerCaseSymbolName = symbolName.toLowerCase(); + if (resolveModuleSpecifiers && lowerCaseTokenText) { + // Use a more restrictive filter if resolving module specifiers since resolving module specifiers is expensive. + return lowerCaseTokenText[0] === lowerCaseSymbolName[0] && stringContainsCharactersInOrder(lowerCaseSymbolName, lowerCaseTokenText); + } + return stringContainsCharactersInOrder(lowerCaseSymbolName, lowerCaseTokenText); + } + function isImportableExportInfo(info: SymbolExportInfo) { const moduleFile = tryCast(info.moduleSymbol.valueDeclaration, isSourceFile); if (!moduleFile) { diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts index dacc26c22e490..576dfdbdc9b0d 100644 --- a/tests/cases/fourslash/importStatementCompletions1.ts +++ b/tests/cases/fourslash/importStatementCompletions1.ts @@ -55,7 +55,10 @@ // @Filename: /index10.ts //// import f/*10*/ from "./mod"; -[6, 7, 8, 9, 10].forEach(marker => { +// @Filename: /index11.ts +//// import oo/*11*/ + +[6, 7, 8, 9, 10, 11].forEach(marker => { verify.completions({ marker: "" + marker, exact: [], From cdb7efda15f7fd38e504c4d164880e412ed2ddf6 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 22 Mar 2021 10:31:40 -0700 Subject: [PATCH 27/35] Fix AutoImportProvider export map cache invalidation --- src/server/project.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index f669a92cfe9ec..2344b530846ba 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1990,11 +1990,8 @@ namespace ts.server { this.projectService.setFileNamesOfAutoImportProviderProject(this, rootFileNames); this.rootFileNames = rootFileNames; - const isSameSetOfFiles = super.updateGraph(); - if (!isSameSetOfFiles) { - this.hostProject.getExportMapCache().clear(); - } - return isSameSetOfFiles; + this.hostProject.getExportMapCache().clear(); + return super.updateGraph(); } hasRoots() { From 9976d3f8a5d2065cfa3aa3238c2564ced74b6bb5 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 22 Mar 2021 11:43:21 -0700 Subject: [PATCH 28/35] Really fix auto import provider export map invalidation --- src/server/project.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 2344b530846ba..916421c76c17a 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1990,8 +1990,12 @@ namespace ts.server { this.projectService.setFileNamesOfAutoImportProviderProject(this, rootFileNames); this.rootFileNames = rootFileNames; - this.hostProject.getExportMapCache().clear(); - return super.updateGraph(); + const oldProgram = this.getCurrentProgram(); + const hasSameSetOfFiles = super.updateGraph(); + if (oldProgram && oldProgram !== this.getCurrentProgram()) { + this.hostProject.getExportMapCache().clear(); + } + return hasSameSetOfFiles; } hasRoots() { From 8550be70486bec947233ddada5dd206fd2ade98d Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 26 Mar 2021 12:15:40 -0700 Subject: [PATCH 29/35] Update test added in master --- src/testRunner/unittests/tsserver/jsdocTag.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/testRunner/unittests/tsserver/jsdocTag.ts b/src/testRunner/unittests/tsserver/jsdocTag.ts index 5033a4fca7fe9..55c386650dbb9 100644 --- a/src/testRunner/unittests/tsserver/jsdocTag.ts +++ b/src/testRunner/unittests/tsserver/jsdocTag.ts @@ -592,6 +592,7 @@ foo` kindModifiers: "", name: "foo", source: undefined, + sourceDisplay: undefined, tags, }]); } From 66caaf2f4ae568434305241ac2f44bf9c7edeb5e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 26 Mar 2021 12:17:09 -0700 Subject: [PATCH 30/35] Use logical or assignment Co-authored-by: Daniel Rosenwasser --- src/server/project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/project.ts b/src/server/project.ts index 916421c76c17a..cfefa329a5c35 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -985,7 +985,7 @@ namespace ts.server { markFileAsDirty(changedFile: Path) { this.markAsDirty(); if (!this.exportMapCache.isEmpty()) { - (this.changedFilesForExportMapCache || (this.changedFilesForExportMapCache = new Set())).add(changedFile); + (this.changedFilesForExportMapCache ||= new Set()).add(changedFile); } } From d10aa0c0d960899e5a22d333d63cf19ed6b515ed Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 26 Mar 2021 12:18:01 -0700 Subject: [PATCH 31/35] Simply conditional by reversing Co-authored-by: Daniel Rosenwasser --- src/services/completions.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 52909de200cc8..aa211be0ee974 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -141,10 +141,11 @@ namespace ts.Completions { if (triggerCharacter === " ") { // `isValidTrigger` ensures we are at `import |` - if (!(preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText)) { - return undefined; + if (preferences.includeCompletionsForImportStatements && preferences.includeCompletionsWithInsertText) { + return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; } - return { isGlobalCompletion: true, isMemberCompletion: false, isNewIdentifierLocation: true, isIncomplete: true, entries: [] }; + return undefined; + } const stringCompletions = StringCompletions.getStringLiteralCompletions(sourceFile, position, contextToken, typeChecker, compilerOptions, host, log, preferences); From 9ea1bd43e21f9ec80334b4100dc584f617a1fecc Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 9 Oct 2020 11:40:07 -0700 Subject: [PATCH 32/35] When file is deleted need to marked correctly in the project as removed file --- src/server/scriptInfo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 9cc588780839b..6e50de20c6d5a 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -483,6 +483,7 @@ namespace ts.server { const existingRoot = p.getRootFilesMap().get(this.path); // detach is unnecessary since we'll clean the list of containing projects anyways p.removeFile(this, /*fileExists*/ false, /*detachFromProjects*/ false); + p.onFileAddedOrRemoved(); // If the info was for the external or configured project's root, // add missing file as the root if (existingRoot && !isInferredProject(p)) { From eb52faa51e0689a90c94e815af9392505ad5e557 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 26 Mar 2021 13:43:49 -0700 Subject: [PATCH 33/35] Simplify hasAddedOrRemovedSymlinks with cherry-picked fix --- src/server/project.ts | 14 ++++---------- src/server/scriptInfo.ts | 31 +++++++++++++++++-------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 3e9868887ef3b..befa75a8da5bb 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -970,10 +970,6 @@ namespace ts.server { info.detachFromProject(this); } - if (info.getRealpathIfDifferent()) { - this.hasAddedOrRemovedSymlinks = true; - } - this.markAsDirty(); } @@ -1017,13 +1013,11 @@ namespace ts.server { } /* @internal */ - onFileAddedOrRemoved() { + onFileAddedOrRemoved(isSymlink: boolean | undefined) { this.hasAddedorRemovedFiles = true; - } - - /* @internal */ - onSymlinkAddedOrRemoved() { - this.hasAddedOrRemovedSymlinks = true; + if (isSymlink) { + this.hasAddedOrRemovedSymlinks = true; + } } /** diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 6e50de20c6d5a..a26fcc6748932 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -391,8 +391,7 @@ namespace ts.server { return this.textStorage.getSnapshot(); } - /** @returns Whether the file was a symlink */ - private ensureRealPath(): boolean { + private ensureRealPath() { if (this.realpath === undefined) { // Default is just the path this.realpath = this.path; @@ -405,12 +404,10 @@ namespace ts.server { // If it is different from this.path, add to the map if (this.realpath !== this.path) { project.projectService.realpathToScriptInfos!.add(this.realpath, this); // TODO: GH#18217 - return true; } } } } - return false; } /*@internal*/ @@ -418,6 +415,15 @@ namespace ts.server { return this.realpath && this.realpath !== this.path ? this.realpath : undefined; } + /** + * @internal + * Does not compute realpath; uses precomputed result. Use `ensureRealPath` + * first if a definite result is needed. + */ + isSymlink(): boolean | undefined { + return this.realpath && this.realpath !== this.path; + } + getFormatCodeSettings(): FormatCodeSettings | undefined { return this.formatSettings; } getPreferences(): protocol.UserPreferences | undefined { return this.preferences; } @@ -425,13 +431,10 @@ namespace ts.server { const isNew = !this.isAttached(project); if (isNew) { this.containingProjects.push(project); - project.onFileAddedOrRemoved(); if (!project.getCompilerOptions().preserveSymlinks) { - const isSymlink = this.ensureRealPath(); - if (isSymlink) { - project.onSymlinkAddedOrRemoved(); - } + this.ensureRealPath(); } + project.onFileAddedOrRemoved(this.isSymlink()); } return isNew; } @@ -453,23 +456,23 @@ namespace ts.server { return; case 1: if (this.containingProjects[0] === project) { - project.onFileAddedOrRemoved(); + project.onFileAddedOrRemoved(this.isSymlink()); this.containingProjects.pop(); } break; case 2: if (this.containingProjects[0] === project) { - project.onFileAddedOrRemoved(); + project.onFileAddedOrRemoved(this.isSymlink()); this.containingProjects[0] = this.containingProjects.pop()!; } else if (this.containingProjects[1] === project) { - project.onFileAddedOrRemoved(); + project.onFileAddedOrRemoved(this.isSymlink()); this.containingProjects.pop(); } break; default: if (unorderedRemoveItem(this.containingProjects, project)) { - project.onFileAddedOrRemoved(); + project.onFileAddedOrRemoved(this.isSymlink()); } break; } @@ -483,7 +486,7 @@ namespace ts.server { const existingRoot = p.getRootFilesMap().get(this.path); // detach is unnecessary since we'll clean the list of containing projects anyways p.removeFile(this, /*fileExists*/ false, /*detachFromProjects*/ false); - p.onFileAddedOrRemoved(); + p.onFileAddedOrRemoved(this.isSymlink()); // If the info was for the external or configured project's root, // add missing file as the root if (existingRoot && !isInferredProject(p)) { From f5df94b92588b535742a3e5885d3a8f215d8373b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 26 Mar 2021 14:05:36 -0700 Subject: [PATCH 34/35] Ensure replacement range is on one line --- src/services/completions.ts | 39 +++++++++++-------- .../fourslash/importStatementCompletions1.ts | 7 +++- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 0532733ebaa6f..3399233374452 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -2921,24 +2921,29 @@ namespace ts.Completions { } function getImportCompletionNode(contextToken: Node) { - const parent = contextToken.parent; - if (isImportEqualsDeclaration(parent)) { - return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined; - } - if (isNamedImports(parent) || isNamespaceImport(parent)) { - return isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name - ? parent.parent.parent - : undefined; - } - if (isImportKeyword(contextToken) && isSourceFile(parent)) { - // A lone import keyword with nothing following it does not parse as a statement at all - return contextToken as Token; - } - if (isImportKeyword(contextToken) && isImportDeclaration(parent)) { - // `import s| from` - return isModuleSpecifierMissingOrEmpty(parent.moduleSpecifier) ? parent : undefined; + const candidate = getCandidate(); + return candidate && rangeIsOnSingleLine(candidate, candidate.getSourceFile()) ? candidate : undefined; + + function getCandidate() { + const parent = contextToken.parent; + if (isImportEqualsDeclaration(parent)) { + return isModuleSpecifierMissingOrEmpty(parent.moduleReference) ? parent : undefined; + } + if (isNamedImports(parent) || isNamespaceImport(parent)) { + return isModuleSpecifierMissingOrEmpty(parent.parent.parent.moduleSpecifier) && (isNamespaceImport(parent) || parent.elements.length < 2) && !parent.parent.name + ? parent.parent.parent + : undefined; + } + if (isImportKeyword(contextToken) && isSourceFile(parent)) { + // A lone import keyword with nothing following it does not parse as a statement at all + return contextToken as Token; + } + if (isImportKeyword(contextToken) && isImportDeclaration(parent)) { + // `import s| from` + return isModuleSpecifierMissingOrEmpty(parent.moduleSpecifier) ? parent : undefined; + } + return undefined; } - return undefined; } function isModuleSpecifierMissingOrEmpty(specifier: ModuleReference | Expression) { diff --git a/tests/cases/fourslash/importStatementCompletions1.ts b/tests/cases/fourslash/importStatementCompletions1.ts index 576dfdbdc9b0d..c9f991339314d 100644 --- a/tests/cases/fourslash/importStatementCompletions1.ts +++ b/tests/cases/fourslash/importStatementCompletions1.ts @@ -58,7 +58,12 @@ // @Filename: /index11.ts //// import oo/*11*/ -[6, 7, 8, 9, 10, 11].forEach(marker => { +// @Filename: /index12.ts +//// import { +//// /*12*/ +//// } + +[6, 7, 8, 9, 10, 11, 12].forEach(marker => { verify.completions({ marker: "" + marker, exact: [], From badcd839b2359138d0e8262a4cce4f6f4251b8c9 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 26 Mar 2021 14:29:13 -0700 Subject: [PATCH 35/35] Update baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 7524ace9b44a1..6cde5031bcc00 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -9410,7 +9410,6 @@ declare namespace ts.server { open(newText: string): void; close(fileExists?: boolean): void; getSnapshot(): IScriptSnapshot; - /** @returns Whether the file was a symlink */ private ensureRealPath; getFormatCodeSettings(): FormatCodeSettings | undefined; getPreferences(): protocol.UserPreferences | undefined;