diff --git a/src/harness/client.ts b/src/harness/client.ts index 22db85eb4be92..1b52ef2abe2f6 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -682,6 +682,19 @@ export class SessionClient implements LanguageService { return { items, applicableSpan, selectedItemIndex, argumentIndex, argumentCount }; } + getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { + const args = this.createFileLocationRequestArgs(fileName, position); + + const request = this.processRequest(protocol.CommandTypes.Occurrences, args); + const response = this.processResponse(request); + + return response.body!.map(entry => ({ // TODO: GH#18217 + fileName: entry.file, + textSpan: this.decodeSpan(entry), + isWriteAccess: entry.isWriteAccess, + })); + } + getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { const args: protocol.DocumentHighlightsRequestArgs = { ...this.createFileLocationRequestArgs(fileName, position), filesToSearch }; diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 7c7c43db639e7..2efd53af5b3f2 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3557,16 +3557,7 @@ export class TestState { } private getOccurrencesAtCurrentPosition() { - return ts.flatMap( - this.languageService.getDocumentHighlights(this.activeFile.fileName, this.currentCaretPosition, [this.activeFile.fileName]), - entry => entry.highlightSpans.map(highlightSpan => ({ - fileName: entry.fileName, - textSpan: highlightSpan.textSpan, - isWriteAccess: highlightSpan.kind === ts.HighlightSpanKind.writtenReference, - ...highlightSpan.isInString && { isInString: true }, - ...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan } - })) - ); + return this.languageService.getOccurrencesAtPosition(this.activeFile.fileName, this.currentCaretPosition); } public verifyOccurrencesAtPositionListContains(fileName: string, start: number, end: number, isWriteAccess?: boolean) { diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 84eb84e330af6..b774bf35e622f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -551,6 +551,9 @@ class LanguageServiceShimProxy implements ts.LanguageService { getFileReferences(fileName: string): ts.ReferenceEntry[] { return unwrapJSONCallResult(this.shim.getFileReferences(fileName)); } + getOccurrencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] { + return unwrapJSONCallResult(this.shim.getOccurrencesAtPosition(fileName, position)); + } getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): ts.DocumentHighlights[] { return unwrapJSONCallResult(this.shim.getDocumentHighlights(fileName, position, JSON.stringify(filesToSearch))); } diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 50546bb8dce8f..1ac6e1f495df0 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -77,6 +77,8 @@ export const enum CommandTypes { NavtoFull = "navto-full", NavTree = "navtree", NavTreeFull = "navtree-full", + /** @deprecated */ + Occurrences = "occurrences", DocumentHighlights = "documentHighlights", /** @internal */ DocumentHighlightsFull = "documentHighlights-full", @@ -1101,6 +1103,33 @@ export interface JsxClosingTagResponse extends Response { readonly body: TextInsertion; } +/** + * @deprecated + * Get occurrences request; value of command field is + * "occurrences". Return response giving spans that are relevant + * in the file at a given line and column. + */ +export interface OccurrencesRequest extends FileLocationRequest { + command: CommandTypes.Occurrences; +} + +/** @deprecated */ +export interface OccurrencesResponseItem extends FileSpanWithContext { + /** + * True if the occurrence is a write location, false otherwise. + */ + isWriteAccess: boolean; + + /** + * True if the occurrence is in a string, undefined otherwise; + */ + isInString?: true; +} + +/** @deprecated */ +export interface OccurrencesResponse extends Response { + body?: OccurrencesResponseItem[]; +} /** * Get document highlights request; value of command field is diff --git a/src/server/session.ts b/src/server/session.ts index a1aa18c60c4bd..86b61f58e97e6 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -913,6 +913,7 @@ const invalidSyntacticModeCommands: readonly protocol.CommandTypes[] = [ protocol.CommandTypes.SignatureHelpFull, protocol.CommandTypes.Navto, protocol.CommandTypes.NavtoFull, + protocol.CommandTypes.Occurrences, protocol.CommandTypes.DocumentHighlights, protocol.CommandTypes.DocumentHighlightsFull, ]; @@ -1767,6 +1768,24 @@ export class Session implements EventSender { implementations.map(Session.mapToOriginalLocation); } + private getOccurrences(args: protocol.FileLocationRequestArgs): readonly protocol.OccurrencesResponseItem[] { + const { file, project } = this.getFileAndProject(args); + const position = this.getPositionInFile(args, file); + const occurrences = project.getLanguageService().getOccurrencesAtPosition(file, position); + return occurrences ? + occurrences.map(occurrence => { + const { fileName, isWriteAccess, textSpan, isInString, contextSpan } = occurrence; + const scriptInfo = project.getScriptInfo(fileName)!; + return { + ...toProtocolTextSpanWithContext(textSpan, contextSpan, scriptInfo), + file: fileName, + isWriteAccess, + ...(isInString ? { isInString } : undefined) + }; + }) : + emptyArray; + } + private getSyntacticDiagnosticsSync(args: protocol.SyntacticDiagnosticsSyncRequestArgs) { const { configFile } = this.getConfigFileAndProject(args); if (configFile) { @@ -3367,6 +3386,9 @@ export class Session implements EventSender { [protocol.CommandTypes.NavTreeFull]: (request: protocol.FileRequest) => { return this.requiredResponse(this.getNavigationTree(request.arguments, /*simplifiedResult*/ false)); }, + [protocol.CommandTypes.Occurrences]: (request: protocol.FileLocationRequest) => { + return this.requiredResponse(this.getOccurrences(request.arguments)); + }, [protocol.CommandTypes.DocumentHighlights]: (request: protocol.DocumentHighlightsRequest) => { return this.requiredResponse(this.getDocumentHighlights(request.arguments, /*simplifiedResult*/ true)); }, diff --git a/src/services/services.ts b/src/services/services.ts index 4bf03927fb3bf..09a2e5942a05f 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -119,6 +119,7 @@ import { hasStaticModifier, hasSyntacticModifier, hasTabstop, + HighlightSpanKind, HostCancellationToken, hostGetCanonicalFileName, hostUsesCaseSensitiveFileNames, @@ -1528,6 +1529,7 @@ const invalidOperationsInSyntacticMode: readonly (keyof LanguageService)[] = [ "getTypeDefinitionAtPosition", "getReferencesAtPosition", "findReferences", + "getOccurrencesAtPosition", "getDocumentHighlights", "getNavigateToItems", "getRenameInfo", @@ -2106,6 +2108,18 @@ export function createLanguageService( } /// References and Occurrences + function getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined { + return flatMap( + getDocumentHighlights(fileName, position, [fileName]), + entry => entry.highlightSpans.map(highlightSpan => ({ + fileName: entry.fileName, + textSpan: highlightSpan.textSpan, + isWriteAccess: highlightSpan.kind === HighlightSpanKind.writtenReference, + ...highlightSpan.isInString && { isInString: true }, + ...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan } + })) + ); + } function getDocumentHighlights(fileName: string, position: number, filesToSearch: readonly string[]): DocumentHighlights[] | undefined { const normalizedFileName = normalizePath(fileName); @@ -2990,6 +3004,7 @@ export function createLanguageService( getReferencesAtPosition, findReferences, getFileReferences, + getOccurrencesAtPosition, getDocumentHighlights, getNameOrDottedNameSpan, getBreakpointStatementAtPosition, diff --git a/src/services/shims.ts b/src/services/shims.ts index a6bdc912e3302..eca35a4a6ebc9 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -300,6 +300,13 @@ export interface LanguageServiceShim extends Shim { */ getFileReferences(fileName: string): string; + /** + * @deprecated + * Returns a JSON-encoded value of the type: + * { fileName: string; textSpan: { start: number; length: number}; isWriteAccess: boolean }[] + */ + getOccurrencesAtPosition(fileName: string, position: number): string; + /** * Returns a JSON-encoded value of the type: * { fileName: string; highlights: { start: number; length: number }[] }[] @@ -1014,6 +1021,13 @@ class LanguageServiceShimObject extends ShimBase implements LanguageServiceShim ); } + public getOccurrencesAtPosition(fileName: string, position: number): string { + return this.forwardJSONCall( + `getOccurrencesAtPosition('${fileName}', ${position})`, + () => this.languageService.getOccurrencesAtPosition(fileName, position) + ); + } + public getDocumentHighlights(fileName: string, position: number, filesToSearch: string): string { return this.forwardJSONCall( `getDocumentHighlights('${fileName}', ${position})`, diff --git a/src/services/types.ts b/src/services/types.ts index de17b02ac2c4d..6af97fa7ef2ab 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -580,6 +580,9 @@ export interface LanguageService { getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; getFileReferences(fileName: string): ReferenceEntry[]; + /** @deprecated */ + getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined; + getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; getNavigationBarItems(fileName: string): NavigationBarItem[]; getNavigationTree(fileName: string): NavigationTree; diff --git a/src/testRunner/unittests/tsserver/cancellationToken.ts b/src/testRunner/unittests/tsserver/cancellationToken.ts index d50f95e9c234f..4fb05770c6d6b 100644 --- a/src/testRunner/unittests/tsserver/cancellationToken.ts +++ b/src/testRunner/unittests/tsserver/cancellationToken.ts @@ -51,9 +51,9 @@ describe("unittests:: tsserver:: cancellationToken", () => { }); expectedRequestId = session.getNextSeq(); - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.DocumentHighlights, - arguments: { file: f1.path, line: 1, offset: 6, filesToSearch: [f1.path] } + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.Occurrences, + arguments: { file: f1.path, line: 1, offset: 6 } }); expectedRequestId = 2; diff --git a/src/testRunner/unittests/tsserver/occurences.ts b/src/testRunner/unittests/tsserver/occurences.ts index 07fe3c34c33d6..ea5ce71a038e0 100644 --- a/src/testRunner/unittests/tsserver/occurences.ts +++ b/src/testRunner/unittests/tsserver/occurences.ts @@ -20,19 +20,19 @@ describe("unittests:: tsserver:: occurrence highlight on string", () => { const host = createServerHost([file1]); const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) }); openFilesForSession([file1], session); - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.DocumentHighlights, - arguments: { file: file1.path, line: 1, offset: 11, filesToSearch: [file1.path] } + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.Occurrences, + arguments: { file: file1.path, line: 1, offset: 11 } }); - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.DocumentHighlights, - arguments: { file: file1.path, line: 3, offset: 13, filesToSearch: [file1.path] } + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.Occurrences, + arguments: { file: file1.path, line: 3, offset: 13 } }); - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.DocumentHighlights, - arguments: { file: file1.path, line: 4, offset: 14, filesToSearch: [file1.path] } + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.Occurrences, + arguments: { file: file1.path, line: 4, offset: 14 } }); baselineTsserverLogs("occurences", "should be marked if only on string values", session); }); diff --git a/src/testRunner/unittests/tsserver/projects.ts b/src/testRunner/unittests/tsserver/projects.ts index 40ffdb6cbdc4f..dc557d7e1223c 100644 --- a/src/testRunner/unittests/tsserver/projects.ts +++ b/src/testRunner/unittests/tsserver/projects.ts @@ -1113,13 +1113,12 @@ describe("unittests:: tsserver:: Projects", () => { }); // Actions on file1 would result in assert - session.executeCommandSeq({ - command: ts.server.protocol.CommandTypes.DocumentHighlights, + session.executeCommandSeq({ + command: ts.server.protocol.CommandTypes.Occurrences, arguments: { file: filesFile1.path, line: 1, - offset: filesFile1.content.indexOf("a"), - filesToSearch: [filesFile1.path], + offset: filesFile1.content.indexOf("a") } }); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 366a15e9811b3..5b043aef89a70 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -117,6 +117,8 @@ declare namespace ts { Navto = "navto", NavTree = "navtree", NavTreeFull = "navtree-full", + /** @deprecated */ + Occurrences = "occurrences", DocumentHighlights = "documentHighlights", Open = "open", Quickinfo = "quickinfo", @@ -877,6 +879,30 @@ declare namespace ts { interface JsxClosingTagResponse extends Response { readonly body: TextInsertion; } + /** + * @deprecated + * Get occurrences request; value of command field is + * "occurrences". Return response giving spans that are relevant + * in the file at a given line and column. + */ + interface OccurrencesRequest extends FileLocationRequest { + command: CommandTypes.Occurrences; + } + /** @deprecated */ + interface OccurrencesResponseItem extends FileSpanWithContext { + /** + * True if the occurrence is a write location, false otherwise. + */ + isWriteAccess: boolean; + /** + * True if the occurrence is in a string, undefined otherwise; + */ + isInString?: true; + } + /** @deprecated */ + interface OccurrencesResponse extends Response { + body?: OccurrencesResponseItem[]; + } /** * Get document highlights request; value of command field is * "documentHighlights". Return response giving spans that are relevant @@ -3848,6 +3874,7 @@ declare namespace ts { private getTypeDefinition; private mapImplementationLocations; private getImplementation; + private getOccurrences; private getSyntacticDiagnosticsSync; private getSemanticDiagnosticsSync; private getSuggestionDiagnosticsSync; @@ -9961,6 +9988,8 @@ declare namespace ts { findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined; getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; getFileReferences(fileName: string): ReferenceEntry[]; + /** @deprecated */ + getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined; getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; getNavigationBarItems(fileName: string): NavigationBarItem[]; getNavigationTree(fileName: string): NavigationTree; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 35f824d416f0b..251acae603dc7 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -6086,6 +6086,8 @@ declare namespace ts { findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined; getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] | undefined; getFileReferences(fileName: string): ReferenceEntry[]; + /** @deprecated */ + getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined; getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles?: boolean): NavigateToItem[]; getNavigationBarItems(fileName: string): NavigationBarItem[]; getNavigationTree(fileName: string): NavigationTree; diff --git a/tests/baselines/reference/tsserver/occurences/should-be-marked-if-only-on-string-values.js b/tests/baselines/reference/tsserver/occurences/should-be-marked-if-only-on-string-values.js index fb0384591b89a..dc1a5669fe7f2 100644 --- a/tests/baselines/reference/tsserver/occurences/should-be-marked-if-only-on-string-values.js +++ b/tests/baselines/reference/tsserver/occurences/should-be-marked-if-only-on-string-values.js @@ -55,14 +55,11 @@ Before request Info 13 [00:00:28.000] request: { - "command": "documentHighlights", + "command": "occurrences", "arguments": { "file": "/a/b/file1.ts", "line": 1, - "offset": 11, - "filesToSearch": [ - "/a/b/file1.ts" - ] + "offset": 11 }, "seq": 2, "type": "request" @@ -71,61 +68,64 @@ Info 14 [00:00:29.000] response: { "response": [ { + "start": { + "line": 1, + "offset": 11 + }, + "end": { + "line": 1, + "offset": 14 + }, "file": "/a/b/file1.ts", - "highlightSpans": [ - { - "start": { - "line": 1, - "offset": 11 - }, - "end": { - "line": 1, - "offset": 14 - }, - "kind": "reference" - }, - { - "start": { - "line": 2, - "offset": 11 - }, - "end": { - "line": 2, - "offset": 14 - }, - "kind": "reference" - }, - { - "start": { - "line": 3, - "offset": 13 - }, - "end": { - "line": 3, - "offset": 16 - }, - "contextStart": { - "line": 3, - "offset": 12 - }, - "contextEnd": { - "line": 3, - "offset": 22 - }, - "kind": "writtenReference" - }, - { - "start": { - "line": 4, - "offset": 14 - }, - "end": { - "line": 4, - "offset": 17 - }, - "kind": "reference" - } - ] + "isWriteAccess": false, + "isInString": true + }, + { + "start": { + "line": 2, + "offset": 11 + }, + "end": { + "line": 2, + "offset": 14 + }, + "file": "/a/b/file1.ts", + "isWriteAccess": false, + "isInString": true + }, + { + "start": { + "line": 3, + "offset": 13 + }, + "end": { + "line": 3, + "offset": 16 + }, + "contextStart": { + "line": 3, + "offset": 12 + }, + "contextEnd": { + "line": 3, + "offset": 22 + }, + "file": "/a/b/file1.ts", + "isWriteAccess": true, + "isInString": true + }, + { + "start": { + "line": 4, + "offset": 14 + }, + "end": { + "line": 4, + "offset": 17 + }, + "file": "/a/b/file1.ts", + "isWriteAccess": false, + "isInString": true } ], "responseRequired": true @@ -136,14 +136,11 @@ Before request Info 15 [00:00:30.000] request: { - "command": "documentHighlights", + "command": "occurrences", "arguments": { "file": "/a/b/file1.ts", "line": 3, - "offset": 13, - "filesToSearch": [ - "/a/b/file1.ts" - ] + "offset": 13 }, "seq": 3, "type": "request" @@ -152,39 +149,36 @@ Info 16 [00:00:31.000] response: { "response": [ { + "start": { + "line": 3, + "offset": 13 + }, + "end": { + "line": 3, + "offset": 16 + }, + "contextStart": { + "line": 3, + "offset": 12 + }, + "contextEnd": { + "line": 3, + "offset": 22 + }, + "file": "/a/b/file1.ts", + "isWriteAccess": true + }, + { + "start": { + "line": 4, + "offset": 14 + }, + "end": { + "line": 4, + "offset": 17 + }, "file": "/a/b/file1.ts", - "highlightSpans": [ - { - "start": { - "line": 3, - "offset": 13 - }, - "end": { - "line": 3, - "offset": 16 - }, - "contextStart": { - "line": 3, - "offset": 12 - }, - "contextEnd": { - "line": 3, - "offset": 22 - }, - "kind": "writtenReference" - }, - { - "start": { - "line": 4, - "offset": 14 - }, - "end": { - "line": 4, - "offset": 17 - }, - "kind": "reference" - } - ] + "isWriteAccess": false } ], "responseRequired": true @@ -195,14 +189,11 @@ Before request Info 17 [00:00:32.000] request: { - "command": "documentHighlights", + "command": "occurrences", "arguments": { "file": "/a/b/file1.ts", "line": 4, - "offset": 14, - "filesToSearch": [ - "/a/b/file1.ts" - ] + "offset": 14 }, "seq": 4, "type": "request" @@ -211,39 +202,36 @@ Info 18 [00:00:33.000] response: { "response": [ { + "start": { + "line": 3, + "offset": 13 + }, + "end": { + "line": 3, + "offset": 16 + }, + "contextStart": { + "line": 3, + "offset": 12 + }, + "contextEnd": { + "line": 3, + "offset": 22 + }, + "file": "/a/b/file1.ts", + "isWriteAccess": true + }, + { + "start": { + "line": 4, + "offset": 14 + }, + "end": { + "line": 4, + "offset": 17 + }, "file": "/a/b/file1.ts", - "highlightSpans": [ - { - "start": { - "line": 3, - "offset": 13 - }, - "end": { - "line": 3, - "offset": 16 - }, - "contextStart": { - "line": 3, - "offset": 12 - }, - "contextEnd": { - "line": 3, - "offset": 22 - }, - "kind": "writtenReference" - }, - { - "start": { - "line": 4, - "offset": 14 - }, - "end": { - "line": 4, - "offset": 17 - }, - "kind": "reference" - } - ] + "isWriteAccess": false } ], "responseRequired": true diff --git a/tests/baselines/reference/tsserver/projects/files-opened-and-closed-affecting-multiple-projects.js b/tests/baselines/reference/tsserver/projects/files-opened-and-closed-affecting-multiple-projects.js index b336146c1eb91..7459418bef7ef 100644 --- a/tests/baselines/reference/tsserver/projects/files-opened-and-closed-affecting-multiple-projects.js +++ b/tests/baselines/reference/tsserver/projects/files-opened-and-closed-affecting-multiple-projects.js @@ -285,14 +285,11 @@ Before request Info 50 [00:01:43.000] request: { - "command": "documentHighlights", + "command": "occurrences", "arguments": { "file": "/a/b/projects/files/file1.ts", "line": 1, - "offset": 11, - "filesToSearch": [ - "/a/b/projects/files/file1.ts" - ] + "offset": 11 }, "seq": 5, "type": "request"