From 8e205765a0c47e4b1a4eff02e94068bac660c312 Mon Sep 17 00:00:00 2001 From: James Birtles Date: Sun, 5 May 2019 19:41:13 +0100 Subject: [PATCH] add go to definition support to ts/js --- README.md | 1 + src/api/fragmentPositions.ts | 10 ++++++ src/api/interfaces.ts | 14 +++++++++ src/api/wrapFragmentPlugin.ts | 24 +++++++++++++- src/lib/documents/DocumentManager.ts | 19 +++++++++++ src/plugins/TypeScriptPlugin.ts | 47 +++++++++++++++++++++++++--- src/plugins/typescript/service.ts | 2 +- src/server.ts | 2 ++ src/utils.ts | 4 +++ 9 files changed, 117 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 9904808..843af23 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Currently Supported: - Formatting (via [prettier](https://github.com/prettier/prettier)) - Symbols in Outline panel - Autocompletions + - Go to definition ## How can I use it? diff --git a/src/api/fragmentPositions.ts b/src/api/fragmentPositions.ts index 342425c..0310068 100644 --- a/src/api/fragmentPositions.ts +++ b/src/api/fragmentPositions.ts @@ -9,6 +9,7 @@ import { ColorPresentation, SymbolInformation, Location, + LocationLink, } from './interfaces'; export function mapRangeToParent(fragment: Fragment, range: Range): Range { @@ -90,3 +91,12 @@ export function mapSymbolInformationToParent( ): SymbolInformation { return { ...info, location: mapLocationToParent(fragment, info.location) }; } + +export function mapLocationLinkToParent(fragment: Fragment, def: LocationLink): LocationLink { + return LocationLink.create( + def.targetUri, + def.targetRange, + def.targetSelectionRange, + def.originSelectionRange ? mapRangeToParent(fragment, def.originSelectionRange) : undefined, + ); +} diff --git a/src/api/interfaces.ts b/src/api/interfaces.ts index c2b39b9..e0f38be 100644 --- a/src/api/interfaces.ts +++ b/src/api/interfaces.ts @@ -19,6 +19,8 @@ import { SymbolInformation, Location, SymbolKind, + DefinitionLink, + LocationLink, } from 'vscode-languageserver-types'; import { Document } from './Document'; @@ -43,6 +45,8 @@ export { SymbolInformation, Location, SymbolKind, + DefinitionLink, + LocationLink, }; export type Resolvable = T | Promise; @@ -135,6 +139,16 @@ export namespace DocumentSymbolsProvider { } } +export interface DefinitionsProvider { + getDefinitions(document: Document, position: Position): Resolvable; +} + +export namespace DefinitionsProvider { + export function is(obj: any): obj is DefinitionsProvider { + return typeof obj.getDefinitions === 'function'; + } +} + export interface Fragment extends Document { details: FragmentDetails; diff --git a/src/api/wrapFragmentPlugin.ts b/src/api/wrapFragmentPlugin.ts index 522c35f..3f22c71 100644 --- a/src/api/wrapFragmentPlugin.ts +++ b/src/api/wrapFragmentPlugin.ts @@ -17,6 +17,7 @@ import { ColorPresentation, DocumentSymbolsProvider, SymbolInformation, + DefinitionsProvider, } from './interfaces'; import { Document } from './Document'; import { @@ -28,6 +29,7 @@ import { mapRangeToFragment, mapColorPresentationToParent, mapSymbolInformationToParent, + mapLocationLinkToParent, } from './fragmentPositions'; import { Host, OnRegister } from './Host'; @@ -108,13 +110,18 @@ export function wrapFragmentPlugin

( plugin.getCompletions = async function( document: Document, position: Position, + triggerCharacter?: string, ): Promise { const fragment = getFragment(document); if (!fragment || !fragment.isInFragment(position)) { return []; } - const items = await getCompletions(fragment, fragment.positionInFragment(position)); + const items = await getCompletions( + fragment, + fragment.positionInFragment(position), + triggerCharacter, + ); return items.map(item => mapCompletionItemToParent(fragment, item)); }; } @@ -189,5 +196,20 @@ export function wrapFragmentPlugin

( }; } + if (DefinitionsProvider.is(plugin)) { + const getDefinitions: DefinitionsProvider['getDefinitions'] = plugin.getDefinitions.bind( + plugin, + ); + plugin.getDefinitions = async function(document, position) { + const fragment = getFragment(document); + if (!fragment || !fragment.isInFragment(position)) { + return []; + } + + const items = await getDefinitions(fragment, fragment.positionInFragment(position)); + return items.map(item => mapLocationLinkToParent(fragment, item)); + }; + } + return plugin; } diff --git a/src/lib/documents/DocumentManager.ts b/src/lib/documents/DocumentManager.ts index 64ae2eb..141213f 100644 --- a/src/lib/documents/DocumentManager.ts +++ b/src/lib/documents/DocumentManager.ts @@ -5,6 +5,7 @@ import { TextDocumentIdentifier, CompletionItem, TextEdit, + DefinitionLink, } from 'vscode-languageserver-types'; import { PluginHost, ExecuteMode } from '../PluginHost'; import { flatten } from '../../utils'; @@ -205,4 +206,22 @@ export class DocumentManager extends PluginHost { ), ); } + + async getDefinitions( + textDocument: TextDocumentIdentifier, + position: Position, + ): Promise { + const document = this.documents.get(textDocument.uri); + if (!document) { + throw new Error('Cannot call methods on an unopened document'); + } + + return flatten( + await this.execute( + 'getDefinitions', + [document, position], + ExecuteMode.Collect, + ), + ); + } } diff --git a/src/plugins/TypeScriptPlugin.ts b/src/plugins/TypeScriptPlugin.ts index 1bd6789..4fd607c 100644 --- a/src/plugins/TypeScriptPlugin.ts +++ b/src/plugins/TypeScriptPlugin.ts @@ -1,5 +1,4 @@ import ts, { NavigationTree } from 'typescript'; -import URL from 'vscode-uri'; import { DiagnosticsProvider, Document, @@ -16,6 +15,9 @@ import { SymbolInformation, CompletionsProvider, CompletionItem, + DefinitionsProvider, + DefinitionLink, + LocationLink, } from '../api'; import { convertRange, @@ -25,6 +27,8 @@ import { getCommitCharactersForScriptElement, } from './typescript/utils'; import { getLanguageServiceForDocument, CreateDocument } from './typescript/service'; +import { pathToUrl } from '../utils'; +import { TextDocument } from '../lib/documents/TextDocument'; export class TypeScriptPlugin implements @@ -32,7 +36,8 @@ export class TypeScriptPlugin HoverProvider, OnRegister, DocumentSymbolsProvider, - CompletionsProvider { + CompletionsProvider, + DefinitionsProvider { public static matchFragment(fragment: Fragment) { return fragment.details.attributes.tag == 'script'; } @@ -50,7 +55,7 @@ export class TypeScriptPlugin onRegister(host: Host) { this.host = host; this.createDocument = (fileName, content) => { - const uri = URL.file(fileName).toString(); + const uri = pathToUrl(fileName); const document = host.openDocument({ languageId: '', text: content, @@ -172,7 +177,6 @@ export class TypeScriptPlugin triggerCharacter: triggerCharacter as any, }, ); - console.log(completions); if (!completions) { return []; @@ -188,4 +192,39 @@ export class TypeScriptPlugin }; }); } + + getDefinitions(document: Document, position: Position): DefinitionLink[] { + const lang = getLanguageServiceForDocument(document, this.createDocument); + + const defs = lang.getDefinitionAndBoundSpan( + document.getFilePath()!, + document.offsetAt(position), + ); + + if (!defs || !defs.definitions) { + return []; + } + + const docs = new Map([[document.getFilePath()!, document]]); + + return defs.definitions + .map(def => { + let defDoc = docs.get(def.fileName); + if (!defDoc) { + defDoc = new TextDocument( + pathToUrl(def.fileName), + ts.sys.readFile(def.fileName) || '', + ); + docs.set(def.fileName, defDoc); + } + + return LocationLink.create( + pathToUrl(def.fileName), + convertRange(defDoc, def.textSpan), + convertRange(defDoc, def.textSpan), + convertRange(document, defs.textSpan), + ); + }) + .filter(res => !!res) as DefinitionLink[]; + } } diff --git a/src/plugins/typescript/service.ts b/src/plugins/typescript/service.ts index 93cf6a2..5f493db 100644 --- a/src/plugins/typescript/service.ts +++ b/src/plugins/typescript/service.ts @@ -81,7 +81,7 @@ export function createLanguageService( return doc; } - return ts.ScriptSnapshot.fromString(ts.sys.readFile(fileName) || ''); + return ts.ScriptSnapshot.fromString(this.readFile!(fileName) || ''); }, getCurrentDirectory: () => workspacePath, getDefaultLibFileName: ts.getDefaultLibFilePath, diff --git a/src/server.ts b/src/server.ts index 1142602..5cb9949 100644 --- a/src/server.ts +++ b/src/server.ts @@ -52,6 +52,7 @@ export function startServer() { documentFormattingProvider: true, colorProvider: true, documentSymbolProvider: true, + definitionProvider: true, }, }; }); @@ -82,6 +83,7 @@ export function startServer() { manager.getColorPresentations(evt.textDocument, evt.range, evt.color), ); connection.onDocumentSymbol(evt => manager.getDocumentSymbols(evt.textDocument)); + connection.onDefinition(evt => manager.getDefinitions(evt.textDocument, evt.position)); manager.on('documentChange', async document => { const diagnostics = await manager.getDiagnostics({ uri: document.getURL() }); diff --git a/src/utils.ts b/src/utils.ts index 46f4912..0ae44ae 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,6 +12,10 @@ export function urlToPath(stringUrl: string): string | null { return url.fsPath.replace(/\\/g, '/'); } +export function pathToUrl(path: string) { + return URL.file(path).toString(); +} + export function flatten(arr: T[][]): T[] { return arr.reduce((all, item) => [...all, ...item], []); }