Skip to content

Commit 328e888

Browse files
authored
Resolve module specifiers for auto imports in completion list (in incomplete chunks) (#44713)
* Enable module specifiers for all auto imports * Use isIncomplete * isIncomplete continuation * Lots of fixes * Merged/transient symbol fixes, resolve all ambient module specifiers up front, pull as many as we want from cache * Fix existing tests * Start testing * Add more tests * Set cache attempt limit, update API baseline * Fix a few tests * Fix contextToken * Split getModuleSpecifiers * Unexport function * Clean up importFixes * Clean up completions * Delete transient symbol assertion - fixing later
1 parent 7c47be7 commit 328e888

19 files changed

+872
-206
lines changed

src/compiler/moduleSpecifiers.ts

+73-19
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,35 @@ namespace ts.moduleSpecifiers {
9090
getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences);
9191
}
9292

93+
export function tryGetModuleSpecifiersFromCache(
94+
moduleSymbol: Symbol,
95+
importingSourceFile: SourceFile,
96+
host: ModuleSpecifierResolutionHost,
97+
userPreferences: UserPreferences,
98+
): readonly string[] | undefined {
99+
return tryGetModuleSpecifiersFromCacheWorker(
100+
moduleSymbol,
101+
importingSourceFile,
102+
host,
103+
userPreferences)[0];
104+
}
105+
106+
function tryGetModuleSpecifiersFromCacheWorker(
107+
moduleSymbol: Symbol,
108+
importingSourceFile: SourceFile,
109+
host: ModuleSpecifierResolutionHost,
110+
userPreferences: UserPreferences,
111+
): readonly [specifiers?: readonly string[], moduleFile?: SourceFile, modulePaths?: readonly ModulePath[], cache?: ModuleSpecifierCache] {
112+
const moduleSourceFile = getSourceFileOfModule(moduleSymbol);
113+
if (!moduleSourceFile) {
114+
return emptyArray as [];
115+
}
116+
117+
const cache = host.getModuleSpecifierCache?.();
118+
const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences);
119+
return [cached?.moduleSpecifiers, moduleSourceFile, cached?.modulePaths, cache];
120+
}
121+
93122
/** Returns an import for each symlink and for the realpath. */
94123
export function getModuleSpecifiers(
95124
moduleSymbol: Symbol,
@@ -99,24 +128,53 @@ namespace ts.moduleSpecifiers {
99128
host: ModuleSpecifierResolutionHost,
100129
userPreferences: UserPreferences,
101130
): readonly string[] {
102-
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker);
103-
if (ambient) return [ambient];
131+
return getModuleSpecifiersWithCacheInfo(
132+
moduleSymbol,
133+
checker,
134+
compilerOptions,
135+
importingSourceFile,
136+
host,
137+
userPreferences,
138+
).moduleSpecifiers;
139+
}
104140

105-
const info = getInfo(importingSourceFile.path, host);
106-
const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration || getNonAugmentationDeclaration(moduleSymbol));
107-
if (!moduleSourceFile) {
108-
return [];
109-
}
141+
export function getModuleSpecifiersWithCacheInfo(
142+
moduleSymbol: Symbol,
143+
checker: TypeChecker,
144+
compilerOptions: CompilerOptions,
145+
importingSourceFile: SourceFile,
146+
host: ModuleSpecifierResolutionHost,
147+
userPreferences: UserPreferences,
148+
): { moduleSpecifiers: readonly string[], computedWithoutCache: boolean } {
149+
let computedWithoutCache = false;
150+
const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol, checker);
151+
if (ambient) return { moduleSpecifiers: [ambient], computedWithoutCache };
110152

111-
const cache = host.getModuleSpecifierCache?.();
112-
const cached = cache?.get(importingSourceFile.path, moduleSourceFile.path, userPreferences);
113-
let modulePaths;
114-
if (cached) {
115-
if (cached.moduleSpecifiers) return cached.moduleSpecifiers;
116-
modulePaths = cached.modulePaths;
117-
}
153+
// eslint-disable-next-line prefer-const
154+
let [specifiers, moduleSourceFile, modulePaths, cache] = tryGetModuleSpecifiersFromCacheWorker(
155+
moduleSymbol,
156+
importingSourceFile,
157+
host,
158+
userPreferences,
159+
);
160+
if (specifiers) return { moduleSpecifiers: specifiers, computedWithoutCache };
161+
if (!moduleSourceFile) return { moduleSpecifiers: emptyArray, computedWithoutCache };
118162

163+
computedWithoutCache = true;
119164
modulePaths ||= getAllModulePathsWorker(importingSourceFile.path, moduleSourceFile.originalFileName, host);
165+
const result = computeModuleSpecifiers(modulePaths, compilerOptions, importingSourceFile, host, userPreferences);
166+
cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, modulePaths, result);
167+
return { moduleSpecifiers: result, computedWithoutCache };
168+
}
169+
170+
function computeModuleSpecifiers(
171+
modulePaths: readonly ModulePath[],
172+
compilerOptions: CompilerOptions,
173+
importingSourceFile: SourceFile,
174+
host: ModuleSpecifierResolutionHost,
175+
userPreferences: UserPreferences,
176+
): readonly string[] {
177+
const info = getInfo(importingSourceFile.path, host);
120178
const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile);
121179
const existingSpecifier = forEach(modulePaths, modulePath => forEach(
122180
host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)),
@@ -131,7 +189,6 @@ namespace ts.moduleSpecifiers {
131189
));
132190
if (existingSpecifier) {
133191
const moduleSpecifiers = [existingSpecifier];
134-
cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, modulePaths, moduleSpecifiers);
135192
return moduleSpecifiers;
136193
}
137194

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

@@ -175,11 +231,9 @@ namespace ts.moduleSpecifiers {
175231
}
176232
}
177233

178-
const moduleSpecifiers = pathsSpecifiers?.length ? pathsSpecifiers :
234+
return pathsSpecifiers?.length ? pathsSpecifiers :
179235
nodeModulesSpecifiers?.length ? nodeModulesSpecifiers :
180236
Debug.checkDefined(relativeSpecifiers);
181-
cache?.set(importingSourceFile.path, moduleSourceFile.path, userPreferences, modulePaths, moduleSpecifiers);
182-
return moduleSpecifiers;
183237
}
184238

185239
interface Info {

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8487,6 +8487,7 @@ namespace ts {
84878487
readonly includeCompletionsWithSnippetText?: boolean;
84888488
readonly includeAutomaticOptionalChainCompletions?: boolean;
84898489
readonly includeCompletionsWithInsertText?: boolean;
8490+
readonly allowIncompleteCompletions?: boolean;
84908491
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
84918492
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
84928493
readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js";

src/compiler/utilities.ts

+4
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ namespace ts {
274274
return node as SourceFile;
275275
}
276276

277+
export function getSourceFileOfModule(module: Symbol) {
278+
return getSourceFileOfNode(module.valueDeclaration || getNonAugmentationDeclaration(module));
279+
}
280+
277281
export function isStatementWithLocals(node: Node) {
278282
switch (node.kind) {
279283
case SyntaxKind.Block:

src/server/editorServices.ts

+22-1
Original file line numberDiff line numberDiff line change
@@ -795,7 +795,8 @@ namespace ts.server {
795795
readonly packageJsonCache: PackageJsonCache;
796796
/*@internal*/
797797
private packageJsonFilesMap: ESMap<Path, FileWatcher> | undefined;
798-
798+
/*@internal*/
799+
private incompleteCompletionsCache: IncompleteCompletionsCache | undefined;
799800
/*@internal*/
800801
readonly session: Session<unknown> | undefined;
801802

@@ -4146,6 +4147,26 @@ namespace ts.server {
41464147
}
41474148
}
41484149
}
4150+
4151+
/*@internal*/
4152+
getIncompleteCompletionsCache() {
4153+
return this.incompleteCompletionsCache ||= createIncompleteCompletionsCache();
4154+
}
4155+
}
4156+
4157+
function createIncompleteCompletionsCache(): IncompleteCompletionsCache {
4158+
let info: CompletionInfo | undefined;
4159+
return {
4160+
get() {
4161+
return info;
4162+
},
4163+
set(newInfo) {
4164+
info = newInfo;
4165+
},
4166+
clear() {
4167+
info = undefined;
4168+
}
4169+
};
41494170
}
41504171

41514172
/* @internal */

src/server/project.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1744,6 +1744,11 @@ namespace ts.server {
17441744
watchNodeModulesForPackageJsonChanges(directoryPath: string) {
17451745
return this.projectService.watchPackageJsonsInNodeModules(this.toPath(directoryPath), this);
17461746
}
1747+
1748+
/*@internal*/
1749+
getIncompleteCompletionsCache() {
1750+
return this.projectService.getIncompleteCompletionsCache();
1751+
}
17471752
}
17481753

17491754
function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: ESMap<Path, readonly string[]>): SortedReadonlyArray<string> {

src/server/protocol.ts

+13
Original file line numberDiff line numberDiff line change
@@ -2146,6 +2146,17 @@ namespace ts.server.protocol {
21462146

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

2149+
export const enum CompletionTriggerKind {
2150+
/** Completion was triggered by typing an identifier, manual invocation (e.g Ctrl+Space) or via API. */
2151+
Invoked = 1,
2152+
2153+
/** Completion was triggered by a trigger character. */
2154+
TriggerCharacter = 2,
2155+
2156+
/** Completion was re-triggered as the current completion list is incomplete. */
2157+
TriggerForIncompleteCompletions = 3,
2158+
}
2159+
21492160
/**
21502161
* Arguments for completions messages.
21512162
*/
@@ -2159,6 +2170,7 @@ namespace ts.server.protocol {
21592170
* Should be `undefined` if a user manually requested completion.
21602171
*/
21612172
triggerCharacter?: CompletionsTriggerCharacter;
2173+
triggerKind?: CompletionTriggerKind;
21622174
/**
21632175
* @deprecated Use UserPreferences.includeCompletionsForModuleExports
21642176
*/
@@ -3373,6 +3385,7 @@ namespace ts.server.protocol {
33733385
* values, with insertion text to replace preceding `.` tokens with `?.`.
33743386
*/
33753387
readonly includeAutomaticOptionalChainCompletions?: boolean;
3388+
readonly allowIncompleteCompletions?: boolean;
33763389
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
33773390
/** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */
33783391
readonly importModuleSpecifierEnding?: "auto" | "minimal" | "index" | "js";

src/server/session.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1841,6 +1841,7 @@ namespace ts.server {
18411841
const completions = project.getLanguageService().getCompletionsAtPosition(file, position, {
18421842
...convertUserPreferences(this.getPreferences(file)),
18431843
triggerCharacter: args.triggerCharacter,
1844+
triggerKind: args.triggerKind as CompletionTriggerKind | undefined,
18441845
includeExternalModuleExports: args.includeExternalModuleExports,
18451846
includeInsertTextCompletions: args.includeInsertTextCompletions
18461847
});

0 commit comments

Comments
 (0)