Skip to content

Commit

Permalink
findAllReferences/rename: Search in all open projects
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy Hanson committed Jul 13, 2018
1 parent 1fb050b commit 366568c
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 67 deletions.
4 changes: 4 additions & 0 deletions src/compiler/program.ts
Expand Up @@ -738,6 +738,10 @@ namespace ts {
getDiagnosticsProducingTypeChecker,
getCommonSourceDirectory,
emit,
getDeclarationEmitPath: fileName => {
const sf = getSourceFile(fileName);
return sf && getDeclarationEmitOutputFilePath(sf, getEmitHost());
},
getCurrentDirectory: () => currentDirectory,
getNodeCount: () => getDiagnosticsProducingTypeChecker().getNodeCount(),
getIdentifierCount: () => getDiagnosticsProducingTypeChecker().getIdentifierCount(),
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Expand Up @@ -2746,6 +2746,8 @@ namespace ts {
* will be invoked when writing the JavaScript and declaration files.
*/
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult;
/* @internal */
getDeclarationEmitPath(fileName: string): string | undefined;

getOptionsDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray<Diagnostic>;
getGlobalDiagnostics(cancellationToken?: CancellationToken): ReadonlyArray<Diagnostic>;
Expand Down
121 changes: 74 additions & 47 deletions src/server/session.ts
Expand Up @@ -298,40 +298,55 @@ namespace ts.server {

function combineProjectOutputWhileOpeningReferencedProjects<T>(
projects: Projects,
defaultProject: Project,
projectService: ProjectService,
action: (project: Project) => ReadonlyArray<T>,
getLocation: (t: T) => sourcemaps.SourceMappableLocation,
resultsEqual: (a: T, b: T) => boolean,
): T[] {
const outputs: T[] = [];
combineProjectOutputWorker(projects, <sourcemaps.SourceMappableLocation><any>undefined, projectService, ({ project }, tryAddToTodo) => {
for (const output of action(project)) {
if (!contains(outputs, output, resultsEqual) && !tryAddToTodo(project, getLocation(output))) {
outputs.push(output);
combineProjectOutputWorker(
projects,
defaultProject,
{ fileName: "<arbitrary>", position: 0 }, // Location is ignored by callback
projectService,
({ project }, tryAddToTodo) => {
for (const output of action(project)) {
if (!contains(outputs, output, resultsEqual) && !tryAddToTodo(project, getLocation(output))) {
outputs.push(output);
}
}
}
});
},
/*getDefinition*/ undefined);
return outputs;
}

function combineProjectOutputForRenameLocations(projects: Projects, initialLocation: sourcemaps.SourceMappableLocation, projectService: ProjectService, findInStrings: boolean, findInComments: boolean): ReadonlyArray<RenameLocation> {
function combineProjectOutputForRenameLocations(
projects: Projects, defaultProject: Project, initialLocation: sourcemaps.SourceMappableLocation, projectService: ProjectService, findInStrings: boolean, findInComments: boolean
): ReadonlyArray<RenameLocation> {
const outputs: RenameLocation[] = [];

combineProjectOutputWorker(projects, initialLocation, projectService, ({ project, location }, tryAddToTodo) => {
combineProjectOutputWorker(projects, defaultProject, initialLocation, projectService, ({ project, location }, tryAddToTodo) => {
for (const output of project.getLanguageService().findRenameLocations(location.fileName, location.position, findInStrings, findInComments) || emptyArray) {
if (!contains(outputs, output, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(output))) {
outputs.push(output);
}
}
});
}, () => getDefinitionLocation(defaultProject, initialLocation));

return outputs;
}

function combineProjectOutputForReferences(projects: Projects, initialLocation: sourcemaps.SourceMappableLocation, projectService: ProjectService): ReadonlyArray<ReferencedSymbol> {
function getDefinitionLocation(defaultProject: Project, initialLocation: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined {
const infos = defaultProject.getLanguageService().getDefinitionAtPosition(initialLocation.fileName, initialLocation.position);
const info = infos && firstOrUndefined(infos);
return info && { fileName: info.fileName, position: info.textSpan.start };
}

function combineProjectOutputForReferences(projects: Projects, defaultProject: Project, initialLocation: sourcemaps.SourceMappableLocation, projectService: ProjectService): ReadonlyArray<ReferencedSymbol> {
const outputs: ReferencedSymbol[] = [];

combineProjectOutputWorker(projects, initialLocation, projectService, ({ project, location }, tryAddToTodo) => {
combineProjectOutputWorker(projects, defaultProject, initialLocation, projectService, ({ project, location }, tryAddToTodo) => {
for (const outputReferencedSymbol of project.getLanguageService().findReferences(location.fileName, location.position) || emptyArray) {
let symbolToAddTo = find(outputs, o => documentSpansEqual(o.definition, outputReferencedSymbol.definition));
if (!symbolToAddTo) {
Expand All @@ -345,7 +360,7 @@ namespace ts.server {
}
}
}
});
}, () => getDefinitionLocation(defaultProject, initialLocation));

return outputs.filter(o => o.references.length !== 0);
}
Expand All @@ -355,79 +370,90 @@ namespace ts.server {
readonly location: sourcemaps.SourceMappableLocation;
}

interface ToDoAndSeenProjects {
readonly toDo: ProjectAndLocation[];
readonly seenProjects: Map<true>;
function forEachProjectInProjects(projects: Projects, path: string, cb: (project: Project, path: string) => void): void {
for (const project of isProjectsArray(projects) ? projects : projects.projects) {
cb(project, path);
}
if (!isArray(projects) && projects.symLinkedProjects) {
projects.symLinkedProjects.forEach((symlinkedProjects, symlinkedPath) => {
for (const project of symlinkedProjects) {
cb(project, symlinkedPath);
}
});
}
}

function combineProjectOutputWorker(
projects: Projects,
defaultProject: Project,
initialLocation: sourcemaps.SourceMappableLocation,
projectService: ProjectService,
cb: (where: ProjectAndLocation, getMappedLocation: (project: Project, location: sourcemaps.SourceMappableLocation) => boolean) => void,
getDefinition: (() => sourcemaps.SourceMappableLocation | undefined) | undefined,
): void {
let toDoAndSeenProjects: ToDoAndSeenProjects | undefined;
for (const project of isProjectsArray(projects) ? projects : projects.projects) {
toDoAndSeenProjects = callbackProjectAndLocation(projects, { project, location: initialLocation }, projectService, toDoAndSeenProjects, cb);
}
if (!isArray(projects) && projects.symLinkedProjects) {
projects.symLinkedProjects.forEach((symlinkedProjects, path) => {
for (const project of symlinkedProjects) {
toDoAndSeenProjects = callbackProjectAndLocation(projects, { project, location: { fileName: path, position: initialLocation.position } }, projectService, toDoAndSeenProjects, cb);
let toDo: ProjectAndLocation[] | undefined;
const seenProjects = createMap<true>();
forEachProjectInProjects(projects, initialLocation.fileName, (project, path) => {
toDo = callbackProjectAndLocation({ project, location: { fileName: path, position: initialLocation.position } }, projectService, toDo, seenProjects, cb);
});

// After initial references are collected, go over every other project and see if it has a reference for the symbol definition.
if (getDefinition) {
const memGetDefinition = memoize(getDefinition);
projectService.forEachProject(project => {
if (!addToSeen(seenProjects, project.projectName)) return;
const definition = getDefinitionInProject(memGetDefinition(), defaultProject, project);
if (definition) {
toDo = callbackProjectAndLocation({ project, location: definition }, projectService, toDo, seenProjects, cb);
}
});
}

while (toDoAndSeenProjects && toDoAndSeenProjects.toDo.length) {
toDoAndSeenProjects = callbackProjectAndLocation(projects, Debug.assertDefined(toDoAndSeenProjects.toDo.pop()), projectService, toDoAndSeenProjects, cb);
while (toDo && toDo.length) {
toDo = callbackProjectAndLocation(Debug.assertDefined(toDo.pop()), projectService, toDo, seenProjects, cb);
}
}

function getDefinitionInProject(definition: sourcemaps.SourceMappableLocation | undefined, definingProject: Project, project: Project): sourcemaps.SourceMappableLocation | undefined {
if (!definition || project.containsFile(toNormalizedPath(definition.fileName))) return definition;
const mappedDefinition = definingProject.getLanguageService().getSourceMapper().tryGetGeneratedLocation(definition);
return mappedDefinition && project.containsFile(toNormalizedPath(mappedDefinition.fileName)) ? mappedDefinition : undefined;
}

function callbackProjectAndLocation(
originalProjects: Projects, // For lazily populating seenProjects
projectAndLocation: ProjectAndLocation,
projectService: ProjectService,
toDoAndSeenProjects: ToDoAndSeenProjects | undefined,
toDo: ProjectAndLocation[] | undefined,
seenProjects: Map<true>,
cb: (where: ProjectAndLocation, getMappedLocation: (project: Project, location: sourcemaps.SourceMappableLocation) => boolean) => void,
): ToDoAndSeenProjects | undefined {
): ProjectAndLocation[] | undefined {
if (projectAndLocation.project.getCancellationToken().isCancellationRequested()) return undefined; // Skip rest of toDo if cancelled
cb(projectAndLocation, (project, location) => {
seenProjects.set(projectAndLocation.project.projectName, true);
const originalLocation = project.getSourceMapper().tryGetOriginalLocation(location);
if (!originalLocation) return false;
const originalProjectAndScriptInfo = projectService.getProjectForFileWithoutOpening(toNormalizedPath(originalLocation.fileName));
if (!originalProjectAndScriptInfo) return false;

if (originalProjectAndScriptInfo) {
if (toDoAndSeenProjects === undefined) {
toDoAndSeenProjects = { toDo: [], seenProjects: createMap<true>() };
for (const project of isProjectsArray(originalProjects) ? originalProjects : originalProjects.projects) {
toDoAndSeenProjects.seenProjects.set(project.projectName, true);
}
if (!isArray(originalProjects) && originalProjects.symLinkedProjects) {
originalProjects.symLinkedProjects.forEach(symlinkedProjects => {
for (const project of symlinkedProjects) {
toDoAndSeenProjects!.seenProjects.set(project.projectName, true);
}
});
}
}
toDo = toDo || [];

for (const project of originalProjectAndScriptInfo.projects) {
addToTodo({ project, location: originalLocation }, toDoAndSeenProjects);
addToTodo({ project, location: originalLocation }, toDo, seenProjects);
}
const symlinkedProjectsMap = projectService.getSymlinkedProjects(originalProjectAndScriptInfo.scriptInfo);
if (symlinkedProjectsMap) {
symlinkedProjectsMap.forEach((symlinkedProjects) => {
for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: originalLocation }, toDoAndSeenProjects!);
for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: originalLocation }, toDo!, seenProjects);
});
}
}
return true;
});
return toDoAndSeenProjects;
return toDo;
}

function addToTodo(projectAndLocation: ProjectAndLocation, { seenProjects, toDo }: ToDoAndSeenProjects): void {
function addToTodo(projectAndLocation: ProjectAndLocation, toDo: ProjectAndLocation[], seenProjects: Map<true>): void {
if (addToSeen(seenProjects, projectAndLocation.project.projectName)) toDo.push(projectAndLocation);
}

Expand Down Expand Up @@ -1117,7 +1143,7 @@ namespace ts.server {
const position = this.getPositionInFile(args, file);
const projects = this.getProjects(args);

const locations = combineProjectOutputForRenameLocations(projects, { fileName: args.file, position }, this.projectService, !!args.findInStrings, !!args.findInComments);
const locations = combineProjectOutputForRenameLocations(projects, this.getDefaultProject(args), { fileName: args.file, position }, this.projectService, !!args.findInStrings, !!args.findInComments);
if (!simplifiedResult) return locations;

const defaultProject = this.getDefaultProject(args);
Expand All @@ -1144,7 +1170,7 @@ namespace ts.server {
const file = toNormalizedPath(args.file);
const projects = this.getProjects(args);
const position = this.getPositionInFile(args, file);
const references = combineProjectOutputForReferences(projects, { fileName: args.file, position }, this.projectService);
const references = combineProjectOutputForReferences(projects, this.getDefaultProject(args), { fileName: args.file, position }, this.projectService);

if (simplifiedResult) {
const defaultProject = this.getDefaultProject(args);
Expand Down Expand Up @@ -1649,6 +1675,7 @@ namespace ts.server {
else {
return combineProjectOutputWhileOpeningReferencedProjects<NavigateToItem>(
this.getProjects(args),
this.getDefaultProject(args),
this.projectService,
project =>
project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, /*fileName*/ undefined, /*excludeDts*/ project.isNonTsProject()),
Expand Down
18 changes: 16 additions & 2 deletions src/services/sourcemaps.ts
Expand Up @@ -8,6 +8,7 @@ namespace ts {
export interface SourceMapper {
toLineColumnOffset(fileName: string, position: number): LineAndCharacter;
tryGetOriginalLocation(info: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined;
tryGetGeneratedLocation(info: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined;
clearCache(): void;
}

Expand All @@ -19,7 +20,7 @@ namespace ts {
getProgram: () => Program,
): SourceMapper {
let sourcemappedFileCache: SourceFileLikeCache;
return { tryGetOriginalLocation, toLineColumnOffset, clearCache };
return { tryGetOriginalLocation, tryGetGeneratedLocation, toLineColumnOffset, clearCache };

function scanForSourcemapURL(fileName: string) {
const mappedFile = sourcemappedFileCache.get(toPath(fileName, currentDirectory, getCanonicalFileName));
Expand Down Expand Up @@ -96,12 +97,25 @@ namespace ts {
function tryGetOriginalLocation(info: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined {
if (!isDeclarationFileName(info.fileName)) return undefined;

const file = getProgram().getSourceFile(info.fileName) || sourcemappedFileCache.get(toPath(info.fileName, currentDirectory, getCanonicalFileName));
const file = getFile(info.fileName);
if (!file) return undefined;
const newLoc = getSourceMapper(info.fileName, file).getOriginalPosition(info);
return newLoc === info ? undefined : tryGetOriginalLocation(newLoc) || newLoc;
}

function tryGetGeneratedLocation(info: sourcemaps.SourceMappableLocation): sourcemaps.SourceMappableLocation | undefined {
const declarationPath = getProgram().getDeclarationEmitPath(info.fileName);
if (declarationPath === undefined) return undefined;
const declarationFile = getFile(declarationPath);
if (!declarationFile) return undefined;
const newLoc = getSourceMapper(declarationPath, declarationFile).getGeneratedPosition(info);
return newLoc === info ? undefined : newLoc;
}

function getFile(fileName: string): SourceFileLike | undefined {
return getProgram().getSourceFile(fileName) || sourcemappedFileCache.get(toPath(fileName, currentDirectory, getCanonicalFileName));
}

function toLineColumnOffset(fileName: string, position: number): LineAndCharacter {
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const file = getProgram().getSourceFile(path) || sourcemappedFileCache.get(path)!; // TODO: GH#18217
Expand Down

0 comments on commit 366568c

Please sign in to comment.