Skip to content
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

Experiment: auto-import from yet-unimported referenced projects #47690

4 changes: 2 additions & 2 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3918,8 +3918,8 @@ namespace ts {
if (!symlinks) {
symlinks = createSymlinkCache(currentDirectory, getCanonicalFileName);
}
if (files && resolvedTypeReferenceDirectives && !symlinks.hasProcessedResolutions()) {
symlinks.setSymlinksFromResolutions(files, resolvedTypeReferenceDirectives);
if (files && resolvedTypeReferenceDirectives && !symlinks.hasProcessedResolutions(/*projectName*/ undefined)) {
symlinks.setSymlinksFromResolutions(/*projectName*/ undefined, files, resolvedTypeReferenceDirectives);
}
return symlinks;
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8683,6 +8683,7 @@ namespace ts {
readonly allowTextChangesInNewFiles?: boolean;
readonly providePrefixAndSuffixTextForRename?: boolean;
readonly includePackageJsonAutoImports?: "auto" | "on" | "off";
readonly includeProjectReferenceAutoImports?: "on" | "off";
readonly provideRefactorNotApplicableReason?: boolean;
readonly jsxAttributeCompletionStyle?: "auto" | "braces" | "none";
}
Expand Down
35 changes: 27 additions & 8 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6376,19 +6376,19 @@ namespace ts {
* don't include automatic type reference directives. Must be called only when
* `hasProcessedResolutions` returns false (once per cache instance).
*/
setSymlinksFromResolutions(files: readonly SourceFile[], typeReferenceDirectives: ReadonlyESMap<string, ResolvedTypeReferenceDirective | undefined> | undefined): void;
setSymlinksFromResolutions(projectName: string | undefined, files: readonly SourceFile[], typeReferenceDirectives: ReadonlyESMap<string, ResolvedTypeReferenceDirective | undefined> | undefined): void;
/**
* @internal
* Whether `setSymlinksFromResolutions` has already been called.
* Whether `setSymlinksFromResolutions` has already been called for the given project.
*/
hasProcessedResolutions(): boolean;
hasProcessedResolutions(projectName: string | undefined): boolean;
}

export function createSymlinkCache(cwd: string, getCanonicalFileName: GetCanonicalFileName): SymlinkCache {
let symlinkedDirectories: ESMap<Path, SymlinkedDirectory | false> | undefined;
let symlinkedDirectoriesByRealpath: MultiMap<Path, string> | undefined;
let symlinkedFiles: ESMap<Path, string> | undefined;
let hasProcessedResolutions = false;
let hasProcessedResolutions: Set<string> | boolean | undefined;
return {
getSymlinkedFiles: () => symlinkedFiles,
getSymlinkedDirectories: () => symlinkedDirectories,
Expand All @@ -6407,15 +6407,34 @@ namespace ts {
(symlinkedDirectories || (symlinkedDirectories = new Map())).set(symlinkPath, real);
}
},
setSymlinksFromResolutions(files, typeReferenceDirectives) {
Debug.assert(!hasProcessedResolutions);
hasProcessedResolutions = true;
setSymlinksFromResolutions(projectName, files, typeReferenceDirectives) {
if (projectName === undefined) {
Debug.assert(!hasProcessedResolutions, "setSymlinksFromResolutions should only be called once per cache instance");
hasProcessedResolutions = true;
}
else {
Debug.assert(hasProcessedResolutions === undefined || typeof hasProcessedResolutions === "object");
((hasProcessedResolutions ||= new Set()) as Set<string>).add(projectName); // #44553
}
for (const file of files) {
file.resolvedModules?.forEach(resolution => processResolution(this, resolution));
}
typeReferenceDirectives?.forEach(resolution => processResolution(this, resolution));
},
hasProcessedResolutions: () => hasProcessedResolutions,
hasProcessedResolutions: projectName => {
if (projectName !== undefined) {
Debug.assert(
hasProcessedResolutions === undefined || typeof hasProcessedResolutions === "object",
"hasProcessedResolutions was called with a project name, but setSymlinksFromResolutions was not");
return !!hasProcessedResolutions?.has(projectName);
}
else {
Debug.assert(
hasProcessedResolutions === undefined || typeof hasProcessedResolutions === "boolean",
"hasProcessedResolutions was called without a project name, but setSymlinksFromResolutions was called with one");
return !!hasProcessedResolutions;
}
},
};

function processResolution(cache: SymlinkCache, resolution: ResolvedModuleFull | ResolvedTypeReferenceDirective | undefined) {
Expand Down
4 changes: 1 addition & 3 deletions src/harness/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,12 @@ namespace ts.server {
}
}

// verify the sequence numbers
Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number.");

// unmarshal errors
if (!response.success) {
throw new Error("Error " + response.message);
}

Debug.assert(response.request_seq === request.seq, "Malformed response: response sequence number did not match request sequence number.");
Debug.assert(expectEmptyBody || !!response.body, "Malformed response: Unexpected empty response body.");
Debug.assert(!expectEmptyBody || !response.body, "Malformed response: Unexpected non-empty response body.");

Expand Down
9 changes: 8 additions & 1 deletion src/harness/fourslashImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,9 @@ namespace FourSlash {
if (expected.isPackageJsonImport !== undefined) {
assert.equal<boolean | undefined>(actual.isPackageJsonImport, expected.isPackageJsonImport, "Expected 'isPackageJsonImport' properties to match");
}
if (expected.data !== undefined) {
assert.deepEqual(actual.data, expected.data);
}

assert.equal(actual.hasAction, expected.hasAction, `Expected 'hasAction' properties to match`);
assert.equal(actual.isRecommended, expected.isRecommended, `Expected 'isRecommended' properties to match'`);
Expand Down Expand Up @@ -3146,9 +3149,13 @@ namespace FourSlash {
});
}

public verifyImportFixModuleSpecifiers(markerName: string, moduleSpecifiers: string[]) {
public verifyImportFixModuleSpecifiers(markerName: string, moduleSpecifiers: string[], options?: ts.UserPreferences) {
if (options) {
this.configure(options);
}
const marker = this.getMarkerByName(markerName);
const codeFixes = this.getCodeFixes(marker.fileName, ts.Diagnostics.Cannot_find_name_0.code, {
...options,
includeCompletionsForModuleExports: true,
includeCompletionsWithInsertText: true
}, marker.position).filter(f => f.fixName === ts.codefix.importFixName);
Expand Down
5 changes: 3 additions & 2 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,8 @@ namespace FourSlashInterface {
this.state.verifyImportFixAtPosition(expectedTextArray, errorCode, preferences);
}

public importFixModuleSpecifiers(marker: string, moduleSpecifiers: string[]) {
this.state.verifyImportFixModuleSpecifiers(marker, moduleSpecifiers);
public importFixModuleSpecifiers(marker: string, moduleSpecifiers: string[], options?: ts.UserPreferences) {
this.state.verifyImportFixModuleSpecifiers(marker, moduleSpecifiers, options);
}

public navigationBar(json: any, options?: { checkSpans?: boolean }) {
Expand Down Expand Up @@ -1694,6 +1694,7 @@ namespace FourSlashInterface {
readonly sourceDisplay?: string;
readonly tags?: readonly ts.JSDocTagInfo[];
readonly sortText?: ts.Completions.SortText;
readonly data?: ts.CompletionEntryData;
}

export type ExpectedExactCompletionsPlus = readonly ExpectedCompletionEntry[] & {
Expand Down
17 changes: 16 additions & 1 deletion src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,21 @@ namespace ts.server {
if (!this.symlinks) {
this.symlinks = createSymlinkCache(this.getCurrentDirectory(), this.getCanonicalFileName);
}
if (this.program && !this.symlinks.hasProcessedResolutions()) {
if (this.program && !this.symlinks.hasProcessedResolutions(this.projectName)) {
this.symlinks.setSymlinksFromResolutions(
this.projectName,
this.program.getSourceFiles(),
this.program.getResolvedTypeReferenceDirectives());
}
if (typeof this.autoImportProviderHost === "object" &&
this.autoImportProviderHost.program &&
!this.symlinks.hasProcessedResolutions(this.autoImportProviderHost.projectName)
) {
this.symlinks.setSymlinksFromResolutions(
this.autoImportProviderHost.projectName,
this.autoImportProviderHost.program.getSourceFiles(),
this.autoImportProviderHost.program.getResolvedTypeReferenceDirectives());
}
return this.symlinks;
}

Expand Down Expand Up @@ -1768,6 +1778,11 @@ namespace ts.server {
getIncompleteCompletionsCache() {
return this.projectService.getIncompleteCompletionsCache();
}

/*@internal*/
getProgramForReferencedProject(configFileName: string) {
return this.projectService.findConfiguredProjectByProjectName(toNormalizedPath(configFileName))?.getCurrentProgram();
amcasey marked this conversation as resolved.
Show resolved Hide resolved
}
}

function getUnresolvedImports(program: Program, cachedUnresolvedImportsPerFile: ESMap<Path, readonly string[]>): SortedReadonlyArray<string> {
Expand Down
1 change: 1 addition & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3406,6 +3406,7 @@ namespace ts.server.protocol {
* `class A { foo }`.
*/
readonly includeCompletionsWithClassMemberSnippets?: boolean;
readonly includeProjectReferenceAutoImports?: "on" | "off";
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" */
Expand Down
Loading