mirror of
https://github.com/withastro/astro.git
synced 2025-01-23 11:01:54 -05:00
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
This commit is contained in:
parent
6ce068b838
commit
4834c090f8
5 changed files with 116 additions and 10 deletions
|
@ -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));
|
||||
|
|
|
@ -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<AppCompletionList | null> {
|
||||
|
@ -53,6 +72,53 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
|
|||
];
|
||||
}
|
||||
|
||||
async getDefinitions(document: Document, position: Position): Promise<DefinitionLink[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<DefinitionLink[]> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue