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:
Matthew Phillips 2021-05-20 15:14:27 -04:00 committed by GitHub
parent 6ce068b838
commit 4834c090f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 116 additions and 10 deletions

View file

@ -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));

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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;

View file

@ -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;
}
}