Skip to content

Resolve module specifiers for auto imports in completion list (in incomplete chunks) #44713

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 73 additions & 19 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,35 @@ namespace ts.moduleSpecifiers {
getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences);
}

export function tryGetModuleSpecifiersFromCache(
moduleSymbol: Symbol,
importingSourceFile: SourceFile,
host: ModuleSpecifierResolutionHost,
userPreferences: UserPreferences,
): readonly string[] | undefined {
return tryGetModuleSpecifiersFromCacheWorker(
moduleSymbol,
importingSourceFile,
host,
userPreferences)[0];
}

function tryGetModuleSpecifiersFromCacheWorker(
moduleSymbol: Symbol,
importingSourceFile: SourceFile,
host: ModuleSpecifierResolutionHost,
userPreferences: UserPreferences,
): readonly [specifiers?: readonly string[], moduleFile?: SourceFile, modulePaths?: readonly ModulePath[], cache?: ModuleSpecifierCache] {
const moduleSourceFile = getSourceFileOfModule(moduleSymbol);
if (!moduleSourceFile) {
return emptyArray as [];
}

const cache = host.getModuleSpecifierCache?.();
const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences);
return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache];
}

/** Returns an import for each symlink and for the realpath. */
export function getModuleSpecifiers(
moduleSymbol: Symbol,
Expand All @@ -99,24 +128,53 @@ namespace ts.moduleSpecifiers {
host: ModuleSpecifierResolutionHost,
userPreferences: UserPreferences,
): readonly string[] {
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker);
if (ambient) return [ambient];
return getModuleSpecifiersWithCacheInfo(
moduleSymbol,
checker,
compilerOptions,
importingSourceFile,
host,
userPreferences,
).moduleSpecifiers;
}

const info = getInfo(importingSourceFile.path, host);
const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol));
if (!moduleSourceFile) {
return [];
}
export function getModuleSpecifiersWithCacheInfo(
moduleSymbol: Symbol,
checker: TypeChecker,
compilerOptions: CompilerOptions,
importingSourceFile: SourceFile,
host: ModuleSpecifierResolutionHost,
userPreferences: UserPreferences,
): { moduleSpecifiers: readonly string[], computedWithoutCache: boolean } {
let computedWithoutCache = false;
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker);
if (ambient) return { moduleSpecifiers: [ambient], computedWithoutCache };

const cache = host.getModuleSpecifierCache?.();
const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences);
let modulePaths;
if (cached) {
if (cached.moduleSpecifiers) return cached.moduleSpecifiers;
modulePaths = cached.modulePaths;
}
// eslint-disable-next-line prefer-const
let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker(
moduleSymbol,
importingSourceFile,
host,
userPreferences,
);
if (specifiers) return { moduleSpecifiers: specifiers, computedWithoutCache };
if (!moduleSourceFile) return { moduleSpecifiers: emptyArray, computedWithoutCache };

computedWithoutCache = true;
modulePaths ||= getAllModulePathsWorker(importingSourceFile.path, moduleSourceFile.originalFileName, host);
const result = computeModuleSpecifiers(modulePaths, compilerOptions, importingSourceFile, host, userPreferences);
cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, modulePaths, result);
return { moduleSpecifiers: result, computedWithoutCache };
}

function computeModuleSpecifiers(
modulePaths: readonly ModulePath[],
compilerOptions: CompilerOptions,
importingSourceFile: SourceFile,
host: ModuleSpecifierResolutionHost,
userPreferences: UserPreferences,
): readonly string[] {
const info = getInfo(importingSourceFile.path, host);
const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile);
const existingSpecifier = forEach(modulePaths, modulePath => forEach(
host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)),
Expand All @@ -131,7 +189,6 @@ namespace ts.moduleSpecifiers {
));
if (existingSpecifier) {
const moduleSpecifiers = [existingSpecifier];
cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, modulePaths, moduleSpecifiers);
return moduleSpecifiers;
}

Expand All @@ -151,7 +208,6 @@ namespace ts.moduleSpecifiers {
if (specifier && modulePath.isRedirect) {
// If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar",
// not "@foo/bar/path/to/file"). No other specifier will be this good, so stop looking.
cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, modulePaths, nodeModulesSpecifiers!);
return nodeModulesSpecifiers!;
}

Expand All @@ -175,11 +231,9 @@ namespace ts.moduleSpecifiers {
}
}

const moduleSpecifiers = pathsSpecifiers?.length ? pathsSpecifiers :
return pathsSpecifiers?.length ? pathsSpecifiers :
nodeModulesSpecifiers?.length ? nodeModulesSpecifiers :
Debug.checkDefined(relativeSpecifiers);
cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, modulePaths, moduleSpecifiers);
return moduleSpecifiers;
}

interface Info {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8474,6 +8474,7 @@ namespace ts {
readonly includeCompletionsWithSnippetText?: boolean;
readonly includeAutomaticOptionalChainCompletions?: boolean;
readonly includeCompletionsWithInsertText?: boolean;
readonly allowIncompleteCompletions?: boolean;
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js";
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ namespace ts {
return node as SourceFile;
}

export function getSourceFileOfModule(module: Symbol) {
return getSourceFileOfNode(module.valueDeclaration || getNonAugmentationDeclaration(module));
}

export function isStatementWithLocals(node: Node) {
switch (node.kind) {
case SyntaxKind.Block:
Expand Down
23 changes: 22 additions & 1 deletion src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,8 @@ namespace ts.server {
readonly packageJsonCache: PackageJsonCache;
/*@internal*/
private packageJsonFilesMap: ESMap<Path, FileWatcher> | undefined;

/*@internal*/
private incompleteCompletionsCache: IncompleteCompletionsCache | undefined;
/*@internal*/
readonly session: Session<unknown> | undefined;

Expand Down Expand Up @@ -4146,6 +4147,26 @@ namespace ts.server {
}
}
}

/*@internal*/
getIncompleteCompletionsCache() {
return this.incompleteCompletionsCache ||= createIncompleteCompletionsCache();
}
}

function createIncompleteCompletionsCache(): IncompleteCompletionsCache {
let info: CompletionInfo | undefined;
return {
get() {
return info;
},
set(newInfo) {
info = newInfo;
},
clear() {
info = undefined;
}
};
}

/* @internal */
Expand Down
5 changes: 5 additions & 0 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,11 @@ namespace ts.server {
watchNodeModulesForPackageJsonChanges(directoryPath: string) {
return this.projectService.watchPackageJsonsInNodeModules(this.toPath(directoryPath), this);
}

/*@internal*/
getIncompleteCompletionsCache() {
return this.projectService.getIncompleteCompletionsCache();
}
}

function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: ESMap<Path, readonly string[]>): SortedReadonlyArray<string> {
Expand Down
13 changes: 13 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2145,6 +2145,17 @@ namespace ts.server.protocol {

export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";

export const enum CompletionTriggerKind {
/** Completion was triggered by typing an identifier, manual invocation (e.g Ctrl+Space) or via API. */
Invoked = 1,

/** Completion was triggered by a trigger character. */
TriggerCharacter = 2,

/** Completion was re-triggered as the current completion list is incomplete. */
TriggerForIncompleteCompletions = 3,
}

/**
* Arguments for completions messages.
*/
Expand All @@ -2158,6 +2169,7 @@ namespace ts.server.protocol {
* Should be `undefined` if a user manually requested completion.
*/
triggerCharacter?: CompletionsTriggerCharacter;
triggerKind?: CompletionTriggerKind;
/**
* @deprecated Use UserPreferences.includeCompletionsForModuleExports
*/
Expand Down Expand Up @@ -3338,6 +3350,7 @@ namespace ts.server.protocol {
* values, with insertion text to replace preceding `.` tokens with `?.`.
*/
readonly includeAutomaticOptionalChainCompletions?: boolean;
readonly allowIncompleteCompletions?: boolean;
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js";
Expand Down
1 change: 1 addition & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,7 @@ namespace ts.server {
const completions = project.getLanguageService().getCompletionsAtPosition(file, position, {
...convertUserPreferences(this.getPreferences(file)),
triggerCharacter: args.triggerCharacter,
triggerKind: args.triggerKind as CompletionTriggerKind | undefined,
includeExternalModuleExports: args.includeExternalModuleExports,
includeInsertTextCompletions: args.includeInsertTextCompletions
});
Expand Down
Loading