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

navigateTo: Collect results from all referenced projects. #25283

Merged
21 commits merged into from
Jul 5, 2018

Conversation

ghost
Copy link

@ghost ghost commented Jun 27, 2018

Fixes navigateTo when multiple projects exist.
The old behavior was to just do navigateTo in the current project, and return results in declaration files from other projects.
The new behavior is to iterate over the current project, and all projects it references (and so on), to get all items; and to exclude those from declaration files for other projects (since those would be redundant).
Note this opens projects and doesn't explicitly close them. (It would be bad to close them immediately since the user is probably about to navigate to one of them.) Since they won't have any files open, I think there is code somewhere to detect this and close them eventually? @sheetalkamat

@@ -1623,7 +1626,7 @@ namespace ts {
);

function getTargetOfMappedDeclarationFiles(infos: ReadonlyArray<DefinitionInfo> | undefined): DefinitionInfo[] | undefined {
return map(infos, d => getTargetOfMappedDeclarationInfo(d));
return map(infos, getTargetOfMappedDeclarationInfo);
}
Copy link
Member

@weswigham weswigham Jun 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map passes a second parameter to the callback, doesn't it? And despite overriding the type to hide it, a second parameter will mangle the initial value of original in getTargetOfMappedDeclarationInfo, no (which is why it wasn't hidden, to point out issues like that)?

const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles();
// Don't include `.d.ts` files that are declarations for other projects,
// since in session.ts we will be getting navigateTo items for those projects anyway.
const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles().filter(f => !isMappedDeclarationFile(f));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapped declaration files can exist independently of project references, meaning if you have a mapped file (eg, in a lib that distributes its own source), but no project ref to it, you now lose navTo ability on types from it.


const outputs: T[] = [];

while (true) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. while (projectsToDo.lenght)

originalTextSpan: original.textSpan
})
);

function getTargetOfMappedDeclarationFiles(infos: ReadonlyArray<DefinitionInfo> | undefined): DefinitionInfo[] | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move this out of services too.. it is strange that we do go-to-def here, and nav-to/find all refs in the session.. i think we can do them all in the session, and have the sourcemapper live there as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is hard to do because of the call to sourceMapper.clearCache() in services.ts. @weswigham There's probably a better signal we could use on when to clear the cache?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not being evicted correctly right now anyway, since it's evicted on program out of date and not file out of date (because services doesn't track up-to-date ness). Now that it's in server, you should actually be able to just evict the files which are out of date, since the cross-project fs watcher/up-to-date checking stuff should be available. Specifically a file in that cache aughta be invalidated when either the associated file or the associated map updates on disk. Also the cache should likely be shared across all projects, if at all possible - I remarked as much with Ryan when I first implemented it. Honestly it makes way more sense in session because of all that.

@mhegazy
Copy link
Contributor

mhegazy commented Jun 28, 2018

Can we add tests as well?


const mapsTo = sourceMapper.tryGetMappedLocation(getLocation(output));
if (mapsTo) {
projectService.openingProjectForFileIfNecessary(toNormalizedPath(mapsTo.fileName), project => { projectsToDo.push(project); });
Copy link
Member

@weswigham weswigham Jun 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file or location that mapsTo points at may not actually exist on disk (for example, because someone shipped a declaration map but not their original source) - does this/can this still return the .d.ts location if it turns out that the .ts pointed at by the map doesn't exist (AFAIK goto def actually doesn't handle that awfully gracefully right now)?

@mhegazy
Copy link
Contributor

mhegazy commented Jun 29, 2018

+1 for @weswigham's comment. why is fourslash not sufficient?

@ghost ghost force-pushed the navigateToWithProjects branch from 4bf47fe to 8cda27f Compare June 30, 2018 02:18
@ghost ghost force-pushed the navigateToWithProjects branch from 8cda27f to a56e1d5 Compare June 30, 2018 02:19
const toDo: ProjectAndLocation[] = (isProjectsArray(projects) ? projects : projects.projects).map(project => ({ project, location: initialLocation }));

while (toDo.length) {
const { project, location } = Debug.assertDefined(toDo.pop());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would check the cancelation token here as well.

Copy link
Contributor

@mhegazy mhegazy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. @weswigham and @sheetalkamat please take another look as well.

We will be moving the sourceMap caching to the ScriptInfo in a different PR.

return { fileJustOpened: false, project: getProject(this.getConfigFileNameForFile(scriptInfo)) };
}
else {
const openResult = this.tryOpenClientFileWithNormalizedPath(fileName, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, /*projectRootPath*/ undefined, /*dontOpenIfNotExists*/ true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to open the script info here? This might break asserts if editor opens file after this operation? (we also need to worry about projectRoot when opening a file which is not handled here)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I change this to:

const getResult = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, this.currentDirectory, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, /*openIfNotExists*/ false);
return getResult && { fileJustOpened: true, project: getProject(this.getConfigFileNameForFile(getResult)) };

There will be an error at Debug.assert(this.openFiles.has(info.path)) in forEachConfigFileLocation. Could you take a look?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've modified forEachConfigFileLocation to not have that assert in this case. If the file isn't open projectRootPath will be undefined, so anySearchPathOk will be true and it should look in all possible locations.

cb({ project, location }, (project, location) => {
const mapsTo = project.getSourceMapper().tryGetMappedLocation(location);
if (!mapsTo) return false;
const info = mapsTo && projectService.tryOpeningProjectForFileIfNecessary(toNormalizedPath(mapsTo.fileName));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of opening a file, better approach would be to get project if exists otherwise get configFileName and create the project (you dont want inferred project in this scenario) so you need to handle case where project created by configFileName doesnt include the project? You would want to rather use addExternalProjectReference (rename it correctly) instead. We shouldnt open the script info since that has many implications with project Root etc. In this scenario what should be projectRoot? Since we dont want the search to go on to say "/user/username/" or some such?

const info = mapsTo && projectService.tryOpeningProjectForFileIfNecessary(toNormalizedPath(mapsTo.fileName));
if (!info) return false;
if (info.fileJustOpened) filesToClose.push(mapsTo.fileName);
if (info.project) toDo.push({ project: info.project, location: mapsTo });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to modify this check as info.project && !seenProject.has(project.projectName) to avoid unnecessary adds.

return file.sourceMapper = sourcemaps.identitySourceMapper;
}

function tryGetMappedLocation(info: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be named tryGetOriginalLocation, since "MappedLocation" is a bit ambiguous as to weather it refers to the original location of a mapped file or the emitted location of the mapping file (since both directions are possible).

//////# sourceMappingURL=index.d.ts.map

goTo.file("/index.ts");
verify.getEmitOutput(["/dist/index.js", "/dist/index.d.ts.map", "/dist/index.d.ts"]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason we're using this new verifier over verify.baselineGetEmitOutput and @emitThisFile?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or did this just replace verify.baselineGetEmitOutput, since I see @emitThisFile is still in the test?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insertResultIntoVfs didn't work when the server implementation of LanguageService was used.

}
}

public baselineGetEmitOutput(insertResultsIntoVfs?: boolean): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The insertResultsIntoVfs flag only existed to make writing fourslash tests with declaration maps easier. If they're all in the server folder now, and none of them use baselineGetEmitOutput, then this flag (and the associate branch of code) is unused and aught to be removed.

@ghost ghost force-pushed the navigateToWithProjects branch from df1162e to 6edfbba Compare July 3, 2018 00:52
@ghost ghost force-pushed the navigateToWithProjects branch from db7c7aa to 2a99ca9 Compare July 3, 2018 21:31
ghost pushed a commit that referenced this pull request Jul 3, 2018
* Support code-fix-all for importFixes

* Change description

* Update API (#25283)
@@ -1304,10 +1304,8 @@ namespace ts.server {
private getConfigFileNameForFile(info: ScriptInfo) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of duplicating code, can you please, make infoShouldBeOpen parameter must here?

}

/** @internal */
tryOpenClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath, dontOpenIfNotExists?: boolean): OpenConfiguredProjectResult | undefined {
Copy link
Member

@sheetalkamat sheetalkamat Jul 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please revert refactoring here.. No need to separate out tryOpenClientFileWithNormalizedPath

}

private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) {
Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content");
private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, kind: GetScriptInfoKind, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: { fileExists(path: string): boolean; }) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldnt need refactoring to getOrCreateScriptInfo*. Please revert it.

/** @internal */
getProjectForFileWithoutOpening(fileName: NormalizedPath): Project | undefined {
const scriptInfo = this.filenameToScriptInfo.get(fileName) ||
this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, this.currentDirectory, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, /*openIfNotExists*/ false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to use getOrCreateScriptInfoNotOpenedByClientForNormalizedPath here instead

@@ -2085,19 +2099,31 @@ namespace ts.server {
});
}

private getOrCreateConfiguredProject(configFileName: NormalizedPath): { readonly project: ConfiguredProject, readonly didCreate: boolean } {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather not have this helper (keep it inlined as was for readbility and avoiding unnecessary objects creation)
In getProjectForFileWithoutOpening use this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName)

}

/** @internal */
fileExists(fileName: NormalizedPath): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats this for?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Called in getMappedLocation in session.ts.

cb: (where: ProjectAndLocation, getMappedLocation: (project: Project, location: sourcemaps.SourceMappableLocation) => boolean) => void,
): void {
const seenProjects = createMap<true>();
const toDo: ProjectAndLocation[] = (isProjectsArray(projects) ? projects : projects.projects).map(project => ({ project, location: initialLocation }));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid creating this array if there is no sourceMaps to navigate to.. That would save current scenarios to create these arrays unnecessarily and would need arrays only if there are sourceMap. (original project and symLinkedProjects are unique so you can directly add then to seenMap.

Copy link
Author

@ghost ghost Jul 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem worth it to me... an array with a single element is probably smaller than an Identifier. Possibly smaller than the closure addToTodo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you pull out calling of cb to a function you wont need to have closures and will not need the creation of new array if not needed.

eg.

 function callbackProjectAndLocation(
        projectAndLocation: ProjectAndLocation,
        projectService: ProjectService,
        seenProjects: Map<true>,
        toDo: ProjectAndLocation[] | undefined,
        cb: (where: ProjectAndLocation, getMappedLocation: (project: Project, location: sourcemaps.SourceMappableLocation) => boolean) => void,
    ) : ProjectAndLocation[] | undefined 

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact you could delay creation of seenProjects as well since that is also needed only when sourceMapped location is found.

if (project.getCancellationToken().isCancellationRequested()) continue;
cb({ project, location }, (project, location) => {
const originalLocation = project.getSourceMapper().tryGetOriginalLocation(location);
const originalProject = originalLocation && projectService.getProjectForFileWithoutOpening(toNormalizedPath(originalLocation.fileName));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you need to see if there are symLinkedProjects for this originalLocation ?

@ghost
Copy link
Author

ghost commented Jul 4, 2018

@sheetalkamat Please re-review

const seenProjects = createMap<true>();
const toDo: ProjectAndLocation[] = (isProjectsArray(projects) ? projects : projects.projects).map(project => ({ project, location: initialLocation }));
if (!isArray(projects) && projects.symLinkedProjects) {
projects.symLinkedProjects.forEach((symlinkedProjects, path) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to add projects and symLinkedProjects to seenProjects as these will be in array to toDo but not in seen so the below check of addToDo will not have these?

const scriptInfo = this.filenameToScriptInfo.get(fileName) ||
this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName, this.currentDirectory, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined);
const configFileName = scriptInfo && this.getConfigFileNameForFile(scriptInfo, /*infoShouldBeOpen*/ false);
const project = configFileName === undefined ? undefined : this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think once you create the project you want to check if projectAndLocation.project.containsScriptInfo(scriptInfo) since configured project could not include the script info?

this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName, this.currentDirectory, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined);
const configFileName = scriptInfo && this.getConfigFileNameForFile(scriptInfo, /*infoShouldBeOpen*/ false);
const project = configFileName === undefined ? undefined : this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
return scriptInfo && project && { scriptInfo, project };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to get scriptInfo.containingProjects instead of just project

@ghost
Copy link
Author

ghost commented Jul 5, 2018

@sheetalkamat Think we can get this in this week for ts3.0?

if (scriptInfo.containingProjects.length) {
return { scriptInfo, projects: scriptInfo.containingProjects };
}
const configFileName = scriptInfo && this.getConfigFileNameForFile(scriptInfo, /*infoShouldBeOpen*/ false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be this.getConfigFileNameForFile(scriptInfo, /infoShouldBeOpen/ false);

@ghost ghost merged commit 64555aa into master Jul 5, 2018
@ghost ghost deleted the navigateToWithProjects branch July 5, 2018 22:39
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants