diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 296b489f0c4..2ebf086cb74 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2122,36 +2122,81 @@ module FourSlash { public verifyOccurrencesAtPositionListContains(fileName: string, start: number, end: number, isWriteAccess?: boolean) { this.taoInvalidReason = "verifyOccurrencesAtPositionListContains NYI"; - let occurances = this.getOccurancesAtCurrentPosition(); + let occurrences = this.getOccurancesAtCurrentPosition(); - if (!occurances || occurances.length === 0) { - this.raiseError("verifyOccurancesAtPositionListContains failed - found 0 references, expected at least one."); + if (!occurrences || occurrences.length === 0) { + this.raiseError('verifyOccurancesAtPositionListContains failed - found 0 references, expected at least one.'); } - for (let i = 0; i < occurances.length; i++) { - let occurance = occurances[i]; - if (occurance && occurance.fileName === fileName && occurance.textSpan.start === start && ts.textSpanEnd(occurance.textSpan) === end) { - if (typeof isWriteAccess !== "undefined" && occurance.isWriteAccess !== isWriteAccess) { - this.raiseError(`verifyOccurancesAtPositionListContains failed - item isWriteAccess value doe not match, actual: ${occurance.isWriteAccess}, expected: ${isWriteAccess}.`); + for (let occurrence of occurrences) { + if (occurrence && occurrence.fileName === fileName && occurrence.textSpan.start === start && ts.textSpanEnd(occurrence.textSpan) === end) { + if (typeof isWriteAccess !== "undefined" && occurrence.isWriteAccess !== isWriteAccess) { + this.raiseError(`verifyOccurrencesAtPositionListContains failed - item isWriteAccess value does not match, actual: ${occurrence.isWriteAccess}, expected: ${isWriteAccess}.`); } return; } } let missingItem = { fileName: fileName, start: start, end: end, isWriteAccess: isWriteAccess }; - this.raiseError(`verifyOccurancesAtPositionListContains failed - could not find the item: ${JSON.stringify(missingItem)} in the returned list: (${JSON.stringify(occurances)})`); + this.raiseError(`verifyOccurrencesAtPositionListContains failed - could not find the item: ${JSON.stringify(missingItem)} in the returned list: (${JSON.stringify(occurrences)})`); } public verifyOccurrencesAtPositionListCount(expectedCount: number) { this.taoInvalidReason = "verifyOccurrencesAtPositionListCount NYI"; - let occurances = this.getOccurancesAtCurrentPosition(); - let actualCount = occurances ? occurances.length : 0; + let occurrences = this.getOccurancesAtCurrentPosition(); + let actualCount = occurrences ? occurrences.length : 0; if (expectedCount !== actualCount) { this.raiseError(`verifyOccurrencesAtPositionListCount failed - actual: ${actualCount}, expected:${expectedCount}`); } } + private getDocumentHighlightsAtCurrentPosition(fileNamesToSearch: string[]) { + let filesToSearch = fileNamesToSearch.map(name => ts.combinePaths(this.basePath, name)); + return this.languageService.getDocumentHighlights(this.activeFile.fileName, this.currentCaretPosition, filesToSearch); + } + + public verifyDocumentHighlightsAtPositionListContains(fileName: string, start: number, end: number, fileNamesToSearch: string[], kind?: string) { + this.taoInvalidReason = 'verifyDocumentHighlightsAtPositionListContains NYI'; + + let documentHighlights = this.getDocumentHighlightsAtCurrentPosition(fileNamesToSearch); + + if (!documentHighlights || documentHighlights.length === 0) { + this.raiseError('verifyDocumentHighlightsAtPositionListContains failed - found 0 highlights, expected at least one.'); + } + + for (let documentHighlight of documentHighlights) { + if (documentHighlight.fileName === fileName) { + let { highlightSpans } = documentHighlight; + + for (let highlight of highlightSpans) { + if (highlight && highlight.textSpan.start === start && ts.textSpanEnd(highlight.textSpan) === end) { + if (typeof kind !== "undefined" && highlight.kind !== kind) { + this.raiseError('verifyDocumentHighlightsAtPositionListContains failed - item "kind" value does not match, actual: ' + highlight.kind + ', expected: ' + kind + '.'); + } + return; + } + } + } + } + + let missingItem = { fileName: fileName, start: start, end: end, kind: kind }; + this.raiseError('verifyOccurancesAtPositionListContains failed - could not find the item: ' + JSON.stringify(missingItem) + ' in the returned list: (' + JSON.stringify(documentHighlights) + ')'); + } + + public verifyDocumentHighlightsAtPositionListCount(expectedCount: number, fileNamesToSearch: string[]) { + this.taoInvalidReason = 'verifyDocumentHighlightsAtPositionListCount NYI'; + + let documentHighlights = this.getDocumentHighlightsAtCurrentPosition(fileNamesToSearch); + let actualCount = documentHighlights + ? documentHighlights.reduce((currentCount, { highlightSpans }) => currentCount + highlightSpans.length, 0) + : 0; + + if (expectedCount !== actualCount) { + this.raiseError('verifyDocumentHighlightsAtPositionListCount failed - actual: ' + actualCount + ', expected:' + expectedCount); + } + } + // Get the text of the entire line the caret is currently at private getCurrentLineContent() { let text = this.getFileContent(this.activeFile.fileName); diff --git a/src/server/client.ts b/src/server/client.ts index 3ad7230cf33..cc89349b442 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -527,8 +527,33 @@ namespace ts.server { }); } - getDocumentHighlights(fileName: string, position: number): DocumentHighlights[] { - throw new Error("Not Implemented Yet."); + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { + let { line, offset } = this.positionToOneBasedLineOffset(fileName, position); + let args: protocol.DocumentHighlightsRequestArgs = { file: fileName, line, offset, filesToSearch }; + + let request = this.processRequest(CommandNames.DocumentHighlights, args); + let response = this.processResponse(request); + + let self = this; + return response.body.map(convertToDocumentHighlights); + + function convertToDocumentHighlights(item: ts.server.protocol.DocumentHighlightsItem): ts.DocumentHighlights { + let { file, highlightSpans } = item; + + return { + fileName: file, + highlightSpans: highlightSpans.map(convertHighlightSpan) + }; + + function convertHighlightSpan(span: ts.server.protocol.HighlightSpan): ts.HighlightSpan { + let start = self.lineOffsetToPosition(file, span.start); + let end = self.lineOffsetToPosition(file, span.end); + return { + textSpan: ts.createTextSpanFromBounds(start, end), + kind: span.kind + }; + } + } } getOutliningSpans(fileName: string): OutliningSpan[] { diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 017a8c1d81a..837bce4f99d 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -156,6 +156,17 @@ declare namespace ts.server.protocol { arguments: FileLocationRequestArgs; } + /** + * Arguments in document highlight request; include: filesToSearch, file, + * line, offset. + */ + export interface DocumentHighlightsRequestArgs extends FileLocationRequestArgs { + /** + * List of files to search for document highlights. + */ + filesToSearch: string[]; + } + /** * Go to definition request; value of command field is * "definition". Return response giving the file locations that @@ -238,6 +249,35 @@ declare namespace ts.server.protocol { body?: OccurrencesResponseItem[]; } + /** + * Get document highlights request; value of command field is + * "documentHighlights". Return response giving spans that are relevant + * in the file at a given line and column. + */ + export interface DocumentHighlightsRequest extends FileLocationRequest { + arguments: DocumentHighlightsRequestArgs + } + + export interface HighlightSpan extends TextSpan { + kind: string + } + + export interface DocumentHighlightsItem { + /** + * File containing highlight spans. + */ + file: string, + + /** + * Spans to highlight in file. + */ + highlightSpans: HighlightSpan[]; + } + + export interface DocumentHighlightsResponse extends Response { + body?: DocumentHighlightsItem[]; + } + /** * Find references request; value of command field is * "references". Return response giving the file locations that diff --git a/src/server/session.ts b/src/server/session.ts index 9a5cee32264..54e4423d382 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -89,6 +89,7 @@ namespace ts.server { export const NavBar = "navbar"; export const Navto = "navto"; export const Occurrences = "occurrences"; + export const DocumentHighlights = "documentHighlights"; export const Open = "open"; export const Quickinfo = "quickinfo"; export const References = "references"; @@ -313,7 +314,7 @@ namespace ts.server { })); } - private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[]{ + private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[] { fileName = ts.normalizePath(fileName); let project = this.projectService.getProjectForFile(fileName); @@ -343,6 +344,42 @@ namespace ts.server { }); } + private getDocumentHighlights(line: number, offset: number, fileName: string, filesToSearch: string[]): protocol.DocumentHighlightsItem[] { + fileName = ts.normalizePath(fileName); + let project = this.projectService.getProjectForFile(fileName); + + if (!project) { + throw Errors.NoProject; + } + + let { compilerService } = project; + let position = compilerService.host.lineOffsetToPosition(fileName, line, offset); + + let documentHighlights = compilerService.languageService.getDocumentHighlights(fileName, position, filesToSearch); + + if (!documentHighlights) { + return undefined; + } + + return documentHighlights.map(convertToDocumentHighlightsItem); + + function convertToDocumentHighlightsItem(documentHighlights: ts.DocumentHighlights): ts.server.protocol.DocumentHighlightsItem { + let { fileName, highlightSpans } = documentHighlights; + + return { + file: fileName, + highlightSpans: highlightSpans.map(convertHighlightSpan) + }; + + function convertHighlightSpan(highlightSpan: ts.HighlightSpan): ts.server.protocol.HighlightSpan { + let { textSpan, kind } = highlightSpan; + let start = compilerService.host.positionToLineOffset(fileName, textSpan.start); + let end = compilerService.host.positionToLineOffset(fileName, ts.textSpanEnd(textSpan)); + return { start, end, kind }; + } + } + } + private getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo { fileName = ts.normalizePath(fileName) let project = this.projectService.getProjectForFile(fileName) @@ -937,6 +974,10 @@ namespace ts.server { var { line, offset, file: fileName } = request.arguments; return {response: this.getOccurrences(line, offset, fileName), responseRequired: true}; }, + [CommandNames.DocumentHighlights]: (request: protocol.Request) => { + var { line, offset, file: fileName, filesToSearch } = request.arguments; + return {response: this.getDocumentHighlights(line, offset, fileName, filesToSearch), responseRequired: true}; + }, [CommandNames.ProjectInfo]: (request: protocol.Request) => { var { file, needFileNameList } = request.arguments; return {response: this.getProjectInfo(file, needFileNameList), responseRequired: true}; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index cbba2e916ce..44ce4525754 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -421,6 +421,14 @@ module FourSlashInterface { FourSlash.currentTestState.verifyOccurrencesAtPositionListCount(expectedCount); } + public documentHighlightsAtPositionContains(range: Range, fileNamesToSearch: string[], kind?: string) { + FourSlash.currentTestState.verifyDocumentHighlightsAtPositionListContains(range.fileName, range.start, range.end, fileNamesToSearch, kind); + } + + public documentHighlightsAtPositionCount(expectedCount: number, fileNamesToSearch: string[]) { + FourSlash.currentTestState.verifyDocumentHighlightsAtPositionListCount(expectedCount, fileNamesToSearch); + } + public completionEntryDetailIs(entryName: string, text: string, documentation?: string, kind?: string) { FourSlash.currentTestState.verifyCompletionEntryDetails(entryName, text, documentation, kind); } diff --git a/tests/cases/fourslash/server/documentHighlights01.ts b/tests/cases/fourslash/server/documentHighlights01.ts new file mode 100644 index 00000000000..d4be88f9357 --- /dev/null +++ b/tests/cases/fourslash/server/documentHighlights01.ts @@ -0,0 +1,18 @@ +/// + +// @Filename: a.ts +////function [|f|](x: typeof [|f|]) { +//// [|f|]([|f|]); +////} + +let ranges = test.ranges(); + +for (let r of ranges) { + goTo.position(r.start); + verify.documentHighlightsAtPositionCount(ranges.length, ["a.ts"]); + + for (let range of ranges) { + verify.documentHighlightsAtPositionContains(range, ["a.ts"]); + } +} + diff --git a/tests/cases/fourslash/server/documentHighlights02.ts b/tests/cases/fourslash/server/documentHighlights02.ts new file mode 100644 index 00000000000..357f82e9c2d --- /dev/null +++ b/tests/cases/fourslash/server/documentHighlights02.ts @@ -0,0 +1,35 @@ +/// + +// @Filename: a.ts +////function [|foo|] () { +//// return 1; +////} +////[|foo|](); + +// @Filename: b.ts +/////// +////foo(); + +// open two files +goTo.file("a.ts"); +goTo.file("b.ts"); + +let ranges = test.ranges(); + +for (let i = 0; i < ranges.length; ++i) { + let r = ranges[i]; + + if (i < 2) { + goTo.file("a.ts"); + } + else { + goTo.file("b.ts"); + } + + goTo.position(r.start); + verify.documentHighlightsAtPositionCount(3, ["a.ts", "b.ts"]); + + for (let range of ranges) { + verify.documentHighlightsAtPositionContains(range, ["a.ts", "b.ts"]); + } +}