Skip to content

Commit 929443c

Browse files
committed
feat: module xml definitions
1 parent 8d5b1c4 commit 929443c

File tree

7 files changed

+144
-1
lines changed

7 files changed

+144
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how
66

77
## [Unreleased]
88
- Added: Generator command for a ViewModel class
9+
- Added: Jump-to-definition for magento modules (in module.xml and routes.xml)
910
- Fixed: Method plugin hover messages are now grouped and include a link to di.xml
1011

1112
## [1.3.1] - 2025-03-23
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { DocumentCstNode, parse } from '@xml-tools/parser';
2+
import DocumentCache from 'cache/DocumentCache';
3+
import PhpParser from 'parser/php/Parser';
4+
import { TextDocument } from 'vscode';
5+
import { buildAst, XMLDocument } from '@xml-tools/ast';
6+
7+
export interface TokenData {
8+
cst: DocumentCstNode;
9+
// xml-tools parser doesnt have an exported type for this
10+
tokenVector: any[];
11+
ast: XMLDocument;
12+
}
13+
14+
class XmlDocumentParser {
15+
protected readonly parser: PhpParser;
16+
17+
constructor() {
18+
this.parser = new PhpParser();
19+
}
20+
21+
public async parse(document: TextDocument): Promise<TokenData> {
22+
const cacheKey = `xml-file`;
23+
24+
if (DocumentCache.has(document, cacheKey)) {
25+
return DocumentCache.get(document, cacheKey);
26+
}
27+
28+
const { cst, tokenVector } = parse(document.getText());
29+
const ast = buildAst(cst as DocumentCstNode, tokenVector);
30+
const tokenData: TokenData = { cst: cst as DocumentCstNode, tokenVector, ast };
31+
DocumentCache.set(document, cacheKey, tokenData);
32+
return tokenData;
33+
}
34+
}
35+
36+
export default new XmlDocumentParser();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { minimatch } from 'minimatch';
2+
import {
3+
CancellationToken,
4+
DefinitionProvider,
5+
LocationLink,
6+
Position,
7+
TextDocument,
8+
} from 'vscode';
9+
import { getSuggestions, SuggestionProviders } from '@xml-tools/content-assist';
10+
import XmlDocumentParser from 'common/xml/XmlDocumentParser';
11+
12+
export abstract class XmlDefinitionProvider implements DefinitionProvider {
13+
public abstract getFilePatterns(): string[];
14+
public abstract getDefinitionProviders(): SuggestionProviders<LocationLink>;
15+
16+
public async provideDefinition(
17+
document: TextDocument,
18+
position: Position,
19+
token: CancellationToken
20+
): Promise<LocationLink[]> {
21+
if (!this.canProvideDefinition(document)) {
22+
return [];
23+
}
24+
25+
const { cst, tokenVector, ast } = await XmlDocumentParser.parse(document);
26+
27+
const definitions = getSuggestions({
28+
ast,
29+
cst,
30+
tokenVector,
31+
offset: document.offsetAt(position),
32+
providers: this.getDefinitionProviders(),
33+
});
34+
35+
return definitions.filter(definition => definition.targetUri.fsPath !== document.uri.fsPath);
36+
}
37+
38+
private canProvideDefinition(document: TextDocument): boolean {
39+
return this.getFilePatterns().some(pattern =>
40+
minimatch(document.uri.fsPath, pattern, { matchBase: true })
41+
);
42+
}
43+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { AttributeValueCompletionOptions, SuggestionProviders } from '@xml-tools/content-assist';
2+
import { XmlDefinitionProvider } from './XmlDefinitionProvider';
3+
import { LocationLink, Uri, Range } from 'vscode';
4+
import ModuleIndexer from 'indexer/module/ModuleIndexer';
5+
import IndexManager from 'indexer/IndexManager';
6+
7+
export class XmlModuleDefinitionProvider extends XmlDefinitionProvider {
8+
public getFilePatterns(): string[] {
9+
return ['**/etc/module.xml', '**/etc/**/routes.xml'];
10+
}
11+
12+
public getDefinitionProviders(): SuggestionProviders<LocationLink> {
13+
return {
14+
attributeValue: [this.getModuleDefinitions],
15+
};
16+
}
17+
18+
private getModuleDefinitions({
19+
element,
20+
attribute,
21+
}: AttributeValueCompletionOptions<undefined>): LocationLink[] {
22+
if (element.name !== 'module' || attribute.key !== 'name') {
23+
return [];
24+
}
25+
26+
const moduleName = attribute.value;
27+
28+
if (!moduleName) {
29+
return [];
30+
}
31+
32+
const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY);
33+
34+
if (!moduleIndexData) {
35+
return [];
36+
}
37+
38+
const module = moduleIndexData.getModule(moduleName);
39+
40+
if (!module) {
41+
return [];
42+
}
43+
44+
const moduleXmlUri = Uri.file(module.moduleXmlPath);
45+
46+
return [
47+
{
48+
targetUri: moduleXmlUri,
49+
targetRange: new Range(0, 0, 0, 0),
50+
originSelectionRange: new Range(
51+
attribute.position.startLine - 1,
52+
attribute.position.startColumn + 5,
53+
attribute.position.endLine - 1,
54+
attribute.position.endColumn - 1
55+
),
56+
},
57+
];
58+
}
59+
}

src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Magento from 'util/Magento';
1313
import { WorkspaceFolder } from 'vscode';
1414
import Logger from 'util/Logger';
1515
import { Command } from 'command/Command';
16+
import { XmlModuleDefinitionProvider } from 'definition/XmlModuleDefinitionProvider';
1617

1718
// This method is called when your extension is activated
1819
// Your extension is activated the very first time the command is executed
@@ -90,7 +91,8 @@ export async function activate(context: vscode.ExtensionContext) {
9091

9192
// definition providers
9293
context.subscriptions.push(
93-
vscode.languages.registerDefinitionProvider('xml', new XmlClasslikeDefinitionProvider())
94+
vscode.languages.registerDefinitionProvider('xml', new XmlClasslikeDefinitionProvider()),
95+
vscode.languages.registerDefinitionProvider('xml', new XmlModuleDefinitionProvider())
9496
);
9597

9698
// codelens providers

src/indexer/module/ModuleIndexer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default class ModuleIndexer extends Indexer<Module> {
4747
name: moduleName,
4848
version: setupVersion,
4949
sequence: sequence.map((module: any) => module['@_name']),
50+
moduleXmlPath: uri.fsPath,
5051
path: Uri.joinPath(uri, '..', '..').fsPath,
5152
location: uri.fsPath.includes('vendor') ? 'vendor' : 'app',
5253
};

src/indexer/module/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export interface Module {
33
version?: string;
44
sequence: string[];
55
path: string;
6+
moduleXmlPath: string;
67
location: 'vendor' | 'app';
78
}

0 commit comments

Comments
 (0)