navigateTo: Collect results from all referenced projects. (#25283)

* navigateTo: Collect results from all referenced projects.

* Don't use project references, just source maps

* Move more code to session

* Test when implementation file is deleted

* Use tsserver tests instead of fourslash tests to ensure session is used

* Support find-all-references

* Restore fourslash tests

* Update emit baselines (added missing newline)

* Support rename

* @weswigham code review

* Don't open/close files

* Avoid growing `toDo` too eagerly

* @sheetalkamat code review

* Also get symlinked projects for originalLocation

* Update API (#24966)

* More @sheetalkamat code review

* Remove unnecessary test

* Update API (#24966)
This commit is contained in:
Andy
2018-07-05 15:39:03 -07:00
committed by GitHub
parent 5b92678285
commit 64555aa6a9
70 changed files with 1536 additions and 1088 deletions

View File

@@ -1171,7 +1171,7 @@ namespace ts.server {
*/
private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) {
Debug.assert(!info.isScriptOpen());
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
this.forEachConfigFileLocation(info, /*infoShouldBeOpen*/ true, (configFileName, canonicalConfigFilePath) => {
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
if (configFileExistenceInfo) {
const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile.get(info.path);
@@ -1205,7 +1205,7 @@ namespace ts.server {
/* @internal */
startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
Debug.assert(info.isScriptOpen());
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
this.forEachConfigFileLocation(info, /*infoShouldBeOpen*/ true, (configFileName, canonicalConfigFilePath) => {
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
if (!configFileExistenceInfo) {
// Create the cache
@@ -1233,7 +1233,7 @@ namespace ts.server {
*/
/* @internal */
stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) {
this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => {
this.forEachConfigFileLocation(info, /*infoShouldBeOpen*/ true, (configFileName, canonicalConfigFilePath) => {
const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) {
Debug.assert(info.isScriptOpen());
@@ -1256,18 +1256,18 @@ namespace ts.server {
* The server must start searching from the directory containing
* the newly opened file.
*/
private forEachConfigFileLocation(info: ScriptInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
private forEachConfigFileLocation(info: ScriptInfo, infoShouldBeOpen: boolean, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
if (this.syntaxOnly) {
return undefined;
}
Debug.assert(this.openFiles.has(info.path));
Debug.assert(!infoShouldBeOpen || this.openFiles.has(info.path));
const projectRootPath = this.openFiles.get(info.path);
let searchPath = asNormalizedPath(getDirectoryPath(info.fileName));
const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames);
// If projectRootPath doesnt contain info.path, then do normal search for config file
// If projectRootPath doesn't contain info.path, then do normal search for config file
const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot();
do {
const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName);
@@ -1301,13 +1301,11 @@ namespace ts.server {
* The server must start searching from the directory containing
* the newly opened file.
*/
private getConfigFileNameForFile(info: ScriptInfo) {
Debug.assert(info.isScriptOpen());
private getConfigFileNameForFile(info: ScriptInfo, infoShouldBeOpen: boolean) {
if (infoShouldBeOpen) Debug.assert(info.isScriptOpen());
this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
const configFileName = this.forEachConfigFileLocation(info,
(configFileName, canonicalConfigFilePath) =>
this.configFileExists(configFileName, canonicalConfigFilePath, info)
);
const configFileName = this.forEachConfigFileLocation(info, infoShouldBeOpen, (configFileName, canonicalConfigFilePath) =>
this.configFileExists(configFileName, canonicalConfigFilePath, info));
if (configFileName) {
this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`);
}
@@ -1840,13 +1838,11 @@ namespace ts.server {
}
}
/*@internal*/
getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) {
private getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) {
return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
}
/*@internal*/
getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) {
private getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) {
return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
}
@@ -1989,7 +1985,7 @@ namespace ts.server {
// we first detect if there is already a configured project created for it: if so,
// we re- read the tsconfig file content and update the project only if we havent already done so
// otherwise we create a new one.
const configFileName = this.getConfigFileNameForFile(info);
const configFileName = this.getConfigFileNameForFile(info, /*infoShouldBeOpen*/ true);
if (configFileName) {
const project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {
@@ -2077,6 +2073,24 @@ namespace ts.server {
return this.openClientFileWithNormalizedPath(toNormalizedPath(fileName), fileContent, scriptKind, /*hasMixedContent*/ false, projectRootPath ? toNormalizedPath(projectRootPath) : undefined);
}
/** @internal */
getProjectForFileWithoutOpening(fileName: NormalizedPath): { readonly scriptInfo: ScriptInfo, readonly projects: ReadonlyArray<Project> } | undefined {
const scriptInfo = this.filenameToScriptInfo.get(fileName) ||
this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName, this.currentDirectory, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined);
if (!scriptInfo) return undefined;
if (scriptInfo.containingProjects.length) {
return { scriptInfo, projects: scriptInfo.containingProjects };
}
const configFileName = this.getConfigFileNameForFile(scriptInfo, /*infoShouldBeOpen*/ false);
const project = configFileName === undefined ? undefined : this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName);
return project && project.containsScriptInfo(scriptInfo) ? { scriptInfo, projects: [project] } : undefined;
}
/** @internal */
fileExists(fileName: NormalizedPath): boolean {
return this.filenameToScriptInfo.has(fileName) || this.host.fileExists(fileName);
}
private findExternalProjectContainingOpenScriptInfo(info: ScriptInfo): ExternalProject | undefined {
return find(this.externalProjects, proj => {
// Ensure project structure is up-to-date to check if info is present in external project
@@ -2090,10 +2104,11 @@ namespace ts.server {
let configFileErrors: ReadonlyArray<Diagnostic> | undefined;
const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent)!; // TODO: GH#18217
this.openFiles.set(info.path, projectRootPath);
let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info);
if (!project && !this.syntaxOnly) { // Checking syntaxOnly is an optimization
configFileName = this.getConfigFileNameForFile(info);
configFileName = this.getConfigFileNameForFile(info, /*infoShouldBeOpen*/ true);
if (configFileName) {
project = this.findConfiguredProjectByProjectName(configFileName);
if (!project) {

View File

@@ -271,8 +271,8 @@ namespace ts.server {
return this.projectStateVersion.toString();
}
getProjectReferences(): ReadonlyArray<ProjectReference> | undefined {
return undefined;
getProjectReferences(): ReadonlyArray<ProjectReference> {
return emptyArray;
}
getScriptFileNames() {
@@ -461,6 +461,11 @@ namespace ts.server {
return this.languageService;
}
/** @internal */
getSourceMapper(): SourceMapper {
return this.getLanguageService().getSourceMapper();
}
private shouldEmitFile(scriptInfo: ScriptInfo) {
return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent();
}
@@ -1351,8 +1356,8 @@ namespace ts.server {
return asNormalizedPath(this.getProjectName());
}
getProjectReferences(): ReadonlyArray<ProjectReference> | undefined {
return this.projectReferences;
getProjectReferences(): ReadonlyArray<ProjectReference> {
return this.projectReferences || emptyArray;
}
updateReferences(refs: ReadonlyArray<ProjectReference> | undefined) {

View File

@@ -34,6 +34,8 @@ namespace ts.server.protocol {
Implementation = "implementation",
/* @internal */
ImplementationFull = "implementation-full",
/* @internal */
EmitOutput = "emit-output",
Exit = "exit",
Format = "format",
Formatonkey = "formatonkey",
@@ -794,6 +796,21 @@ namespace ts.server.protocol {
command: CommandTypes.Definition;
}
export interface DefinitionAndBoundSpanRequest extends FileLocationRequest {
readonly command: CommandTypes.DefinitionAndBoundSpan;
}
export interface DefinitionAndBoundSpanResponse extends Response {
readonly body: DefinitionInfoAndBoundSpan;
}
/** @internal */
export interface EmitOutputRequest extends FileRequest {}
/** @internal */
export interface EmitOutputResponse extends Response {
readonly body: EmitOutput;
}
/**
* Go to type request; value of command field is
* "typeDefinition". Return response giving the file locations that
@@ -1056,6 +1073,17 @@ namespace ts.server.protocol {
arguments: RenameRequestArgs;
}
/* @internal */
export interface RenameFullRequest extends FileLocationRequest {
readonly command: CommandTypes.RenameLocationsFull;
readonly arguments: RenameRequestArgs;
}
/* @internal */
export interface RenameFullResponse extends Response {
readonly body: ReadonlyArray<RenameLocation>;
}
/**
* Information about the item to be renamed.
*/

View File

@@ -42,31 +42,6 @@ namespace ts.server {
return false;
}
interface FileStart {
file: string;
start: protocol.Location;
}
function compareNumber(a: number, b: number) {
return a - b;
}
function compareFileStart(a: FileStart, b: FileStart) {
if (a.file < b.file) {
return -1;
}
else if (a.file === b.file) {
const n = compareNumber(a.start.line, b.start.line);
if (n === 0) {
return compareNumber(a.start.offset, b.start.offset);
}
else return n;
}
else {
return 1;
}
}
function formatDiag(fileName: NormalizedPath, project: Project, diag: Diagnostic): protocol.Diagnostic {
const scriptInfo = project.getScriptInfoForNormalizedPath(fileName)!; // TODO: GH#18217
return {
@@ -279,8 +254,8 @@ namespace ts.server {
}
type Projects = ReadonlyArray<Project> | {
projects: ReadonlyArray<Project>;
symLinkedProjects: MultiMap<Project>;
readonly projects: ReadonlyArray<Project>;
readonly symLinkedProjects: MultiMap<Project>;
};
function isProjectsArray(projects: Projects): projects is ReadonlyArray<Project> {
@@ -290,7 +265,14 @@ namespace ts.server {
/**
* This helper function processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project.
*/
function combineProjectOutput<T, U>(defaultValue: T, getValue: (path: Path) => T, projects: Projects, action: (project: Project, value: T) => ReadonlyArray<U> | U | undefined, comparer?: (a: U, b: U) => number, areEqual?: (a: U, b: U) => boolean) {
function combineProjectOutput<T, U>(
defaultValue: T,
getValue: (path: Path) => T,
projects: Projects,
action: (project: Project, value: T) => ReadonlyArray<U> | U | undefined,
comparer?: (a: U, b: U) => number,
areEqual?: (a: U, b: U) => boolean,
): U[] {
const outputs = flatMap(isProjectsArray(projects) ? projects : projects.projects, project => action(project, defaultValue));
if (!isProjectsArray(projects) && projects.symLinkedProjects) {
projects.symLinkedProjects.forEach((projects, path) => {
@@ -304,6 +286,150 @@ namespace ts.server {
: deduplicate(outputs, areEqual);
}
function combineProjectOutputWhileOpeningReferencedProjects<T>(
projects: Projects,
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);
}
}
});
return outputs;
}
function combineProjectOutputForRenameLocations(projects: Projects, initialLocation: sourcemaps.SourceMappableLocation, projectService: ProjectService, findInStrings: boolean, findInComments: boolean): ReadonlyArray<RenameLocation> {
const outputs: RenameLocation[] = [];
combineProjectOutputWorker(projects, 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);
}
}
});
return outputs;
}
function combineProjectOutputForReferences(projects: Projects, initialLocation: sourcemaps.SourceMappableLocation, projectService: ProjectService): ReadonlyArray<ReferencedSymbol> {
const outputs: ReferencedSymbol[] = [];
combineProjectOutputWorker(projects, 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) {
symbolToAddTo = { definition: outputReferencedSymbol.definition, references: [] };
outputs.push(symbolToAddTo);
}
for (const ref of outputReferencedSymbol.references) {
if (!contains(symbolToAddTo.references, ref, documentSpansEqual) && !tryAddToTodo(project, documentSpanLocation(ref))) {
symbolToAddTo.references.push(ref);
}
}
}
});
return outputs.filter(o => o.references.length !== 0);
}
interface ProjectAndLocation {
readonly project: Project;
readonly location: sourcemaps.SourceMappableLocation;
}
interface ToDoAndSeenProjects {
readonly toDo: ProjectAndLocation[];
readonly seenProjects: Map<true>;
}
function combineProjectOutputWorker(
projects: Projects,
initialLocation: sourcemaps.SourceMappableLocation,
projectService: ProjectService,
cb: (where: ProjectAndLocation, getMappedLocation: (project: Project, location: sourcemaps.SourceMappableLocation) => boolean) => void,
): 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);
}
});
}
while (toDoAndSeenProjects && toDoAndSeenProjects.toDo.length) {
toDoAndSeenProjects = callbackProjectAndLocation(projects, Debug.assertDefined(toDoAndSeenProjects.toDo.pop()), projectService, toDoAndSeenProjects, cb);
}
}
function callbackProjectAndLocation(
originalProjects: Projects, // For lazily populating seenProjects
projectAndLocation: ProjectAndLocation,
projectService: ProjectService,
toDoAndSeenProjects: ToDoAndSeenProjects | undefined,
cb: (where: ProjectAndLocation, getMappedLocation: (project: Project, location: sourcemaps.SourceMappableLocation) => boolean) => void,
): ToDoAndSeenProjects | undefined {
if (projectAndLocation.project.getCancellationToken().isCancellationRequested()) return undefined; // Skip rest of toDo if cancelled
cb(projectAndLocation, (project, location) => {
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);
}
});
}
}
for (const project of originalProjectAndScriptInfo.projects) {
addToTodo({ project, location: originalLocation }, toDoAndSeenProjects);
}
const symlinkedProjectsMap = projectService.getSymlinkedProjects(originalProjectAndScriptInfo.scriptInfo);
if (symlinkedProjectsMap) {
symlinkedProjectsMap.forEach((symlinkedProjects) => {
for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: originalLocation }, toDoAndSeenProjects!);
});
}
}
return true;
});
return toDoAndSeenProjects;
}
function addToTodo(projectAndLocation: ProjectAndLocation, { seenProjects, toDo }: ToDoAndSeenProjects): void {
if (addToSeen(seenProjects, projectAndLocation.project.projectName)) toDo.push(projectAndLocation);
}
function documentSpanLocation({ fileName, textSpan }: DocumentSpan): sourcemaps.SourceMappableLocation {
return { fileName, position: textSpan.start };
}
function getMappedLocation(location: sourcemaps.SourceMappableLocation, projectService: ProjectService, project: Project): sourcemaps.SourceMappableLocation | undefined {
const mapsTo = project.getSourceMapper().tryGetOriginalLocation(location);
return mapsTo && projectService.fileExists(toNormalizedPath(mapsTo.fileName)) ? mapsTo : undefined;
}
export interface SessionOptions {
host: ServerHost;
cancellationToken: ServerCancellationToken;
@@ -690,46 +816,64 @@ namespace ts.server {
private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileSpan> | ReadonlyArray<DefinitionInfo> {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
const definitions = this.mapDefinitionInfoLocations(project.getLanguageService().getDefinitionAtPosition(file, position) || emptyArray, project);
return simplifiedResult ? this.mapDefinitionInfo(definitions, project) : definitions.map(Session.mapToOriginalLocation);
}
const definitions = project.getLanguageService().getDefinitionAtPosition(file, position);
if (!definitions) {
return emptyArray;
}
if (simplifiedResult) {
return this.mapDefinitionInfo(definitions, project);
}
return definitions.map(Session.mapToOriginalLocation);
private mapDefinitionInfoLocations(definitions: ReadonlyArray<DefinitionInfo>, project: Project): ReadonlyArray<DefinitionInfo> {
return definitions.map((info): DefinitionInfo => {
const newLoc = getMappedLocation(documentSpanLocation(info), this.projectService, project);
return !newLoc ? info : {
containerKind: info.containerKind,
containerName: info.containerName,
fileName: newLoc.fileName,
kind: info.kind,
name: info.name,
textSpan: {
start: newLoc.position,
length: info.textSpan.length
},
originalFileName: info.fileName,
originalTextSpan: info.textSpan,
};
});
}
private getDefinitionAndBoundSpan(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.DefinitionInfoAndBoundSpan | DefinitionInfoAndBoundSpan {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
const scriptInfo = project.getScriptInfo(file)!;
const scriptInfo = Debug.assertDefined(project.getScriptInfo(file));
const definitionAndBoundSpan = project.getLanguageService().getDefinitionAndBoundSpan(file, position);
const unmappedDefinitionAndBoundSpan = project.getLanguageService().getDefinitionAndBoundSpan(file, position);
if (!definitionAndBoundSpan || !definitionAndBoundSpan.definitions) {
if (!unmappedDefinitionAndBoundSpan || !unmappedDefinitionAndBoundSpan.definitions) {
return {
definitions: emptyArray,
textSpan: undefined! // TODO: GH#18217
};
}
const definitions = this.mapDefinitionInfoLocations(unmappedDefinitionAndBoundSpan.definitions, project);
const { textSpan } = unmappedDefinitionAndBoundSpan;
if (simplifiedResult) {
return {
definitions: this.mapDefinitionInfo(definitionAndBoundSpan.definitions, project),
textSpan: this.toLocationTextSpan(definitionAndBoundSpan.textSpan, scriptInfo)
definitions: this.mapDefinitionInfo(definitions, project),
textSpan: this.toLocationTextSpan(textSpan, scriptInfo)
};
}
return {
...definitionAndBoundSpan,
definitions: definitionAndBoundSpan.definitions.map(Session.mapToOriginalLocation)
definitions: definitions.map(Session.mapToOriginalLocation),
textSpan,
};
}
private getEmitOutput(args: protocol.FileRequestArgs): EmitOutput {
const { file, project } = this.getFileAndProject(args);
return project.getLanguageService().getEmitOutput(file);
}
private mapDefinitionInfo(definitions: ReadonlyArray<DefinitionInfo>, project: Project): ReadonlyArray<protocol.FileSpan> {
return definitions.map(def => this.toFileSpan(def.fileName, def.textSpan, project));
}
@@ -771,21 +915,31 @@ namespace ts.server {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
const definitions = project.getLanguageService().getTypeDefinitionAtPosition(file, position);
if (!definitions) {
return emptyArray;
}
const definitions = this.mapDefinitionInfoLocations(project.getLanguageService().getTypeDefinitionAtPosition(file, position) || emptyArray, project);
return this.mapDefinitionInfo(definitions, project);
}
private mapImplementationLocations(implementations: ReadonlyArray<ImplementationLocation>, project: Project): ReadonlyArray<ImplementationLocation> {
return implementations.map((info): ImplementationLocation => {
const newLoc = getMappedLocation(documentSpanLocation(info), this.projectService, project);
return !newLoc ? info : {
fileName: newLoc.fileName,
kind: info.kind,
displayParts: info.displayParts,
textSpan: {
start: newLoc.position,
length: info.textSpan.length
},
originalFileName: info.fileName,
originalTextSpan: info.textSpan,
};
});
}
private getImplementation(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileSpan> | ReadonlyArray<ImplementationLocation> {
const { file, project } = this.getFileAndProject(args);
const position = this.getPositionInFile(args, file);
const implementations = project.getLanguageService().getImplementationAtPosition(file, position);
if (!implementations) {
return emptyArray;
}
const implementations = this.mapImplementationLocations(project.getLanguageService().getImplementationAtPosition(file, position) || emptyArray, project);
if (simplifiedResult) {
return implementations.map(({ fileName, textSpan }) => this.toFileSpan(fileName, textSpan, project));
}
@@ -948,179 +1102,59 @@ namespace ts.server {
return info.getDefaultProject();
}
private getRenameLocations(args: protocol.RenameRequestArgs, simplifiedResult: boolean): protocol.RenameResponseBody | ReadonlyArray<RenameLocation> | undefined {
private getRenameLocations(args: protocol.RenameRequestArgs, simplifiedResult: boolean): protocol.RenameResponseBody | ReadonlyArray<RenameLocation> {
const file = toNormalizedPath(args.file);
const position = this.getPositionInFile(args, file);
const projects = this.getProjects(args);
if (simplifiedResult) {
const defaultProject = this.getDefaultProject(args);
// The rename info should be the same for every project
const renameInfo = defaultProject.getLanguageService().getRenameInfo(file, position);
if (!renameInfo) {
return undefined;
}
const locations = combineProjectOutputForRenameLocations(projects, { fileName: args.file, position }, this.projectService, !!args.findInStrings, !!args.findInComments);
if (!simplifiedResult) return locations;
if (!renameInfo.canRename) {
return {
info: renameInfo,
locs: emptyArray
};
}
const defaultProject = this.getDefaultProject(args);
const renameInfo = Session.mapRenameInfo(defaultProject.getLanguageService().getRenameInfo(file, position));
return { info: renameInfo, locs: this.toSpanGroups(locations) };
}
const fileSpans = combineProjectOutput(
file,
path => this.projectService.getScriptInfoForPath(path)!.fileName,
projects,
(project, file) => {
const renameLocations = project.getLanguageService().findRenameLocations(file, position, args.findInStrings!, args.findInComments!);
if (!renameLocations) {
return emptyArray;
}
// strips 'triggerSpan'
private static mapRenameInfo({ canRename, localizedErrorMessage, displayName, fullDisplayName, kind, kindModifiers }: RenameInfo): protocol.RenameInfo {
return { canRename, localizedErrorMessage, displayName, fullDisplayName, kind, kindModifiers };
}
return renameLocations.map(location => {
const locationScriptInfo = project.getScriptInfo(location.fileName)!;
return {
file: location.fileName,
start: locationScriptInfo.positionToLineOffset(location.textSpan.start),
end: locationScriptInfo.positionToLineOffset(textSpanEnd(location.textSpan)),
};
});
},
compareRenameLocation,
(a, b) => a.file === b.file && a.start.line === b.start.line && a.start.offset === b.start.offset
);
const locs: protocol.SpanGroup[] = [];
for (const cur of fileSpans) {
let curFileAccum: protocol.SpanGroup | undefined;
if (locs.length > 0) {
curFileAccum = locs[locs.length - 1];
if (curFileAccum.file !== cur.file) {
curFileAccum = undefined;
}
}
if (!curFileAccum) {
curFileAccum = { file: cur.file, locs: [] };
locs.push(curFileAccum);
}
curFileAccum.locs.push({ start: cur.start, end: cur.end });
}
return { info: renameInfo, locs };
}
else {
return combineProjectOutput(
file,
path => this.projectService.getScriptInfoForPath(path)!.fileName,
projects,
(p, file) => p.getLanguageService().findRenameLocations(file, position, args.findInStrings!, args.findInComments!),
/*comparer*/ undefined,
renameLocationIsEqualTo
);
}
function renameLocationIsEqualTo(a: RenameLocation, b: RenameLocation) {
if (a === b) {
return true;
}
if (!a || !b) {
return false;
}
return a.fileName === b.fileName &&
a.textSpan.start === b.textSpan.start &&
a.textSpan.length === b.textSpan.length;
}
function compareRenameLocation(a: protocol.FileSpan, b: protocol.FileSpan) {
if (a.file < b.file) {
return -1;
}
else if (a.file > b.file) {
return 1;
}
else {
// reverse sort assuming no overlap
if (a.start.line < b.start.line) {
return 1;
}
else if (a.start.line > b.start.line) {
return -1;
}
else {
return b.start.offset - a.start.offset;
}
}
private toSpanGroups(locations: ReadonlyArray<RenameLocation>): ReadonlyArray<protocol.SpanGroup> {
const map = createMap<protocol.SpanGroup>();
for (const { fileName, textSpan } of locations) {
let group = map.get(fileName);
if (!group) map.set(fileName, group = { file: fileName, locs: [] });
group.locs.push(this.toLocationTextSpan(textSpan, Debug.assertDefined(this.projectService.getScriptInfo(fileName))));
}
return arrayFrom(map.values());
}
private getReferences(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.ReferencesResponseBody | undefined | ReadonlyArray<ReferencedSymbol> {
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 defaultProject = this.getDefaultProject(args);
const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file)!;
const position = this.getPosition(args, scriptInfo);
if (simplifiedResult) {
const defaultProject = this.getDefaultProject(args);
const scriptInfo = defaultProject.getScriptInfoForNormalizedPath(file)!;
const nameInfo = defaultProject.getLanguageService().getQuickInfoAtPosition(file, position);
const displayString = nameInfo ? displayPartsToString(nameInfo.displayParts) : "";
const symbolDisplayString = nameInfo ? displayPartsToString(nameInfo.displayParts) : "";
const nameSpan = nameInfo && nameInfo.textSpan;
const nameColStart = nameSpan ? scriptInfo.positionToLineOffset(nameSpan.start).offset : 0;
const nameText = nameSpan ? scriptInfo.getSnapshot().getText(nameSpan.start, textSpanEnd(nameSpan)) : "";
const refs = combineProjectOutput<NormalizedPath, protocol.ReferencesResponseItem>(
file,
path => this.projectService.getScriptInfoForPath(path)!.fileName,
projects,
(project, file) => {
const references = project.getLanguageService().getReferencesAtPosition(file, position);
if (!references) {
return emptyArray;
}
return references.map(ref => {
const refScriptInfo = project.getScriptInfo(ref.fileName)!;
const start = refScriptInfo.positionToLineOffset(ref.textSpan.start);
const refLineSpan = refScriptInfo.lineToTextSpan(start.line - 1);
const lineText = refScriptInfo.getSnapshot().getText(refLineSpan.start, textSpanEnd(refLineSpan)).replace(/\r|\n/g, "");
return {
file: ref.fileName,
start,
lineText,
end: refScriptInfo.positionToLineOffset(textSpanEnd(ref.textSpan)),
isWriteAccess: ref.isWriteAccess,
isDefinition: ref.isDefinition
};
});
},
compareFileStart,
areReferencesResponseItemsForTheSameLocation
);
return {
refs,
symbolName: nameText,
symbolStartOffset: nameColStart,
symbolDisplayString: displayString
};
const symbolStartOffset = nameSpan ? scriptInfo.positionToLineOffset(nameSpan.start).offset : 0;
const symbolName = nameSpan ? scriptInfo.getSnapshot().getText(nameSpan.start, textSpanEnd(nameSpan)) : "";
const refs: protocol.ReferencesResponseItem[] = flatMap(references, referencedSymbol =>
referencedSymbol.references.map(({ fileName, textSpan, isWriteAccess, isDefinition }): protocol.ReferencesResponseItem => {
const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(fileName));
const lineText = scriptInfo.getSnapshot().getText(textSpan.start, textSpanEnd(textSpan));
return { ...toFileSpan(fileName, textSpan, scriptInfo), lineText, isWriteAccess, isDefinition };
}));
const result: protocol.ReferencesResponseBody = { refs, symbolName, symbolStartOffset, symbolDisplayString };
return result;
}
else {
return combineProjectOutput(
file,
path => this.projectService.getScriptInfoForPath(path)!.fileName,
projects,
(project, file) => project.getLanguageService().findReferences(file, position),
/*comparer*/ undefined,
equateValues
);
}
function areReferencesResponseItemsForTheSameLocation(a: protocol.ReferencesResponseItem, b: protocol.ReferencesResponseItem) {
if (a && b) {
return a.file === b.file &&
a.start === b.start &&
a.end === b.end;
}
return false;
return references;
}
}
@@ -1568,63 +1602,45 @@ namespace ts.server {
}
private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.NavtoItem> | ReadonlyArray<NavigateToItem> {
const projects = this.getProjects(args);
const full = this.getFullNavigateToItems(args);
return !simplifiedResult ? full : full.map((navItem) => {
const { file, project } = this.getFileAndProject({ file: navItem.fileName });
const scriptInfo = project.getScriptInfo(file)!;
const bakedItem: protocol.NavtoItem = {
name: navItem.name,
kind: navItem.kind,
isCaseSensitive: navItem.isCaseSensitive,
matchKind: navItem.matchKind,
file: navItem.fileName,
start: scriptInfo.positionToLineOffset(navItem.textSpan.start),
end: scriptInfo.positionToLineOffset(textSpanEnd(navItem.textSpan))
};
if (navItem.kindModifiers && (navItem.kindModifiers !== "")) {
bakedItem.kindModifiers = navItem.kindModifiers;
}
if (navItem.containerName && (navItem.containerName.length > 0)) {
bakedItem.containerName = navItem.containerName;
}
if (navItem.containerKind && (navItem.containerKind.length > 0)) {
bakedItem.containerKind = navItem.containerKind;
}
return bakedItem;
});
}
const fileName = args.currentFileOnly ? args.file && normalizeSlashes(args.file) : undefined;
if (simplifiedResult) {
return combineProjectOutput(
fileName,
() => undefined,
projects,
(project, file) => {
if (fileName && !file) {
return undefined;
}
const navItems = project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isNonTsProject());
if (!navItems) {
return emptyArray;
}
return navItems.map((navItem) => {
const scriptInfo = project.getScriptInfo(navItem.fileName)!;
const bakedItem: protocol.NavtoItem = {
name: navItem.name,
kind: navItem.kind,
isCaseSensitive: navItem.isCaseSensitive,
matchKind: navItem.matchKind,
file: navItem.fileName,
start: scriptInfo.positionToLineOffset(navItem.textSpan.start),
end: scriptInfo.positionToLineOffset(textSpanEnd(navItem.textSpan))
};
if (navItem.kindModifiers && (navItem.kindModifiers !== "")) {
bakedItem.kindModifiers = navItem.kindModifiers;
}
if (navItem.containerName && (navItem.containerName.length > 0)) {
bakedItem.containerName = navItem.containerName;
}
if (navItem.containerKind && (navItem.containerKind.length > 0)) {
bakedItem.containerKind = navItem.containerKind;
}
return bakedItem;
});
},
/*comparer*/ undefined,
areNavToItemsForTheSameLocation
);
private getFullNavigateToItems(args: protocol.NavtoRequestArgs): ReadonlyArray<NavigateToItem> {
const { currentFileOnly, searchValue, maxResultCount } = args;
if (currentFileOnly) {
const { file, project } = this.getFileAndProject(args);
return project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, file);
}
else {
return combineProjectOutput(
fileName,
() => undefined,
projects,
(project, file) => {
if (fileName && !file) {
return undefined;
}
return project.getLanguageService().getNavigateToItems(args.searchValue, args.maxResultCount, fileName, /*excludeDts*/ project.isNonTsProject());
},
/*comparer*/ undefined,
return combineProjectOutputWhileOpeningReferencedProjects<NavigateToItem>(
this.getProjects(args),
this.projectService,
project =>
project.getLanguageService().getNavigateToItems(searchValue, maxResultCount, /*fileName*/ undefined, /*excludeDts*/ project.isNonTsProject()),
documentSpanLocation,
navigateToItemIsEqualTo);
}
@@ -1646,15 +1662,6 @@ namespace ts.server {
a.textSpan.start === b.textSpan.start &&
a.textSpan.length === b.textSpan.length;
}
function areNavToItemsForTheSameLocation(a: protocol.NavtoItem, b: protocol.NavtoItem) {
if (a && b) {
return a.file === b.file &&
a.start === b.start &&
a.end === b.end;
}
return false;
}
}
private getSupportedCodeFixes(): string[] {
@@ -1976,6 +1983,9 @@ namespace ts.server {
[CommandNames.DefinitionAndBoundSpanFull]: (request: protocol.DefinitionRequest) => {
return this.requiredResponse(this.getDefinitionAndBoundSpan(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.EmitOutput]: (request: protocol.EmitOutputRequest) => {
return this.requiredResponse(this.getEmitOutput(request.arguments));
},
[CommandNames.TypeDefinition]: (request: protocol.FileLocationRequest) => {
return this.requiredResponse(this.getTypeDefinition(request.arguments));
},
@@ -1991,10 +2001,10 @@ namespace ts.server {
[CommandNames.ReferencesFull]: (request: protocol.FileLocationRequest) => {
return this.requiredResponse(this.getReferences(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.Rename]: (request: protocol.Request) => {
[CommandNames.Rename]: (request: protocol.RenameRequest) => {
return this.requiredResponse(this.getRenameLocations(request.arguments, /*simplifiedResult*/ true));
},
[CommandNames.RenameLocationsFull]: (request: protocol.RenameRequest) => {
[CommandNames.RenameLocationsFull]: (request: protocol.RenameFullRequest) => {
return this.requiredResponse(this.getRenameLocations(request.arguments, /*simplifiedResult*/ false));
},
[CommandNames.RenameInfoFull]: (request: protocol.FileLocationRequest) => {
@@ -2332,6 +2342,10 @@ namespace ts.server {
readonly project: Project;
}
function toFileSpan(fileName: string, textSpan: TextSpan, scriptInfo: ScriptInfo): protocol.FileSpan {
return { file: fileName, start: scriptInfo.positionToLineOffset(textSpan.start), end: scriptInfo.positionToLineOffset(textSpanEnd(textSpan)) };
}
function mapTextChangesToCodeEdits(textChanges: FileTextChanges, sourceFile: SourceFile | undefined): protocol.FileCodeEdits {
Debug.assert(!!textChanges.isNewFile === !sourceFile);
if (sourceFile) {