From 5a6eba15918a556e2bcb04638c313ac9dd03212c Mon Sep 17 00:00:00 2001 From: Jojoshua Date: Mon, 30 May 2022 04:11:37 -0400 Subject: [PATCH] (feat) Implement find file references (#1491) #1485 --- packages/language-server/src/ls-config.ts | 4 + .../language-server/src/plugins/PluginHost.ts | 4 + .../language-server/src/plugins/interfaces.ts | 5 ++ .../plugins/typescript/TypeScriptPlugin.ts | 16 ++++ .../features/FindFileReferencesProvider.ts | 51 +++++++++++ packages/language-server/src/server.ts | 4 + .../FindFileReferencesProvider.test.ts | 57 +++++++++++++ .../find-file-references-child.svelte | 6 ++ .../find-file-references-parent.svelte | 10 +++ packages/svelte-vscode/package.json | 26 ++++++ packages/svelte-vscode/src/extension.ts | 3 + .../src/typescript/findFileReferences.ts | 84 +++++++++++++++++++ 12 files changed, 270 insertions(+) create mode 100644 packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts create mode 100644 packages/language-server/test/plugins/typescript/features/FindFileReferencesProvider.test.ts create mode 100644 packages/language-server/test/plugins/typescript/testfiles/find-file-references-child.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/find-file-references-parent.svelte create mode 100644 packages/svelte-vscode/src/typescript/findFileReferences.ts diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts index 75278af7f..4ad2842dc 100644 --- a/packages/language-server/src/ls-config.ts +++ b/packages/language-server/src/ls-config.ts @@ -14,6 +14,7 @@ const defaultLSConfig: LSConfig = { completions: { enable: true }, definitions: { enable: true }, findReferences: { enable: true }, + fileReferences: { enable: true }, documentSymbols: { enable: true }, codeActions: { enable: true }, rename: { enable: true }, @@ -97,6 +98,9 @@ export interface LSTypescriptConfig { findReferences: { enable: boolean; }; + fileReferences: { + enable: boolean; + }; definitions: { enable: boolean; }; diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index 8a31aff66..c763b4441 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -389,6 +389,10 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { ); } + async fileReferences(uri: string): Promise { + return await this.execute('fileReferences', [uri], ExecuteMode.FirstNonNull, 'high'); + } + async getSignatureHelp( textDocument: TextDocumentIdentifier, position: Position, diff --git a/packages/language-server/src/plugins/interfaces.ts b/packages/language-server/src/plugins/interfaces.ts index 10872b124..870acac2c 100644 --- a/packages/language-server/src/plugins/interfaces.ts +++ b/packages/language-server/src/plugins/interfaces.ts @@ -143,6 +143,10 @@ export interface FindReferencesProvider { ): Promise; } +export interface FileReferencesProvider { + fileReferences(uri: string): Promise; +} + export interface SignatureHelpProvider { getSignatureHelp( document: Document, @@ -199,6 +203,7 @@ type ProviderBase = DiagnosticsProvider & UpdateImportsProvider & CodeActionsProvider & FindReferencesProvider & + FileReferencesProvider & RenameProvider & SignatureHelpProvider & SemanticTokensProvider & diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index 386936026..e8ad66e52 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -36,6 +36,7 @@ import { DocumentSymbolsProvider, FileRename, FindReferencesProvider, + FileReferencesProvider, HoverProvider, ImplementationProvider, OnWatchFileChanges, @@ -54,6 +55,7 @@ import { CompletionsProviderImpl } from './features/CompletionProvider'; import { DiagnosticsProviderImpl } from './features/DiagnosticsProvider'; +import { FindFileReferencesProviderImpl } from './features/FindFileReferencesProvider'; import { FindReferencesProviderImpl } from './features/FindReferencesProvider'; import { getDirectiveCommentCompletions } from './features/getDirectiveCommentCompletions'; import { HoverProviderImpl } from './features/HoverProvider'; @@ -85,6 +87,7 @@ export class TypeScriptPlugin UpdateImportsProvider, RenameProvider, FindReferencesProvider, + FileReferencesProvider, SelectionRangeProvider, SignatureHelpProvider, SemanticTokensProvider, @@ -104,6 +107,8 @@ export class TypeScriptPlugin private readonly renameProvider: RenameProviderImpl; private readonly hoverProvider: HoverProviderImpl; private readonly findReferencesProvider: FindReferencesProviderImpl; + private readonly findFileReferencesProvider: FindFileReferencesProviderImpl; + private readonly selectionRangeProvider: SelectionRangeProviderImpl; private readonly signatureHelpProvider: SignatureHelpProviderImpl; private readonly semanticTokensProvider: SemanticTokensProviderImpl; @@ -130,6 +135,9 @@ export class TypeScriptPlugin this.renameProvider = new RenameProviderImpl(this.lsAndTsDocResolver, configManager); this.hoverProvider = new HoverProviderImpl(this.lsAndTsDocResolver); this.findReferencesProvider = new FindReferencesProviderImpl(this.lsAndTsDocResolver); + this.findFileReferencesProvider = new FindFileReferencesProviderImpl( + this.lsAndTsDocResolver + ); this.selectionRangeProvider = new SelectionRangeProviderImpl(this.lsAndTsDocResolver); this.signatureHelpProvider = new SignatureHelpProviderImpl(this.lsAndTsDocResolver); this.semanticTokensProvider = new SemanticTokensProviderImpl(this.lsAndTsDocResolver); @@ -423,6 +431,14 @@ export class TypeScriptPlugin return this.findReferencesProvider.findReferences(document, position, context); } + async fileReferences(uri: string): Promise { + if (!this.featureEnabled('fileReferences')) { + return null; + } + + return this.findFileReferencesProvider.fileReferences(uri); + } + async onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): Promise { let doneUpdateProjectFiles = false; diff --git a/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts b/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts new file mode 100644 index 000000000..64b7ce21d --- /dev/null +++ b/packages/language-server/src/plugins/typescript/features/FindFileReferencesProvider.ts @@ -0,0 +1,51 @@ +import { Location } from 'vscode-languageserver'; +import { URI } from 'vscode-uri'; +import { pathToUrl } from '../../../utils'; +import { FileReferencesProvider } from '../../interfaces'; +import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; +import { convertToLocationRange, hasNonZeroRange } from '../utils'; +import { SnapshotFragmentMap } from './utils'; + +export class FindFileReferencesProviderImpl implements FileReferencesProvider { + constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {} + + async fileReferences(uri: string): Promise { + const u = URI.parse(uri); + const fileName = u.fsPath; + + const lang = await this.getLSForPath(fileName); + const tsDoc = await this.getSnapshotForPath(fileName); + const fragment = tsDoc.getFragment(); + + const references = lang.getFileReferences(fileName); + + if (!references) { + return null; + } + + const docs = new SnapshotFragmentMap(this.lsAndTsDocResolver); + docs.set(tsDoc.filePath, { fragment, snapshot: tsDoc }); + + const locations = await Promise.all( + references.map(async (ref) => { + const defDoc = await docs.retrieveFragment(ref.fileName); + + return Location.create( + pathToUrl(ref.fileName), + convertToLocationRange(defDoc, ref.textSpan) + ); + }) + ); + // Some references are in generated code but not wrapped with explicit ignore comments. + // These show up as zero-length ranges, so filter them out. + return locations.filter(hasNonZeroRange); + } + + private async getLSForPath(path: string) { + return this.lsAndTsDocResolver.getLSForPath(path); + } + + private async getSnapshotForPath(path: string) { + return this.lsAndTsDocResolver.getSnapshot(path); + } +} diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 0b4e12bba..3871ddd1c 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -426,6 +426,10 @@ export function startServer(options?: LSOptions) { pluginHost.updateImports(fileRename) ); + connection.onRequest('$/getFileReferences', async (uri: string) => { + return pluginHost.fileReferences(uri); + }); + connection.onRequest('$/getCompiledCode', async (uri: DocumentUri) => { const doc = docManager.get(uri); if (!doc) { diff --git a/packages/language-server/test/plugins/typescript/features/FindFileReferencesProvider.test.ts b/packages/language-server/test/plugins/typescript/features/FindFileReferencesProvider.test.ts new file mode 100644 index 000000000..fe7dde00f --- /dev/null +++ b/packages/language-server/test/plugins/typescript/features/FindFileReferencesProvider.test.ts @@ -0,0 +1,57 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import ts from 'typescript'; +import { Location, Position, Range } from 'vscode-languageserver'; +import { Document, DocumentManager } from '../../../../src/lib/documents'; +import { LSConfigManager } from '../../../../src/ls-config'; +import { FindFileReferencesProviderImpl } from '../../../../src/plugins/typescript/features/FindFileReferencesProvider'; +import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDocResolver'; +import { pathToUrl } from '../../../../src/utils'; + +const testDir = path.join(__dirname, '..'); + +describe('FindFileReferencesProvider', () => { + function getFullPath(filename: string) { + return path.join(testDir, 'testfiles', filename); + } + function getUri(filename: string) { + const filePath = path.join(testDir, 'testfiles', filename); + return pathToUrl(filePath); + } + + function setup(filename: string) { + const docManager = new DocumentManager( + (textDocument) => new Document(textDocument.uri, textDocument.text) + ); + const lsConfigManager = new LSConfigManager(); + const lsAndTsDocResolver = new LSAndTSDocResolver(docManager, [testDir], lsConfigManager); + const provider = new FindFileReferencesProviderImpl(lsAndTsDocResolver); + const document = openDoc(filename); + return { provider, document, openDoc }; + + function openDoc(filename: string) { + const filePath = getFullPath(filename); + const doc = docManager.openDocument({ + uri: pathToUrl(filePath), + text: ts.sys.readFile(filePath) || '' + }); + return doc; + } + } + + it('finds file references', async () => { + const { provider, document, openDoc } = setup('find-file-references-child.svelte'); + //Make known all the associated files + openDoc('find-file-references-parent.svelte'); + + const results = await provider.fileReferences(document.uri.toString()); + const expectedResults = [ + Location.create( + getUri('find-file-references-parent.svelte'), + Range.create(Position.create(1, 37), Position.create(1, 72)) + ) + ]; + + assert.deepStrictEqual(results, expectedResults); + }); +}); diff --git a/packages/language-server/test/plugins/typescript/testfiles/find-file-references-child.svelte b/packages/language-server/test/plugins/typescript/testfiles/find-file-references-child.svelte new file mode 100644 index 000000000..d4379fc66 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/find-file-references-child.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/language-server/test/plugins/typescript/testfiles/find-file-references-parent.svelte b/packages/language-server/test/plugins/typescript/testfiles/find-file-references-parent.svelte new file mode 100644 index 000000000..0be8aab96 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/find-file-references-parent.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/svelte-vscode/package.json b/packages/svelte-vscode/package.json index 72913c504..e3fb79c4d 100644 --- a/packages/svelte-vscode/package.json +++ b/packages/svelte-vscode/package.json @@ -534,6 +534,10 @@ { "command": "svelte.extractComponent", "title": "Svelte: Extract Component" + }, + { + "command": "svelte.typescript.findAllFileReferences", + "title": "Svelte: Find File References" } ], "menus": { @@ -541,6 +545,10 @@ { "command": "svelte.showCompiledCodeToSide", "when": "editorLangId == svelte" + }, + { + "command": "svelte.typescript.findAllFileReferences", + "when": "editorLangId == svelte && resourceScheme == file" } ], "editor/title": [ @@ -550,11 +558,29 @@ "group": "navigation" } ], + "editor/title/context": [ + { + "command": "svelte.typescript.findAllFileReferences", + "when": "resourceLangId == svelte && resourceScheme == file" + } + ], "editor/context": [ { "command": "svelte.extractComponent", "when": "editorLangId == svelte", "group": "1_modification" + }, + { + "command": "svelte.typescript.findAllFileReferences", + "when": "editorLangId == svelte", + "group": "4_search" + } + ], + "explorer/context": [ + { + "command": "svelte.typescript.findAllFileReferences", + "when": "resourceLangId == svelte", + "group": "4_search" } ] }, diff --git a/packages/svelte-vscode/src/extension.ts b/packages/svelte-vscode/src/extension.ts index 9c7e9700b..1b7eddbcd 100644 --- a/packages/svelte-vscode/src/extension.ts +++ b/packages/svelte-vscode/src/extension.ts @@ -29,6 +29,7 @@ import CompiledCodeContentProvider from './CompiledCodeContentProvider'; import { activateTagClosing } from './html/autoClose'; import { EMPTY_ELEMENTS } from './html/htmlEmptyTagsShared'; import { TsPlugin } from './tsplugin'; +import { addFindFileReferencesListener } from './typescript/findFileReferences'; namespace TagCloseRequest { export const type: RequestType = new RequestType( @@ -223,6 +224,8 @@ export function activateSvelteLanguageServer(context: ExtensionContext) { addDidChangeTextDocumentListener(getLS); + addFindFileReferencesListener(getLS, context); + addRenameFileListener(getLS); addCompilePreviewCommand(getLS, context); diff --git a/packages/svelte-vscode/src/typescript/findFileReferences.ts b/packages/svelte-vscode/src/typescript/findFileReferences.ts new file mode 100644 index 000000000..69da5ad6d --- /dev/null +++ b/packages/svelte-vscode/src/typescript/findFileReferences.ts @@ -0,0 +1,84 @@ +import { + commands, + ExtensionContext, + ProgressLocation, + Uri, + window, + workspace, + Position, + Location, + Range +} from 'vscode'; +import { LanguageClient } from 'vscode-languageclient/node'; +import { Location as LSLocation } from 'vscode-languageclient'; + +/** + * adopted from https://github.com/microsoft/vscode/blob/5f3e9c120a4407de3e55465588ce788618526eb0/extensions/typescript-language-features/src/languageFeatures/fileReferences.ts + */ +export async function addFindFileReferencesListener( + getLS: () => LanguageClient, + context: ExtensionContext +) { + const disposable = commands.registerCommand('svelte.typescript.findAllFileReferences', handler); + + context.subscriptions.push(disposable); + + async function handler(resource?: Uri) { + if (!resource) { + resource = window.activeTextEditor?.document.uri; + } + + if (!resource || resource.scheme !== 'file') { + return; + } + + const document = await workspace.openTextDocument(resource); + + await window.withProgress( + { + location: ProgressLocation.Window, + title: 'Finding file references' + }, + async (_, token) => { + const lsLocations = await getLS().sendRequest( + '$/getFileReferences', + document.uri.toString(), + token + ); + + if (!lsLocations) { + return; + } + + const config = workspace.getConfiguration('references'); + const existingSetting = config.inspect('preferredLocation'); + + await config.update('preferredLocation', 'view'); + try { + await commands.executeCommand( + 'editor.action.showReferences', + resource, + new Position(0, 0), + lsLocations.map( + (ref) => + new Location( + Uri.parse(ref.uri), + new Range( + ref.range.start.line, + ref.range.start.character, + ref.range.end.line, + ref.range.end.character + ) + ) + ) + ); + } finally { + await config.update( + 'preferredLocation', + existingSetting?.workspaceFolderValue ?? existingSetting?.workspaceValue + ); + } + } + ); + } +}