-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Conversation
src/services/services.ts
Outdated
@@ -1623,7 +1626,7 @@ namespace ts { | |||
); | |||
|
|||
function getTargetOfMappedDeclarationFiles(infos: ReadonlyArray<DefinitionInfo> | undefined): DefinitionInfo[] | undefined { | |||
return map(infos, d => getTargetOfMappedDeclarationInfo(d)); | |||
return map(infos, getTargetOfMappedDeclarationInfo); | |||
} |
There was a problem hiding this comment.
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)?
src/services/services.ts
Outdated
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)); |
There was a problem hiding this comment.
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.
src/server/session.ts
Outdated
|
||
const outputs: T[] = []; | ||
|
||
while (true) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. while (projectsToDo.lenght)
src/services/services.ts
Outdated
originalTextSpan: original.textSpan | ||
}) | ||
); | ||
|
||
function getTargetOfMappedDeclarationFiles(infos: ReadonlyArray<DefinitionInfo> | undefined): DefinitionInfo[] | undefined { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Can we add tests as well? |
src/server/session.ts
Outdated
|
||
const mapsTo = sourceMapper.tryGetMappedLocation(getLocation(output)); | ||
if (mapsTo) { | ||
projectService.openingProjectForFileIfNecessary(toNormalizedPath(mapsTo.fileName), project => { projectsToDo.push(project); }); |
There was a problem hiding this comment.
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)?
+1 for @weswigham's comment. why is fourslash not sufficient? |
4bf47fe
to
8cda27f
Compare
8cda27f
to
a56e1d5
Compare
src/server/session.ts
Outdated
const toDo: ProjectAndLocation[] = (isProjectsArray(projects) ? projects : projects.projects).map(project => ({ project, location: initialLocation })); | ||
|
||
while (toDo.length) { | ||
const { project, location } = Debug.assertDefined(toDo.pop()); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
src/server/editorServices.ts
Outdated
return { fileJustOpened: false, project: getProject(this.getConfigFileNameForFile(scriptInfo)) }; | ||
} | ||
else { | ||
const openResult = this.tryOpenClientFileWithNormalizedPath(fileName, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, /*projectRootPath*/ undefined, /*dontOpenIfNotExists*/ true); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/server/session.ts
Outdated
cb({ project, location }, (project, location) => { | ||
const mapsTo = project.getSourceMapper().tryGetMappedLocation(location); | ||
if (!mapsTo) return false; | ||
const info = mapsTo && projectService.tryOpeningProjectForFileIfNecessary(toNormalizedPath(mapsTo.fileName)); |
There was a problem hiding this comment.
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?
src/server/session.ts
Outdated
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 }); |
There was a problem hiding this comment.
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.
src/services/sourcemaps.ts
Outdated
return file.sourceMapper = sourcemaps.identitySourceMapper; | ||
} | ||
|
||
function tryGetMappedLocation(info: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined { |
There was a problem hiding this comment.
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"]); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/harness/fourslash.ts
Outdated
} | ||
} | ||
|
||
public baselineGetEmitOutput(insertResultsIntoVfs?: boolean): void { |
There was a problem hiding this comment.
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.
df1162e
to
6edfbba
Compare
db7c7aa
to
2a99ca9
Compare
* Support code-fix-all for importFixes * Change description * Update API (#25283)
src/server/editorServices.ts
Outdated
@@ -1304,10 +1304,8 @@ namespace ts.server { | |||
private getConfigFileNameForFile(info: ScriptInfo) { |
There was a problem hiding this comment.
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?
src/server/editorServices.ts
Outdated
} | ||
|
||
/** @internal */ | ||
tryOpenClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath, dontOpenIfNotExists?: boolean): OpenConfiguredProjectResult | undefined { |
There was a problem hiding this comment.
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
src/server/editorServices.ts
Outdated
} | ||
|
||
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; }) { |
There was a problem hiding this comment.
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.
src/server/editorServices.ts
Outdated
/** @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); |
There was a problem hiding this comment.
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
src/server/editorServices.ts
Outdated
@@ -2085,19 +2099,31 @@ namespace ts.server { | |||
}); | |||
} | |||
|
|||
private getOrCreateConfiguredProject(configFileName: NormalizedPath): { readonly project: ConfiguredProject, readonly didCreate: boolean } { |
There was a problem hiding this comment.
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)
src/server/editorServices.ts
Outdated
} | ||
|
||
/** @internal */ | ||
fileExists(fileName: NormalizedPath): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whats this for?
There was a problem hiding this comment.
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
.
src/server/session.ts
Outdated
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 })); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/server/session.ts
Outdated
if (project.getCancellationToken().isCancellationRequested()) continue; | ||
cb({ project, location }, (project, location) => { | ||
const originalLocation = project.getSourceMapper().tryGetOriginalLocation(location); | ||
const originalProject = originalLocation && projectService.getProjectForFileWithoutOpening(toNormalizedPath(originalLocation.fileName)); |
There was a problem hiding this comment.
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 ?
@sheetalkamat Please re-review |
src/server/session.ts
Outdated
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) => { |
There was a problem hiding this comment.
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?
src/server/editorServices.ts
Outdated
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); |
There was a problem hiding this comment.
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?
src/server/editorServices.ts
Outdated
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 }; |
There was a problem hiding this comment.
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
@sheetalkamat Think we can get this in this week for ts3.0? |
src/server/editorServices.ts
Outdated
if (scriptInfo.containingProjects.length) { | ||
return { scriptInfo, projects: scriptInfo.containingProjects }; | ||
} | ||
const configFileName = scriptInfo && this.getConfigFileNameForFile(scriptInfo, /*infoShouldBeOpen*/ false); |
There was a problem hiding this comment.
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);
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