mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Import statement completions (#43149)
* WIP * WIP * Get completion details working * Start unifying eager and lazy auto imports * Fix export= * Fix completion details for totally misspelled names * Almost fixed duplication... * Fix remaining completion tests * Refactor to support multiple origins for same symbol * Make import fixes make slightly more sense * Add cache back in * Set insertText based on import kind * Update API baselines * Add semicolons, snippet support, and sourceDisplay * Add some tests * Update baselines * Fix pattern ambient modules appearing in auto imports * Fix tests * Remove commented code * Switch to valueDeclaration for getting module source file * Small optimizations * Cache module specifiers / importableness and export map separately * Fix and test cache invalidation logic * Update API baselines * Add separate user preference for snippet-formatted completions * Require first character to match when resolving module specifiers * Fix AutoImportProvider export map cache invalidation * Really fix auto import provider export map invalidation * Update test added in master * Use logical or assignment Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com> * Simply conditional by reversing Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com> * When file is deleted need to marked correctly in the project as removed file * Simplify hasAddedOrRemovedSymlinks with cherry-picked fix * Ensure replacement range is on one line * Update baselines Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com> Co-authored-by: Sheetal Nandi <shkamat@microsoft.com>
This commit is contained in:
co-authored by
Daniel Rosenwasser
Sheetal Nandi
parent
a545ab1ac2
commit
2d6a490363
+13
-6
@@ -3498,7 +3498,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;
|
||||
}
|
||||
@@ -3522,11 +3525,15 @@ 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` is too expensive to use in this auto-imports hot path
|
||||
isArrayType(resolvedExternalModuleType) ||
|
||||
isTupleType(resolvedExternalModuleType));
|
||||
}
|
||||
|
||||
function getExportsOfSymbol(symbol: Symbol): SymbolTable {
|
||||
|
||||
@@ -327,19 +327,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<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>();
|
||||
let importedFileFromNodeModules = false;
|
||||
forEachFileNameOfModule(
|
||||
@@ -358,7 +356,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);
|
||||
@@ -384,6 +382,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8015,6 +8015,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;
|
||||
|
||||
@@ -8025,6 +8026,21 @@ namespace ts {
|
||||
getFileIncludeReasons(): MultiMap<Path, FileIncludeReason>;
|
||||
}
|
||||
|
||||
/* @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 {
|
||||
@@ -8314,6 +8330,8 @@ namespace ts {
|
||||
readonly disableSuggestions?: boolean;
|
||||
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";
|
||||
|
||||
@@ -2412,6 +2412,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);
|
||||
}
|
||||
|
||||
@@ -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 && actual.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,13 @@ 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");
|
||||
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 {
|
||||
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'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1599,6 +1599,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;
|
||||
|
||||
@@ -2958,7 +2958,7 @@ namespace ts.server {
|
||||
});
|
||||
}
|
||||
if (includePackageJsonAutoImports !== args.preferences.includePackageJsonAutoImports) {
|
||||
this.invalidateProjectAutoImports(/*packageJsonPath*/ undefined);
|
||||
this.invalidateProjectPackageJson(/*packageJsonPath*/ undefined);
|
||||
}
|
||||
}
|
||||
if (args.extraFileExtensions) {
|
||||
@@ -4041,7 +4041,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) => {
|
||||
@@ -4051,11 +4051,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);
|
||||
}
|
||||
@@ -4083,15 +4083,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+62
-73
@@ -146,6 +146,8 @@ namespace ts.server {
|
||||
lastCachedUnresolvedImportsList: SortedReadonlyArray<string> | undefined;
|
||||
/*@internal*/
|
||||
private hasAddedorRemovedFiles = false;
|
||||
/*@internal*/
|
||||
private hasAddedOrRemovedSymlinks = false;
|
||||
|
||||
/*@internal*/
|
||||
lastFileExceededProgramSize: string | undefined;
|
||||
@@ -204,7 +206,7 @@ namespace ts.server {
|
||||
originalConfiguredProjects: Set<NormalizedPath> | undefined;
|
||||
|
||||
/*@internal*/
|
||||
packageJsonsForAutoImport: Set<string> | undefined;
|
||||
private packageJsonsForAutoImport: Set<string> | undefined;
|
||||
|
||||
/*@internal*/
|
||||
getResolvedProjectReferenceToRedirect(_fileName: string): ResolvedProjectReference | undefined {
|
||||
@@ -248,9 +250,11 @@ namespace ts.server {
|
||||
public readonly getCanonicalFileName: GetCanonicalFileName;
|
||||
|
||||
/*@internal*/
|
||||
private importSuggestionsCache = Completions.createImportSuggestionsForFileCache();
|
||||
private exportMapCache = createExportMapCache();
|
||||
/*@internal*/
|
||||
private dirtyFilesForSuggestions: Set<Path> | undefined;
|
||||
private changedFilesForExportMapCache: Set<Path> | undefined;
|
||||
/*@internal*/
|
||||
private moduleSpecifierCache = createModuleSpecifierCache();
|
||||
/*@internal*/
|
||||
private symlinks: SymlinkCache | undefined;
|
||||
/*@internal*/
|
||||
@@ -979,8 +983,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 ||= new Set()).add(changedFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,17 +996,31 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
markAutoImportProviderAsDirty() {
|
||||
onAutoImportProviderSettingsChanged() {
|
||||
if (this.autoImportProviderHost === false) {
|
||||
this.autoImportProviderHost = undefined;
|
||||
}
|
||||
this.autoImportProviderHost?.markAsDirty();
|
||||
this.importSuggestionsCache.clear();
|
||||
else {
|
||||
this.autoImportProviderHost?.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
onPackageJsonChange(packageJsonPath: Path) {
|
||||
if (this.packageJsonsForAutoImport?.has(packageJsonPath)) {
|
||||
this.moduleSpecifierCache.clear();
|
||||
if (this.autoImportProviderHost) {
|
||||
this.autoImportProviderHost.markAsDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
onFileAddedOrRemoved() {
|
||||
onFileAddedOrRemoved(isSymlink: boolean | undefined) {
|
||||
this.hasAddedorRemovedFiles = true;
|
||||
if (isSymlink) {
|
||||
this.hasAddedOrRemovedSymlinks = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1016,6 +1034,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;
|
||||
|
||||
@@ -1166,27 +1185,30 @@ 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 => {
|
||||
const oldSourceFile = oldProgram.getSourceFile(fileName);
|
||||
const sourceFile = this.program!.getSourceFile(fileName);
|
||||
if (this.sourceFileHasChangedOwnImportSuggestions(oldSourceFile, sourceFile)) {
|
||||
this.importSuggestionsCache.clear();
|
||||
else if (this.changedFilesForExportMapCache && oldProgram && this.program) {
|
||||
forEachKey(this.changedFilesForExportMapCache, fileName => {
|
||||
const oldSourceFile = oldProgram.getSourceFileByPath(fileName);
|
||||
const sourceFile = this.program!.getSourceFileByPath(fileName);
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray<string>;
|
||||
@@ -1218,54 +1240,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) {
|
||||
@@ -1683,8 +1657,13 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getImportSuggestionsCache() {
|
||||
return this.importSuggestionsCache;
|
||||
getExportMapCache() {
|
||||
return this.exportMapCache;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getModuleSpecifierCache() {
|
||||
return this.moduleSpecifierCache;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
@@ -2016,8 +1995,12 @@ namespace ts.server {
|
||||
|
||||
this.projectService.setFileNamesOfAutoImportProviderProject(this, rootFileNames);
|
||||
this.rootFileNames = rootFileNames;
|
||||
this.hostProject.getImportSuggestionsCache().clear();
|
||||
return super.updateGraph();
|
||||
const oldProgram = this.getCurrentProgram();
|
||||
const hasSameSetOfFiles = super.updateGraph();
|
||||
if (oldProgram && oldProgram !== this.getCurrentProgram()) {
|
||||
this.hostProject.getExportMapCache().clear();
|
||||
}
|
||||
return hasSameSetOfFiles;
|
||||
}
|
||||
|
||||
hasRoots() {
|
||||
@@ -2037,10 +2020,16 @@ namespace ts.server {
|
||||
throw new Error("AutoImportProviderProject language service should never be used. To get the program, use `project.getCurrentProgram()`.");
|
||||
}
|
||||
|
||||
markAutoImportProviderAsDirty(): never {
|
||||
/*@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.");
|
||||
}
|
||||
|
||||
+25
-2
@@ -2142,7 +2142,7 @@ namespace ts.server.protocol {
|
||||
arguments: FormatOnKeyRequestArgs;
|
||||
}
|
||||
|
||||
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
|
||||
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
|
||||
|
||||
/**
|
||||
* Arguments for completions messages.
|
||||
@@ -2253,6 +2253,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.
|
||||
@@ -2268,6 +2272,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.
|
||||
@@ -2329,9 +2337,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. */
|
||||
@@ -2353,6 +2366,7 @@ namespace ts.server.protocol {
|
||||
* must be used to commit that completion entry.
|
||||
*/
|
||||
readonly optionalReplacementSpan?: TextSpan;
|
||||
readonly isIncomplete?: boolean;
|
||||
readonly entries: readonly CompletionEntry[];
|
||||
}
|
||||
|
||||
@@ -3298,6 +3312,15 @@ 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;
|
||||
/**
|
||||
* 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"]`.
|
||||
|
||||
@@ -415,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; }
|
||||
|
||||
@@ -422,10 +431,10 @@ namespace ts.server {
|
||||
const isNew = !this.isAttached(project);
|
||||
if (isNew) {
|
||||
this.containingProjects.push(project);
|
||||
project.onFileAddedOrRemoved();
|
||||
if (!project.getCompilerOptions().preserveSymlinks) {
|
||||
this.ensureRealPath();
|
||||
}
|
||||
project.onFileAddedOrRemoved(this.isSymlink());
|
||||
}
|
||||
return isNew;
|
||||
}
|
||||
@@ -447,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;
|
||||
}
|
||||
@@ -477,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)) {
|
||||
|
||||
@@ -1838,10 +1838,10 @@ namespace ts.server {
|
||||
const prefix = args.prefix || "";
|
||||
const entries = stableSort(mapDefined<CompletionEntry, protocol.CompletionEntry>(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));
|
||||
|
||||
|
||||
@@ -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,21 +177,7 @@ namespace ts.codefix {
|
||||
readonly importKind: ImportKind;
|
||||
readonly typeOnly: boolean;
|
||||
readonly useRequire: boolean;
|
||||
}
|
||||
|
||||
const enum ImportKind {
|
||||
Named,
|
||||
Default,
|
||||
Namespace,
|
||||
CommonJS,
|
||||
}
|
||||
|
||||
/** Information about how a symbol is exported from a module. (We don't need to store the exported symbol, just its module.) */
|
||||
interface SymbolExportInfo {
|
||||
readonly moduleSymbol: Symbol;
|
||||
readonly importKind: ImportKind;
|
||||
/** If true, can't use an es6 import from a js file. */
|
||||
readonly exportedSymbolIsTypeOnly: boolean;
|
||||
readonly exportInfo?: SymbolExportInfo;
|
||||
}
|
||||
|
||||
/** Information needed to augment an existing import declaration. */
|
||||
@@ -211,42 +199,40 @@ 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));
|
||||
const moduleSpecifier = getBestFix(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences), sourceFile, program, host).moduleSpecifier;
|
||||
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");
|
||||
// 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);
|
||||
return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, host);
|
||||
}
|
||||
|
||||
function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction {
|
||||
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());
|
||||
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 {
|
||||
const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions);
|
||||
function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined {
|
||||
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
|
||||
if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) {
|
||||
return { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker) };
|
||||
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 { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker) };
|
||||
return { symbol: named, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,25 +240,92 @@ 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) => {
|
||||
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))) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) });
|
||||
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
|
||||
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) {
|
||||
result.push({ moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker) });
|
||||
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 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) {
|
||||
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?.());
|
||||
if (cached) {
|
||||
host.log?.("getSymbolToExportInfoMap: cache hit");
|
||||
return cached;
|
||||
}
|
||||
else {
|
||||
host.log?.("getSymbolToExportInfoMap: cache miss or empty; calculating new results");
|
||||
}
|
||||
}
|
||||
|
||||
const result: MultiMap<string, SymbolExportInfo> = createMultiMap();
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
const target = getEmitScriptTarget(compilerOptions);
|
||||
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, _moduleFile, program, isFromPackageJson) => {
|
||||
const checker = program.getTypeChecker();
|
||||
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, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson });
|
||||
}
|
||||
const seenExports = new Map<Symbol, true>();
|
||||
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, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (cache) {
|
||||
host.log?.("getSymbolToExportInfoMap: caching results");
|
||||
cache.set(result, host.getProjectVersion?.());
|
||||
}
|
||||
host.log?.(`getSymbolToExportInfoMap: done in ${timestamp() - start} ms`);
|
||||
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 {
|
||||
@@ -283,7 +336,7 @@ namespace ts.codefix {
|
||||
return isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
|
||||
}
|
||||
|
||||
function getFixForImport(
|
||||
function getImportFixes(
|
||||
exportInfos: readonly SymbolExportInfo[],
|
||||
symbolName: string,
|
||||
/** undefined only for missing JSX namespace */
|
||||
@@ -296,7 +349,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.
|
||||
@@ -319,10 +372,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 };
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -364,17 +418,19 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -412,7 +468,7 @@ namespace ts.codefix {
|
||||
return true;
|
||||
}
|
||||
|
||||
function getNewImportInfos(
|
||||
function getNewImportFixes(
|
||||
program: Program,
|
||||
sourceFile: SourceFile,
|
||||
position: number | undefined,
|
||||
@@ -424,13 +480,14 @@ namespace ts.codefix {
|
||||
): readonly (FixAddNewImport | FixUseImportType)[] {
|
||||
const isJs = isSourceFileJS(sourceFile);
|
||||
const compilerOptions = program.getCompilerOptions();
|
||||
return flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) =>
|
||||
moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, createModuleSpecifierResolutionHost(program, host), preferences)
|
||||
const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host);
|
||||
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
|
||||
? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.checkDefined(position, "position should be defined") }
|
||||
: { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, useRequire, typeOnly: preferTypeOnlyImport }));
|
||||
exportInfo.exportedSymbolIsTypeOnly && isJs && position !== undefined
|
||||
? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo }
|
||||
: { kind: ImportFixKind.AddNew, moduleSpecifier, importKind: getImportKind(sourceFile, exportInfo.exportKind, compilerOptions), useRequire, typeOnly: preferTypeOnlyImport, exportInfo }));
|
||||
}
|
||||
|
||||
function getFixesForAddImport(
|
||||
@@ -445,16 +502,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;
|
||||
}
|
||||
|
||||
@@ -464,20 +518,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<T extends ImportFix>(fixes: readonly T[], sourceFile: SourceFile, program: Program, host: LanguageServiceHost): T {
|
||||
function getBestFix<T extends ImportFix>(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
|
||||
);
|
||||
@@ -497,9 +551,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 }];
|
||||
const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, exportKind: ExportKind.UMD, 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 {
|
||||
@@ -514,6 +568,16 @@ namespace ts.codefix {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export 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)) {
|
||||
@@ -553,7 +617,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 };
|
||||
}
|
||||
|
||||
@@ -573,7 +637,7 @@ namespace ts.codefix {
|
||||
symbolName: string,
|
||||
currentTokenMeaning: SemanticMeaning,
|
||||
cancellationToken: CancellationToken,
|
||||
sourceFile: SourceFile,
|
||||
fromFile: SourceFile,
|
||||
program: Program,
|
||||
useAutoImportProvider: boolean,
|
||||
host: LanguageServiceHost
|
||||
@@ -581,43 +645,52 @@ 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<SymbolExportInfo>();
|
||||
function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind, checker: TypeChecker): void {
|
||||
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker) });
|
||||
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) => {
|
||||
forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, 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.kind, checker);
|
||||
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, ImportKind.Named, checker);
|
||||
addSymbol(moduleSymbol, sourceFile, exportSymbolWithIdenticalName, ExportKind.Named, program, 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 } | 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 } = exported;
|
||||
const { symbol, exportKind } = exported;
|
||||
const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions);
|
||||
return info && { symbol, kind, ...info };
|
||||
return info && { symbol, exportKind, ...info };
|
||||
}
|
||||
|
||||
function getDefaultLikeExportWorker(importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbol: Symbol, readonly kind: ImportKind } | undefined {
|
||||
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
|
||||
if (defaultExport) return { symbol: defaultExport, kind: ImportKind.Default };
|
||||
function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined {
|
||||
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
|
||||
return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, kind: getExportEqualsImportKind(importingFile, compilerOptions) };
|
||||
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
|
||||
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
|
||||
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
|
||||
}
|
||||
|
||||
function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind {
|
||||
@@ -636,7 +709,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;
|
||||
}
|
||||
}
|
||||
@@ -880,57 +954,23 @@ 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()) {
|
||||
cb(ambient, /*sourceFile*/ undefined);
|
||||
if (!stringContains(ambient.name, "*")) {
|
||||
cb(ambient, /*sourceFile*/ undefined);
|
||||
}
|
||||
}
|
||||
for (const sourceFile of allSourceFiles) {
|
||||
if (isExternalOrCommonJsModule(sourceFile)) {
|
||||
@@ -939,42 +979,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);
|
||||
}
|
||||
@@ -1005,117 +1009,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 || !moduleSymbol.valueDeclaration) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+392
-334
File diff suppressed because it is too large
Load Diff
@@ -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, preferences);
|
||||
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 };
|
||||
}
|
||||
|
||||
+16
-3
@@ -305,7 +305,9 @@ namespace ts {
|
||||
/* @internal */
|
||||
getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[];
|
||||
/* @internal */
|
||||
getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache;
|
||||
getExportMapCache?(): ExportMapCache;
|
||||
/* @internal */
|
||||
getModuleSpecifierCache?(): ModuleSpecifierCache;
|
||||
/* @internal */
|
||||
setCompilerHost?(host: CompilerHost): void;
|
||||
/* @internal */
|
||||
@@ -552,7 +554,7 @@ namespace ts {
|
||||
|
||||
export type OrganizeImportsScope = CombinedCodeFixScope;
|
||||
|
||||
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
|
||||
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
|
||||
|
||||
export interface GetCompletionsAtPositionOptions extends UserPreferences {
|
||||
/**
|
||||
@@ -1140,11 +1142,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[];
|
||||
}
|
||||
|
||||
@@ -1160,6 +1165,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
|
||||
@@ -1169,6 +1178,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.
|
||||
@@ -1177,6 +1187,7 @@ namespace ts {
|
||||
replacementSpan?: TextSpan;
|
||||
hasAction?: true;
|
||||
source?: string;
|
||||
sourceDisplay?: SymbolDisplayPart[];
|
||||
isRecommended?: true;
|
||||
isFromUncheckedFile?: true;
|
||||
isPackageJsonImport?: true;
|
||||
@@ -1199,7 +1210,9 @@ namespace ts {
|
||||
documentation?: SymbolDisplayPart[];
|
||||
tags?: JSDocTagInfo[];
|
||||
codeActions?: CodeAction[];
|
||||
/** @deprecated Use `sourceDisplay` instead. */
|
||||
source?: SymbolDisplayPart[];
|
||||
sourceDisplay?: SymbolDisplayPart[];
|
||||
}
|
||||
|
||||
export interface OutliningSpan {
|
||||
|
||||
+327
-1
@@ -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,
|
||||
@@ -2846,6 +2847,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 || !moduleSymbol.valueDeclaration) {
|
||||
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);
|
||||
@@ -2994,5 +3114,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<string, SymbolExportInfo> | undefined;
|
||||
set(suggestions: MultiMap<string, SymbolExportInfo>, 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<string, SymbolExportInfo> | 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 (usableByFileName && 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) {
|
||||
Object.defineProperty(wrapped, "__cache", { get: () => 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<Path, boolean | readonly ModulePath[]> | 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) {
|
||||
Object.defineProperty(wrapped, "__cache", { get: () => 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
|
||||
}
|
||||
|
||||
@@ -169,6 +169,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",
|
||||
@@ -177,12 +178,12 @@
|
||||
"unittests/tsserver/getExportReferences.ts",
|
||||
"unittests/tsserver/getFileReferences.ts",
|
||||
"unittests/tsserver/importHelpers.ts",
|
||||
"unittests/tsserver/importSuggestionsCache.ts",
|
||||
"unittests/tsserver/inferredProjects.ts",
|
||||
"unittests/tsserver/jsdocTag.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",
|
||||
|
||||
@@ -39,7 +39,9 @@ namespace ts.projectSystem {
|
||||
isPackageJsonImport: undefined,
|
||||
sortText: Completions.SortText.AutoImportSuggestions,
|
||||
source: "/a",
|
||||
data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }
|
||||
sourceDisplay: undefined,
|
||||
isSnippet: undefined,
|
||||
data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined, moduleSpecifier: undefined }
|
||||
};
|
||||
assert.deepEqual<protocol.CompletionInfo | undefined>(response, {
|
||||
isGlobalCompletion: true,
|
||||
@@ -69,6 +71,7 @@ namespace ts.projectSystem {
|
||||
kindModifiers: ScriptElementKindModifier.exportedModifier,
|
||||
name: "foo",
|
||||
source: [{ text: "./a", kind: "text" }],
|
||||
sourceDisplay: [{ text: "./a", kind: "text" }],
|
||||
};
|
||||
assert.deepEqual<readonly protocol.CompletionEntryDetails[] | undefined>(detailsResponse, [
|
||||
{
|
||||
|
||||
@@ -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<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
|
||||
...requestLocation,
|
||||
includeExternalModuleExports: true,
|
||||
prefix: "foo",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, 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));
|
||||
});
|
||||
|
||||
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, checker));
|
||||
});
|
||||
|
||||
it("invalidates 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));
|
||||
});
|
||||
});
|
||||
|
||||
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<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
|
||||
...requestLocation,
|
||||
includeExternalModuleExports: true,
|
||||
prefix: "foo",
|
||||
});
|
||||
const checker = project.getLanguageService().getProgram()!.getTypeChecker();
|
||||
return { host, project, projectService, importSuggestionsCache: project.getImportSuggestionsCache(), checker };
|
||||
}
|
||||
}
|
||||
@@ -592,6 +592,7 @@ foo`
|
||||
kindModifiers: "",
|
||||
name: "foo",
|
||||
source: undefined,
|
||||
sourceDisplay: undefined,
|
||||
tags,
|
||||
}]);
|
||||
}
|
||||
|
||||
@@ -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<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
|
||||
...requestLocation,
|
||||
includeExternalModuleExports: true,
|
||||
prefix: "foo",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,8 @@ import { something } from "something";
|
||||
replacementSpan: undefined,
|
||||
source: undefined,
|
||||
data: undefined,
|
||||
sourceDisplay: undefined,
|
||||
isSnippet: undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
+39
-4
@@ -3905,6 +3905,8 @@ declare namespace ts {
|
||||
readonly disableSuggestions?: boolean;
|
||||
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";
|
||||
@@ -5654,7 +5656,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
|
||||
@@ -6140,6 +6142,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 {
|
||||
@@ -6154,6 +6160,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;
|
||||
@@ -6161,6 +6171,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.
|
||||
@@ -6169,6 +6180,7 @@ declare namespace ts {
|
||||
replacementSpan?: TextSpan;
|
||||
hasAction?: true;
|
||||
source?: string;
|
||||
sourceDisplay?: SymbolDisplayPart[];
|
||||
isRecommended?: true;
|
||||
isFromUncheckedFile?: true;
|
||||
isPackageJsonImport?: true;
|
||||
@@ -6190,7 +6202,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. */
|
||||
@@ -8214,7 +8228,7 @@ declare namespace ts.server.protocol {
|
||||
command: CommandTypes.Formatonkey;
|
||||
arguments: FormatOnKeyRequestArgs;
|
||||
}
|
||||
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
|
||||
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
|
||||
/**
|
||||
* Arguments for completions messages.
|
||||
*/
|
||||
@@ -8316,6 +8330,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.
|
||||
@@ -8331,6 +8349,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.
|
||||
@@ -8387,9 +8409,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 {
|
||||
@@ -8408,6 +8434,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 {
|
||||
@@ -9190,6 +9217,15 @@ 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;
|
||||
/**
|
||||
* 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"]`.
|
||||
@@ -9617,7 +9653,6 @@ declare namespace ts.server {
|
||||
markAsDirty(): void;
|
||||
getScriptFileNames(): string[];
|
||||
getLanguageService(): never;
|
||||
markAutoImportProviderAsDirty(): never;
|
||||
getModuleResolutionHostForAutoImportProvider(): never;
|
||||
getProjectReferences(): readonly ProjectReference[] | undefined;
|
||||
getTypeAcquisition(): TypeAcquisition;
|
||||
|
||||
+15
-1
@@ -3905,6 +3905,8 @@ declare namespace ts {
|
||||
readonly disableSuggestions?: boolean;
|
||||
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";
|
||||
@@ -5654,7 +5656,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
|
||||
@@ -6140,6 +6142,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 {
|
||||
@@ -6154,6 +6160,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;
|
||||
@@ -6161,6 +6171,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.
|
||||
@@ -6169,6 +6180,7 @@ declare namespace ts {
|
||||
replacementSpan?: TextSpan;
|
||||
hasAction?: true;
|
||||
source?: string;
|
||||
sourceDisplay?: SymbolDisplayPart[];
|
||||
isRecommended?: true;
|
||||
isFromUncheckedFile?: true;
|
||||
isPackageJsonImport?: true;
|
||||
@@ -6190,7 +6202,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. */
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
///<reference path="fourslash.ts" />
|
||||
|
||||
////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");
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
////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"`,
|
||||
@@ -15,3 +18,17 @@ verify.applyCodeActionFromCompletion("", {
|
||||
|
||||
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";
|
||||
|
||||
acb;`,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -621,6 +621,8 @@ declare namespace FourSlashInterface {
|
||||
interface UserPreferences {
|
||||
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";
|
||||
@@ -650,6 +652,7 @@ declare namespace FourSlashInterface {
|
||||
readonly kindModifiers?: string;
|
||||
readonly sortText?: completion.SortText;
|
||||
readonly isPackageJsonImport?: boolean;
|
||||
readonly isSnippet?: boolean;
|
||||
|
||||
// details
|
||||
readonly text?: string;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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,
|
||||
includeCompletionsWithSnippetText: 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";
|
||||
|
||||
// @Filename: /index11.ts
|
||||
//// import oo/*11*/
|
||||
|
||||
// @Filename: /index12.ts
|
||||
//// import {
|
||||
//// /*12*/
|
||||
//// }
|
||||
|
||||
[6, 7, 8, 9, 10, 11, 12].forEach(marker => {
|
||||
verify.completions({
|
||||
marker: "" + marker,
|
||||
exact: [],
|
||||
preferences: {
|
||||
includeCompletionsForImportStatements: true,
|
||||
includeInsertTextCompletions: true,
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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,
|
||||
includeCompletionsWithSnippetText: true,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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,
|
||||
includeCompletionsWithSnippetText: true,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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,
|
||||
includeCompletionsWithSnippetText: true,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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,
|
||||
includeCompletionsWithSnippetText: true,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @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,
|
||||
includeCompletionsWithSnippetText: true,
|
||||
}
|
||||
});
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user