From 4834c090f85e21a63c5e64fecfb70e090b5708a6 Mon Sep 17 00:00:00 2001 From: Matthew Phillips Date: Thu, 20 May 2021 15:14:27 -0400 Subject: [PATCH] Support for Go to Definition in Astro components (#220) * Start on css completion * Support for CSS completions * Adds support for Go to Definition in TypeScript in Astro * Run formatting * Add support for Astro component go to definition * Formatting * Jump directly to file where definition is found --- tools/astro-languageserver/src/index.ts | 2 +- .../src/plugins/astro/AstroPlugin.ts | 102 +++++++++++++++++- .../plugins/typescript/TypeScriptPlugin.ts | 10 +- .../src/plugins/typescript/languageService.ts | 6 +- .../src/plugins/typescript/utils.ts | 6 +- 5 files changed, 116 insertions(+), 10 deletions(-) diff --git a/tools/astro-languageserver/src/index.ts b/tools/astro-languageserver/src/index.ts index e3532f2520..5e4c736a2e 100644 --- a/tools/astro-languageserver/src/index.ts +++ b/tools/astro-languageserver/src/index.ts @@ -23,7 +23,7 @@ export function startServer() { filterIncompleteCompletions: !evt.initializationOptions?.dontFilterIncompleteCompletions, definitionLinkSupport: !!evt.capabilities.textDocument?.definition?.linkSupport, }); - pluginHost.register(new AstroPlugin(docManager, configManager)); + pluginHost.register(new AstroPlugin(docManager, configManager, workspaceUris)); pluginHost.register(new HTMLPlugin(docManager, configManager)); pluginHost.register(new CSSPlugin(docManager, configManager)); pluginHost.register(new TypeScriptPlugin(docManager, configManager, workspaceUris)); diff --git a/tools/astro-languageserver/src/plugins/astro/AstroPlugin.ts b/tools/astro-languageserver/src/plugins/astro/AstroPlugin.ts index 6baf407a59..3d5d10430f 100644 --- a/tools/astro-languageserver/src/plugins/astro/AstroPlugin.ts +++ b/tools/astro-languageserver/src/plugins/astro/AstroPlugin.ts @@ -1,17 +1,36 @@ +import { DefinitionLink } from 'vscode-languageserver'; import type { Document, DocumentManager } from '../../core/documents'; import type { ConfigManager } from '../../core/config'; -import type { CompletionsProvider, AppCompletionItem, AppCompletionList, FoldingRangeProvider } from '../interfaces'; -import { CompletionContext, Position, CompletionList, CompletionItem, CompletionItemKind, InsertTextFormat, FoldingRange, TextEdit } from 'vscode-languageserver'; -import { isPossibleClientComponent } from '../../utils'; +import type { CompletionsProvider, AppCompletionList, FoldingRangeProvider } from '../interfaces'; +import { + CompletionContext, + Position, + CompletionList, + CompletionItem, + CompletionItemKind, + InsertTextFormat, + LocationLink, + FoldingRange, + Range, + TextEdit, +} from 'vscode-languageserver'; +import { Node } from 'vscode-html-languageservice'; +import { isPossibleClientComponent, pathToUrl, urlToPath } from '../../utils'; +import { isInsideFrontmatter } from '../../core/documents/utils'; +import * as ts from 'typescript'; +import { LanguageServiceManager as TypeScriptLanguageServiceManager } from '../typescript/LanguageServiceManager'; +import { ensureRealFilePath } from '../typescript/utils'; import { FoldingRangeKind } from 'vscode-languageserver-types'; export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider { private readonly docManager: DocumentManager; private readonly configManager: ConfigManager; + private readonly tsLanguageServiceManager: TypeScriptLanguageServiceManager; - constructor(docManager: DocumentManager, configManager: ConfigManager) { + constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) { this.docManager = docManager; this.configManager = configManager; + this.tsLanguageServiceManager = new TypeScriptLanguageServiceManager(docManager, configManager, workspaceUris); } async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise { @@ -53,6 +72,53 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider { ]; } + async getDefinitions(document: Document, position: Position): Promise { + if (this.isInsideFrontmatter(document, position)) { + return []; + } + + const offset = document.offsetAt(position); + const html = document.html; + + const node = html.findNodeAt(offset); + if (!this.isComponentTag(node)) { + return []; + } + + const [componentName] = node.tag!.split(':'); + + const filePath = urlToPath(document.uri); + const tsFilePath = filePath + '.ts'; + + const { lang, tsDoc } = await this.tsLanguageServiceManager.getTypeScriptDoc(document); + + const sourceFile = lang.getProgram()?.getSourceFile(tsFilePath); + if (!sourceFile) { + return []; + } + + const specifier = this.getImportSpecifierForIdentifier(sourceFile, componentName); + if(!specifier) { + return []; + } + + const defs = lang.getDefinitionAtPosition(tsFilePath, specifier.getStart()); + if(!defs) { + return []; + } + + const tsFragment = await tsDoc.getFragment(); + const startRange: Range = Range.create(Position.create(0, 0), Position.create(0, 0)); + const links = defs.map(def => { + const defFilePath = ensureRealFilePath(def.fileName); + return LocationLink.create( + pathToUrl(defFilePath), startRange, startRange + ); + }); + + return links; + } + private getClientHintCompletion(document: Document, position: Position, completionContext?: CompletionContext): CompletionItem[] | null { const node = document.html.findNodeAt(document.offsetAt(position)); if (!isPossibleClientComponent(node)) return null; @@ -104,4 +170,32 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider { } return null; } + + private isInsideFrontmatter(document: Document, position: Position) { + return isInsideFrontmatter(document.getText(), document.offsetAt(position)); + } + + private isComponentTag(node: Node): boolean { + if (!node.tag) { + return false; + } + const firstChar = node.tag[0]; + return /[A-Z]/.test(firstChar); + } + + private getImportSpecifierForIdentifier(sourceFile: ts.SourceFile, identifier: string): ts.Expression | undefined { + let importSpecifier: ts.Expression | undefined = undefined; + ts.forEachChild(sourceFile, (tsNode) => { + if (ts.isImportDeclaration(tsNode)) { + if (tsNode.importClause) { + const { name } = tsNode.importClause; + if (name && name.getText() === identifier) { + importSpecifier = tsNode.moduleSpecifier; + return true; + } + } + } + }); + return importSpecifier; + } } diff --git a/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts b/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts index 30781a5087..a3521a3026 100644 --- a/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts +++ b/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts @@ -1,4 +1,4 @@ -import type { Document, DocumentManager } from '../../core/documents'; +import { Document, DocumentManager, isInsideFrontmatter } from '../../core/documents'; import type { ConfigManager } from '../../core/config'; import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces'; import { CompletionContext, DefinitionLink, FileChangeType, Position, LocationLink } from 'vscode-languageserver'; @@ -36,6 +36,10 @@ export class TypeScriptPlugin implements CompletionsProvider { } async getDefinitions(document: Document, position: Position): Promise { + if(!this.isInsideFrontmatter(document, position)) { + return []; + } + const { lang, tsDoc } = await this.languageServiceManager.getTypeScriptDoc(document); const mainFragment = await tsDoc.getFragment(); @@ -102,4 +106,8 @@ export class TypeScriptPlugin implements CompletionsProvider { public async getSnapshotManager(fileName: string) { return this.languageServiceManager.getSnapshotManager(fileName); } + + private isInsideFrontmatter(document: Document, position: Position) { + return isInsideFrontmatter(document.getText(), document.offsetAt(position)); + } } diff --git a/tools/astro-languageserver/src/plugins/typescript/languageService.ts b/tools/astro-languageserver/src/plugins/typescript/languageService.ts index b7ff6df203..22e2b1cdd5 100644 --- a/tools/astro-languageserver/src/plugins/typescript/languageService.ts +++ b/tools/astro-languageserver/src/plugins/typescript/languageService.ts @@ -31,9 +31,9 @@ export async function getLanguageService(path: string, workspaceUris: string[], if (services.has(tsconfigPath)) { service = (await services.get(tsconfigPath)) as LanguageServiceContainer; } else { - const newService = createLanguageService(tsconfigPath, workspaceRoot, docContext); - services.set(tsconfigPath, newService); - service = await newService; + const newServicePromise = createLanguageService(tsconfigPath, workspaceRoot, docContext); + services.set(tsconfigPath, newServicePromise); + service = await newServicePromise; } return service; diff --git a/tools/astro-languageserver/src/plugins/typescript/utils.ts b/tools/astro-languageserver/src/plugins/typescript/utils.ts index b212d9cd32..1b1a673da6 100644 --- a/tools/astro-languageserver/src/plugins/typescript/utils.ts +++ b/tools/astro-languageserver/src/plugins/typescript/utils.ts @@ -189,6 +189,10 @@ export function ensureRealAstroFilePath(filePath: string) { return isVirtualAstroFilePath(filePath) ? toRealAstroFilePath(filePath) : filePath; } +export function ensureRealFilePath(filePath: string) { + return isVirtualFilePath(filePath) ? filePath.slice(0, 3) : filePath; +} + export function findTsConfigPath(fileName: string, rootUris: string[]) { const searchDir = dirname(fileName); const path = ts.findConfigFile(searchDir, ts.sys.fileExists, 'tsconfig.json') || ts.findConfigFile(searchDir, ts.sys.fileExists, 'jsconfig.json') || ''; @@ -229,4 +233,4 @@ function append(result: string, str: string, n: number): string { str += str; } return result; -} +} \ No newline at end of file