diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 76306e290ca..79b6249539d 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -91,10 +91,10 @@ namespace ts { return undefined; } - export function contains(array: T[], value: T): boolean { + export function contains(array: T[], value: T, areEqual?: (a: T, b: T) => boolean): boolean { if (array) { for (const v of array) { - if (v === value) { + if (areEqual ? areEqual(v, value) : v === value) { return true; } } @@ -156,12 +156,12 @@ namespace ts { return array1.concat(array2); } - export function deduplicate(array: T[]): T[] { + export function deduplicate(array: T[], areEqual?: (a: T, b: T) => boolean): T[] { let result: T[]; if (array) { result = []; for (const item of array) { - if (!contains(result, item)) { + if (!contains(result, item, areEqual)) { result.push(item); } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 3c00e925321..c4d4a23ebfa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -498,6 +498,14 @@ namespace ts.server { return copiedList; } + /** + * This helper funciton processes a list of projects and return the concatenated, sortd and deduplicated output of processing each project. + */ + export function combineProjectOutput(projects: Project[], action: (project: Project) => T[], comparer?: (a: T, b: T) => number, areEqual?: (a: T, b: T) => boolean) { + const result = projects.reduce((previous, current) => concatenate(previous, action(current)), []).sort(comparer); + return projects.length > 1 ? deduplicate(result, areEqual) : result; + } + export interface ProjectServiceEventHandler { (eventName: string, project: Project, fileName: string): void; } diff --git a/src/server/session.ts b/src/server/session.ts index 2adceee71ae..df8273116c1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -141,8 +141,8 @@ namespace ts.server { ) { this.projectService = new ProjectService(host, logger, (eventName, project, fileName) => { - this.handleEvent(eventName, project, fileName); - }); + this.handleEvent(eventName, project, fileName); + }); } private handleEvent(eventName: string, project: Project, fileName: string) { @@ -412,14 +412,17 @@ namespace ts.server { private getRenameLocations(line: number, offset: number, fileName: string, findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody { const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { + const info = this.projectService.getScriptInfo(file); + const projects = this.projectService.findReferencingProjects(info); + if (!projects.length) { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); - const renameInfo = compilerService.languageService.getRenameInfo(file, position); + const defaultProject = projects[0]; + // The rename info should be the same for every project + const defaultProjectCompilerService = defaultProject.compilerService; + const position = defaultProjectCompilerService.host.lineOffsetToPosition(file, line, offset); + const renameInfo = defaultProjectCompilerService.languageService.getRenameInfo(file, position); if (!renameInfo) { return undefined; } @@ -431,16 +434,43 @@ namespace ts.server { }; } - const renameLocations = compilerService.languageService.findRenameLocations(file, position, findInStrings, findInComments); - if (!renameLocations) { - return undefined; - } + const fileSpans = combineProjectOutput( + projects, + (project: Project) => { + const compilerService = project.compilerService; + const renameLocations = compilerService.languageService.findRenameLocations(file, position, findInStrings, findInComments); + if (!renameLocations) { + return []; + } - const bakedRenameLocs = renameLocations.map(location => ({ - file: location.fileName, - start: compilerService.host.positionToLineOffset(location.fileName, location.textSpan.start), - end: compilerService.host.positionToLineOffset(location.fileName, ts.textSpanEnd(location.textSpan)), - })).sort((a, b) => { + return renameLocations.map(location => ({ + file: location.fileName, + start: compilerService.host.positionToLineOffset(location.fileName, location.textSpan.start), + end: compilerService.host.positionToLineOffset(location.fileName, ts.textSpanEnd(location.textSpan)), + })); + }, + compareRenameLocation, + (a, b) => a.file === b.file && a.start.line === b.start.line && a.start.offset === b.start.offset + ); + const locs = fileSpans.reduce((accum, cur) => { + let curFileAccum: protocol.SpanGroup; + if (accum.length > 0) { + curFileAccum = accum[accum.length - 1]; + if (curFileAccum.file !== cur.file) { + curFileAccum = undefined; + } + } + if (!curFileAccum) { + curFileAccum = { file: cur.file, locs: [] }; + accum.push(curFileAccum); + } + curFileAccum.locs.push({ start: cur.start, end: cur.end }); + return accum; + }, []); + + return { info: renameInfo, locs }; + + function compareRenameLocation(a: protocol.FileSpan, b: protocol.FileSpan) { if (a.file < b.file) { return -1; } @@ -459,70 +489,70 @@ namespace ts.server { return b.start.offset - a.start.offset; } } - }).reduce((accum: protocol.SpanGroup[], cur: protocol.FileSpan) => { - let curFileAccum: protocol.SpanGroup; - if (accum.length > 0) { - curFileAccum = accum[accum.length - 1]; - if (curFileAccum.file != cur.file) { - curFileAccum = undefined; - } - } - if (!curFileAccum) { - curFileAccum = { file: cur.file, locs: [] }; - accum.push(curFileAccum); - } - curFileAccum.locs.push({ start: cur.start, end: cur.end }); - return accum; - }, []); - - return { info: renameInfo, locs: bakedRenameLocs }; + } } private getReferences(line: number, offset: number, fileName: string): protocol.ReferencesResponseBody { - // TODO: get all projects for this file; report refs for all projects deleting duplicates - // can avoid duplicates by eliminating same ref file from subsequent projects const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { + const info = this.projectService.getScriptInfo(file); + const projects = this.projectService.findReferencingProjects(info); + if (!projects.length) { throw Errors.NoProject; } - const compilerService = project.compilerService; - const position = compilerService.host.lineOffsetToPosition(file, line, offset); - - const references = compilerService.languageService.getReferencesAtPosition(file, position); - if (!references) { - return undefined; - } - - const nameInfo = compilerService.languageService.getQuickInfoAtPosition(file, position); + const defaultProject = projects[0]; + const position = defaultProject.compilerService.host.lineOffsetToPosition(file, line, offset); + const nameInfo = defaultProject.compilerService.languageService.getQuickInfoAtPosition(file, position); if (!nameInfo) { return undefined; } const displayString = ts.displayPartsToString(nameInfo.displayParts); const nameSpan = nameInfo.textSpan; - const nameColStart = compilerService.host.positionToLineOffset(file, nameSpan.start).offset; - const nameText = compilerService.host.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan)); - const bakedRefs: protocol.ReferencesResponseItem[] = references.map(ref => { - const start = compilerService.host.positionToLineOffset(ref.fileName, ref.textSpan.start); - const refLineSpan = compilerService.host.lineToTextSpan(ref.fileName, start.line - 1); - const snap = compilerService.host.getScriptSnapshot(ref.fileName); - const lineText = snap.getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); - return { - file: ref.fileName, - start: start, - lineText: lineText, - end: compilerService.host.positionToLineOffset(ref.fileName, ts.textSpanEnd(ref.textSpan)), - isWriteAccess: ref.isWriteAccess - }; - }).sort(compareFileStart); + const nameColStart = defaultProject.compilerService.host.positionToLineOffset(file, nameSpan.start).offset; + const nameText = defaultProject.compilerService.host.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan)); + const refs = combineProjectOutput( + projects, + (project: Project) => { + const compilerService = project.compilerService; + const references = compilerService.languageService.getReferencesAtPosition(file, position); + if (!references) { + return []; + } + + return references.map(ref => { + const start = compilerService.host.positionToLineOffset(ref.fileName, ref.textSpan.start); + const refLineSpan = compilerService.host.lineToTextSpan(ref.fileName, start.line - 1); + const snap = compilerService.host.getScriptSnapshot(ref.fileName); + const lineText = snap.getText(refLineSpan.start, ts.textSpanEnd(refLineSpan)).replace(/\r|\n/g, ""); + return { + file: ref.fileName, + start: start, + lineText: lineText, + end: compilerService.host.positionToLineOffset(ref.fileName, ts.textSpanEnd(ref.textSpan)), + isWriteAccess: ref.isWriteAccess + }; + }); + }, + compareFileStart, + areReferencesResponseItemsForTheSameLocation + ); + return { - refs: bakedRefs, + refs, symbolName: nameText, symbolStartOffset: nameColStart, symbolDisplayString: displayString }; + + 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; + } } /** @@ -836,41 +866,60 @@ namespace ts.server { private getNavigateToItems(searchValue: string, fileName: string, maxResultCount?: number): protocol.NavtoItem[] { const file = ts.normalizePath(fileName); - const project = this.projectService.getProjectForFile(file); - if (!project) { + const info = this.projectService.getScriptInfo(file); + const projects = this.projectService.findReferencingProjects(info); + const defaultProject = projects[0]; + if (!defaultProject) { throw Errors.NoProject; } - const compilerService = project.compilerService; - const navItems = compilerService.languageService.getNavigateToItems(searchValue, maxResultCount); - if (!navItems) { - return undefined; - } + const allNavToItems = combineProjectOutput( + projects, + (project: Project) => { + const compilerService = project.compilerService; + const navItems = compilerService.languageService.getNavigateToItems(searchValue, maxResultCount); + if (!navItems) { + return []; + } - return navItems.map((navItem) => { - const start = compilerService.host.positionToLineOffset(navItem.fileName, navItem.textSpan.start); - const end = compilerService.host.positionToLineOffset(navItem.fileName, ts.textSpanEnd(navItem.textSpan)); - const bakedItem: protocol.NavtoItem = { - name: navItem.name, - kind: navItem.kind, - file: navItem.fileName, - start: start, - end: end, - }; - if (navItem.kindModifiers && (navItem.kindModifiers != "")) { - bakedItem.kindModifiers = navItem.kindModifiers; + return navItems.map((navItem) => { + const start = compilerService.host.positionToLineOffset(navItem.fileName, navItem.textSpan.start); + const end = compilerService.host.positionToLineOffset(navItem.fileName, ts.textSpanEnd(navItem.textSpan)); + const bakedItem: protocol.NavtoItem = { + name: navItem.name, + kind: navItem.kind, + file: navItem.fileName, + start: start, + end: end, + }; + if (navItem.kindModifiers && (navItem.kindModifiers !== "")) { + bakedItem.kindModifiers = navItem.kindModifiers; + } + if (navItem.matchKind !== "none") { + bakedItem.matchKind = navItem.matchKind; + } + 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 + ); + return allNavToItems; + + function areNavToItemsForTheSameLocation(a: protocol.NavtoItem, b: protocol.NavtoItem) { + if (a && b) { + return a.file === b.file && + a.start === b.start && + a.end === b.end; } - if (navItem.matchKind !== "none") { - bakedItem.matchKind = navItem.matchKind; - } - if (navItem.containerName && (navItem.containerName.length > 0)) { - bakedItem.containerName = navItem.containerName; - } - if (navItem.containerKind && (navItem.containerKind.length > 0)) { - bakedItem.containerKind = navItem.containerKind; - } - return bakedItem; - }); + return false; + } } private getBraceMatching(line: number, offset: number, fileName: string): protocol.TextSpan[] { @@ -944,26 +993,26 @@ namespace ts.server { exit() { } - private handlers: Map<(request: protocol.Request) => {response?: any, responseRequired?: boolean}> = { + private handlers: Map<(request: protocol.Request) => { response?: any, responseRequired?: boolean }> = { [CommandNames.Exit]: () => { this.exit(); - return { responseRequired: false}; + return { responseRequired: false }; }, [CommandNames.Definition]: (request: protocol.Request) => { const defArgs = request.arguments; - return {response: this.getDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true}; + return { response: this.getDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; }, [CommandNames.TypeDefinition]: (request: protocol.Request) => { const defArgs = request.arguments; - return {response: this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true}; + return { response: this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; }, [CommandNames.References]: (request: protocol.Request) => { const defArgs = request.arguments; - return {response: this.getReferences(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true}; + return { response: this.getReferences(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true }; }, [CommandNames.Rename]: (request: protocol.Request) => { const renameArgs = request.arguments; - return {response: this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings), responseRequired: true}; + return { response: this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings), responseRequired: true }; }, [CommandNames.Open]: (request: protocol.Request) => { const openArgs = request.arguments; @@ -983,52 +1032,54 @@ namespace ts.server { break; } this.openClientFile(openArgs.file, openArgs.fileContent, scriptKind); - return {responseRequired: false}; + return { responseRequired: false }; }, [CommandNames.Quickinfo]: (request: protocol.Request) => { const quickinfoArgs = request.arguments; - return {response: this.getQuickInfo(quickinfoArgs.line, quickinfoArgs.offset, quickinfoArgs.file), responseRequired: true}; + return { response: this.getQuickInfo(quickinfoArgs.line, quickinfoArgs.offset, quickinfoArgs.file), responseRequired: true }; }, [CommandNames.Format]: (request: protocol.Request) => { const formatArgs = request.arguments; - return {response: this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file), responseRequired: true}; + return { response: this.getFormattingEditsForRange(formatArgs.line, formatArgs.offset, formatArgs.endLine, formatArgs.endOffset, formatArgs.file), responseRequired: true }; }, [CommandNames.Formatonkey]: (request: protocol.Request) => { const formatOnKeyArgs = request.arguments; - return {response: this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file), responseRequired: true}; + return { response: this.getFormattingEditsAfterKeystroke(formatOnKeyArgs.line, formatOnKeyArgs.offset, formatOnKeyArgs.key, formatOnKeyArgs.file), responseRequired: true }; }, [CommandNames.Completions]: (request: protocol.Request) => { const completionsArgs = request.arguments; - return {response: this.getCompletions(completionsArgs.line, completionsArgs.offset, completionsArgs.prefix, completionsArgs.file), responseRequired: true}; + return { response: this.getCompletions(completionsArgs.line, completionsArgs.offset, completionsArgs.prefix, completionsArgs.file), responseRequired: true }; }, [CommandNames.CompletionDetails]: (request: protocol.Request) => { const completionDetailsArgs = request.arguments; - return {response: this.getCompletionEntryDetails(completionDetailsArgs.line, completionDetailsArgs.offset, - completionDetailsArgs.entryNames, completionDetailsArgs.file), responseRequired: true}; + return { + response: this.getCompletionEntryDetails(completionDetailsArgs.line, completionDetailsArgs.offset, + completionDetailsArgs.entryNames, completionDetailsArgs.file), responseRequired: true + }; }, [CommandNames.SignatureHelp]: (request: protocol.Request) => { const signatureHelpArgs = request.arguments; - return {response: this.getSignatureHelpItems(signatureHelpArgs.line, signatureHelpArgs.offset, signatureHelpArgs.file), responseRequired: true}; + return { response: this.getSignatureHelpItems(signatureHelpArgs.line, signatureHelpArgs.offset, signatureHelpArgs.file), responseRequired: true }; }, [CommandNames.Geterr]: (request: protocol.Request) => { const geterrArgs = request.arguments; - return {response: this.getDiagnostics(geterrArgs.delay, geterrArgs.files), responseRequired: false}; + return { response: this.getDiagnostics(geterrArgs.delay, geterrArgs.files), responseRequired: false }; }, [CommandNames.GeterrForProject]: (request: protocol.Request) => { const { file, delay } = request.arguments; - return {response: this.getDiagnosticsForProject(delay, file), responseRequired: false}; + return { response: this.getDiagnosticsForProject(delay, file), responseRequired: false }; }, [CommandNames.Change]: (request: protocol.Request) => { const changeArgs = request.arguments; this.change(changeArgs.line, changeArgs.offset, changeArgs.endLine, changeArgs.endOffset, - changeArgs.insertString, changeArgs.file); - return {responseRequired: false}; + changeArgs.insertString, changeArgs.file); + return { responseRequired: false }; }, [CommandNames.Configure]: (request: protocol.Request) => { const configureArgs = request.arguments; this.projectService.setHostConfiguration(configureArgs); this.output(undefined, CommandNames.Configure, request.seq); - return {responseRequired: false}; + return { responseRequired: false }; }, [CommandNames.Reload]: (request: protocol.Request) => { const reloadArgs = request.arguments; @@ -1038,50 +1089,50 @@ namespace ts.server { [CommandNames.Saveto]: (request: protocol.Request) => { const savetoArgs = request.arguments; this.saveToTmp(savetoArgs.file, savetoArgs.tmpfile); - return {responseRequired: false}; + return { responseRequired: false }; }, [CommandNames.Close]: (request: protocol.Request) => { const closeArgs = request.arguments; this.closeClientFile(closeArgs.file); - return {responseRequired: false}; + return { responseRequired: false }; }, [CommandNames.Navto]: (request: protocol.Request) => { const navtoArgs = request.arguments; - return {response: this.getNavigateToItems(navtoArgs.searchValue, navtoArgs.file, navtoArgs.maxResultCount), responseRequired: true}; + return { response: this.getNavigateToItems(navtoArgs.searchValue, navtoArgs.file, navtoArgs.maxResultCount), responseRequired: true }; }, [CommandNames.Brace]: (request: protocol.Request) => { const braceArguments = request.arguments; - return {response: this.getBraceMatching(braceArguments.line, braceArguments.offset, braceArguments.file), responseRequired: true}; + return { response: this.getBraceMatching(braceArguments.line, braceArguments.offset, braceArguments.file), responseRequired: true }; }, [CommandNames.NavBar]: (request: protocol.Request) => { const navBarArgs = request.arguments; - return {response: this.getNavigationBarItems(navBarArgs.file), responseRequired: true}; + return { response: this.getNavigationBarItems(navBarArgs.file), responseRequired: true }; }, [CommandNames.Occurrences]: (request: protocol.Request) => { const { line, offset, file: fileName } = request.arguments; - return {response: this.getOccurrences(line, offset, fileName), responseRequired: true}; + return { response: this.getOccurrences(line, offset, fileName), responseRequired: true }; }, [CommandNames.DocumentHighlights]: (request: protocol.Request) => { const { line, offset, file: fileName, filesToSearch } = request.arguments; - return {response: this.getDocumentHighlights(line, offset, fileName, filesToSearch), responseRequired: true}; + return { response: this.getDocumentHighlights(line, offset, fileName, filesToSearch), responseRequired: true }; }, [CommandNames.ProjectInfo]: (request: protocol.Request) => { const { file, needFileNameList } = request.arguments; - return {response: this.getProjectInfo(file, needFileNameList), responseRequired: true}; + return { response: this.getProjectInfo(file, needFileNameList), responseRequired: true }; }, [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { this.reloadProjects(); - return {responseRequired: false}; + return { responseRequired: false }; } }; - public addProtocolHandler(command: string, handler: (request: protocol.Request) => {response?: any, responseRequired: boolean}) { + public addProtocolHandler(command: string, handler: (request: protocol.Request) => { response?: any, responseRequired: boolean }) { if (this.handlers[command]) { throw new Error(`Protocol handler already exists for command "${command}"`); } this.handlers[command] = handler; } - public executeCommand(request: protocol.Request): {response?: any, responseRequired?: boolean} { + public executeCommand(request: protocol.Request): { response?: any, responseRequired?: boolean } { const handler = this.handlers[request.command]; if (handler) { return handler(request); @@ -1089,7 +1140,7 @@ namespace ts.server { else { this.projectService.log("Unrecognized JSON command: " + JSON.stringify(request)); this.output(undefined, CommandNames.Unknown, request.seq, "Unrecognized JSON command: " + request.command); - return {responseRequired: false}; + return { responseRequired: false }; } } diff --git a/tests/cases/fourslash/findReferencesAcrossMultipleProjects.ts b/tests/cases/fourslash/findReferencesAcrossMultipleProjects.ts new file mode 100644 index 00000000000..0ef7745f8f9 --- /dev/null +++ b/tests/cases/fourslash/findReferencesAcrossMultipleProjects.ts @@ -0,0 +1,17 @@ +/// + +//@Filename: a.ts +////var /*1*/x: number; + +//@Filename: b.ts +/////// +////x++; + +//@Filename: c.ts +/////// +////x++; + +goTo.file("a.ts"); +goTo.marker("1"); + +verify.referencesCountIs(3); \ No newline at end of file diff --git a/tests/cases/fourslash/goToDefinitionAcrossMultipleProjects.ts b/tests/cases/fourslash/goToDefinitionAcrossMultipleProjects.ts new file mode 100644 index 00000000000..bac47638be6 --- /dev/null +++ b/tests/cases/fourslash/goToDefinitionAcrossMultipleProjects.ts @@ -0,0 +1,17 @@ +/// + +//@Filename: a.ts +////var x: number; + +//@Filename: b.ts +////var x: number; + +//@Filename: c.ts +/////// +/////// +/////**/x++; + +goTo.file("c.ts"); +goTo.marker(); + +verify.definitionCountIs(2); \ No newline at end of file diff --git a/tests/cases/fourslash/renameAcrossMultipleProjects.ts b/tests/cases/fourslash/renameAcrossMultipleProjects.ts new file mode 100644 index 00000000000..44b5c0baef4 --- /dev/null +++ b/tests/cases/fourslash/renameAcrossMultipleProjects.ts @@ -0,0 +1,17 @@ +/// + +//@Filename: a.ts +////var /*1*/[|x|]: number; + +//@Filename: b.ts +/////// +////[|x|]++; + +//@Filename: c.ts +/////// +////[|x|]++; + +goTo.file("a.ts"); +goTo.marker("1"); + +verify.renameLocations( /*findInStrings*/ false, /*findInComments*/ false); \ No newline at end of file