diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e97139..623693e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] - Added: Generator command for a ViewModel class +- Added: Jump-to-definition for magento modules (in module.xml and routes.xml) - Fixed: Method plugin hover messages are now grouped and include a link to di.xml ## [1.3.1] - 2025-03-23 diff --git a/src/common/xml/XmlDocumentParser.ts b/src/common/xml/XmlDocumentParser.ts new file mode 100644 index 0000000..4dbfe1f --- /dev/null +++ b/src/common/xml/XmlDocumentParser.ts @@ -0,0 +1,36 @@ +import { DocumentCstNode, parse } from '@xml-tools/parser'; +import DocumentCache from 'cache/DocumentCache'; +import PhpParser from 'parser/php/Parser'; +import { TextDocument } from 'vscode'; +import { buildAst, XMLDocument } from '@xml-tools/ast'; + +export interface TokenData { + cst: DocumentCstNode; + // xml-tools parser doesnt have an exported type for this + tokenVector: any[]; + ast: XMLDocument; +} + +class XmlDocumentParser { + protected readonly parser: PhpParser; + + constructor() { + this.parser = new PhpParser(); + } + + public async parse(document: TextDocument): Promise { + const cacheKey = `xml-file`; + + if (DocumentCache.has(document, cacheKey)) { + return DocumentCache.get(document, cacheKey); + } + + const { cst, tokenVector } = parse(document.getText()); + const ast = buildAst(cst as DocumentCstNode, tokenVector); + const tokenData: TokenData = { cst: cst as DocumentCstNode, tokenVector, ast }; + DocumentCache.set(document, cacheKey, tokenData); + return tokenData; + } +} + +export default new XmlDocumentParser(); diff --git a/src/definition/XmlDefinitionProvider.ts b/src/definition/XmlDefinitionProvider.ts new file mode 100644 index 0000000..908da4c --- /dev/null +++ b/src/definition/XmlDefinitionProvider.ts @@ -0,0 +1,43 @@ +import { minimatch } from 'minimatch'; +import { + CancellationToken, + DefinitionProvider, + LocationLink, + Position, + TextDocument, +} from 'vscode'; +import { getSuggestions, SuggestionProviders } from '@xml-tools/content-assist'; +import XmlDocumentParser from 'common/xml/XmlDocumentParser'; + +export abstract class XmlDefinitionProvider implements DefinitionProvider { + public abstract getFilePatterns(): string[]; + public abstract getDefinitionProviders(): SuggestionProviders; + + public async provideDefinition( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + if (!this.canProvideDefinition(document)) { + return []; + } + + const { cst, tokenVector, ast } = await XmlDocumentParser.parse(document); + + const definitions = getSuggestions({ + ast, + cst, + tokenVector, + offset: document.offsetAt(position), + providers: this.getDefinitionProviders(), + }); + + return definitions.filter(definition => definition.targetUri.fsPath !== document.uri.fsPath); + } + + private canProvideDefinition(document: TextDocument): boolean { + return this.getFilePatterns().some(pattern => + minimatch(document.uri.fsPath, pattern, { matchBase: true }) + ); + } +} diff --git a/src/definition/XmlModuleDefinitionProvider.ts b/src/definition/XmlModuleDefinitionProvider.ts new file mode 100644 index 0000000..7b706f6 --- /dev/null +++ b/src/definition/XmlModuleDefinitionProvider.ts @@ -0,0 +1,59 @@ +import { AttributeValueCompletionOptions, SuggestionProviders } from '@xml-tools/content-assist'; +import { XmlDefinitionProvider } from './XmlDefinitionProvider'; +import { LocationLink, Uri, Range } from 'vscode'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import IndexManager from 'indexer/IndexManager'; + +export class XmlModuleDefinitionProvider extends XmlDefinitionProvider { + public getFilePatterns(): string[] { + return ['**/etc/module.xml', '**/etc/**/routes.xml']; + } + + public getDefinitionProviders(): SuggestionProviders { + return { + attributeValue: [this.getModuleDefinitions], + }; + } + + private getModuleDefinitions({ + element, + attribute, + }: AttributeValueCompletionOptions): LocationLink[] { + if (element.name !== 'module' || attribute.key !== 'name') { + return []; + } + + const moduleName = attribute.value; + + if (!moduleName) { + return []; + } + + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + return []; + } + + const module = moduleIndexData.getModule(moduleName); + + if (!module) { + return []; + } + + const moduleXmlUri = Uri.file(module.moduleXmlPath); + + return [ + { + targetUri: moduleXmlUri, + targetRange: new Range(0, 0, 0, 0), + originSelectionRange: new Range( + attribute.position.startLine - 1, + attribute.position.startColumn + 5, + attribute.position.endLine - 1, + attribute.position.endColumn - 1 + ), + }, + ]; + } +} diff --git a/src/extension.ts b/src/extension.ts index 376445b..54928c6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,6 +13,7 @@ import Magento from 'util/Magento'; import { WorkspaceFolder } from 'vscode'; import Logger from 'util/Logger'; import { Command } from 'command/Command'; +import { XmlModuleDefinitionProvider } from 'definition/XmlModuleDefinitionProvider'; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -90,7 +91,8 @@ export async function activate(context: vscode.ExtensionContext) { // definition providers context.subscriptions.push( - vscode.languages.registerDefinitionProvider('xml', new XmlClasslikeDefinitionProvider()) + vscode.languages.registerDefinitionProvider('xml', new XmlClasslikeDefinitionProvider()), + vscode.languages.registerDefinitionProvider('xml', new XmlModuleDefinitionProvider()) ); // codelens providers diff --git a/src/indexer/module/ModuleIndexer.ts b/src/indexer/module/ModuleIndexer.ts index e12527f..87b74e5 100644 --- a/src/indexer/module/ModuleIndexer.ts +++ b/src/indexer/module/ModuleIndexer.ts @@ -47,6 +47,7 @@ export default class ModuleIndexer extends Indexer { name: moduleName, version: setupVersion, sequence: sequence.map((module: any) => module['@_name']), + moduleXmlPath: uri.fsPath, path: Uri.joinPath(uri, '..', '..').fsPath, location: uri.fsPath.includes('vendor') ? 'vendor' : 'app', }; diff --git a/src/indexer/module/types.ts b/src/indexer/module/types.ts index 47c0a4a..9e878b4 100644 --- a/src/indexer/module/types.ts +++ b/src/indexer/module/types.ts @@ -3,5 +3,6 @@ export interface Module { version?: string; sequence: string[]; path: string; + moduleXmlPath: string; location: 'vendor' | 'app'; }