Skip to content

Commit db6d096

Browse files
committed
feat: xml class and module completion
1 parent 9d638fd commit db6d096

16 files changed

+411
-17
lines changed

package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,20 @@
1212
"type": "git",
1313
"url": "https://github.com/magebitcom/magento-toolbox.git"
1414
},
15+
"homepage": "https://github.com/magebitcom/magento-toolbox",
16+
"bugs": {
17+
"url": "https://github.com/magebitcom/magento-toolbox/issues"
18+
},
1519
"categories": [
1620
"Other"
1721
],
22+
"keywords": [
23+
"magento",
24+
"adobe commerce",
25+
"code completion",
26+
"code generation",
27+
"intellisense"
28+
],
1829
"activationEvents": [
1930
"workspaceContains:**/app/etc/env.php",
2031
"workspaceContains:**/app/etc/di.xml",
@@ -41,6 +52,21 @@
4152
"editPresentation": "multilineText",
4253
"default": "",
4354
"markdownDescription": "`%module%` will be replaced with the module name. \n\n **Do not add comment symbols like `<!--` or `-->`, they will be added automatically.**"
55+
},
56+
"magento-toolbox.provideXmlCompletions": {
57+
"type": "boolean",
58+
"default": true,
59+
"description": "Enable autocomplete for Magento 2 XML files."
60+
},
61+
"magento-toolbox.provideXmlDefinitions": {
62+
"type": "boolean",
63+
"default": true,
64+
"description": "Enable definitions for Magento 2 XML files."
65+
},
66+
"magento-toolbox.provideXmlHovers": {
67+
"type": "boolean",
68+
"default": true,
69+
"description": "Enable hover decorations for Magento 2 XML files."
4470
}
4571
}
4672
},

src/common/Config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { workspace } from 'vscode';
2+
3+
class Config {
4+
public readonly SECTION = 'magento-toolbox';
5+
6+
public get<T = string>(key: string): T | undefined {
7+
return workspace.getConfiguration(this.SECTION).get<T>(key);
8+
}
9+
}
10+
11+
export default new Config();

src/common/PhpNamespace.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,22 @@ export default class PhpNamespace {
1717
return new PhpNamespace(parts);
1818
}
1919

20+
public pop(): string {
21+
return this.parts.pop() as string;
22+
}
23+
2024
public getParts(): string[] {
2125
return this.parts;
2226
}
2327

28+
public getHead(): string {
29+
return this.parts[0];
30+
}
31+
32+
public getTail(): string {
33+
return this.parts[this.parts.length - 1];
34+
}
35+
2436
public toString(): string {
2537
return this.parts.join(PhpNamespace.NS_SEPARATOR);
2638
}

src/common/php/FileHeader.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { workspace } from 'vscode';
1+
import Config from 'common/Config';
22

33
export default class FileHeader {
44
public static getHeader(module: string): string | undefined {
5-
const header = workspace
6-
.getConfiguration('magento-toolbox')
7-
.get<string>('phpFileHeaderComment');
5+
const header = Config.get<string>('phpFileHeaderComment');
86

97
if (!header) {
108
return undefined;

src/common/xml/FileHeader.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { workspace } from 'vscode';
1+
import Config from 'common/Config';
22

33
export default class FileHeader {
44
public static getHeader(module: string): string | undefined {
5-
const header = workspace
6-
.getConfiguration('magento-toolbox')
7-
.get<string>('xmlFileHeaderComment');
5+
const header = Config.get<string>('xmlFileHeaderComment');
86

97
if (!header) {
108
return undefined;

src/common/xml/XmlDocumentParser.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@ class XmlDocumentParser {
1818
this.parser = new PhpParser();
1919
}
2020

21-
public async parse(document: TextDocument): Promise<TokenData> {
21+
public async parse(document: TextDocument, skipCache = false): Promise<TokenData> {
2222
const cacheKey = `xml-file`;
2323

24-
if (DocumentCache.has(document, cacheKey)) {
24+
if (!skipCache && DocumentCache.has(document, cacheKey)) {
2525
return DocumentCache.get(document, cacheKey);
2626
}
2727

2828
const { cst, tokenVector } = parse(document.getText());
2929
const ast = buildAst(cst as DocumentCstNode, tokenVector);
3030
const tokenData: TokenData = { cst: cst as DocumentCstNode, tokenVector, ast };
31-
DocumentCache.set(document, cacheKey, tokenData);
31+
32+
if (!skipCache) {
33+
DocumentCache.set(document, cacheKey, tokenData);
34+
}
35+
3236
return tokenData;
3337
}
3438
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { minimatch } from 'minimatch';
2+
import {
3+
CancellationToken,
4+
CompletionItem,
5+
CompletionItemProvider,
6+
CompletionList,
7+
Position,
8+
TextDocument,
9+
Range,
10+
} from 'vscode';
11+
import { getSuggestions, SuggestionProviders } from '@xml-tools/content-assist';
12+
import XmlDocumentParser, { TokenData } from 'common/xml/XmlDocumentParser';
13+
import Config from 'common/Config';
14+
import { ModuleCompletionItemProvider } from './xml/ModuleCompletionItemProvider';
15+
import { NamespaceCompletionItemProvider } from './xml/NamespaceCompletionItemProvider';
16+
import { XmlCompletionItemProvider } from './xml/XmlCompletionItemProvider';
17+
18+
export class XmlCompletionProvider implements CompletionItemProvider {
19+
private readonly providers: XmlCompletionItemProvider[];
20+
21+
public constructor() {
22+
this.providers = [new ModuleCompletionItemProvider(), new NamespaceCompletionItemProvider()];
23+
}
24+
25+
public async provideCompletionItems(
26+
document: TextDocument,
27+
position: Position,
28+
token: CancellationToken
29+
): Promise<CompletionItem[]> {
30+
if (!this.providers.some(provider => provider.canProvideCompletion(document))) {
31+
return [];
32+
}
33+
34+
const tokenData = await XmlDocumentParser.parse(document, true);
35+
36+
const providerCompletionItems = await Promise.all(
37+
this.providers.map(provider =>
38+
this.getProviderCompletionItems(provider, document, position, tokenData)
39+
)
40+
);
41+
42+
return providerCompletionItems.flat();
43+
}
44+
45+
private async getProviderCompletionItems(
46+
provider: XmlCompletionItemProvider,
47+
document: TextDocument,
48+
position: Position,
49+
tokenData: TokenData
50+
): Promise<CompletionItem[]> {
51+
if (!provider.canProvideCompletion(document)) {
52+
return [];
53+
}
54+
55+
return provider.getCompletions(document, position, tokenData);
56+
}
57+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { CompletionItem, CompletionItemKind } from 'vscode';
2+
import { SuggestionProviders } from '@xml-tools/content-assist';
3+
import IndexManager from 'indexer/IndexManager';
4+
import ModuleIndexer from 'indexer/module/ModuleIndexer';
5+
import { XMLElement, XMLAttribute } from '@xml-tools/ast';
6+
import { XmlCompletionItemProvider } from './XmlCompletionItemProvider';
7+
8+
export class ModuleCompletionItemProvider extends XmlCompletionItemProvider {
9+
getFilePatterns(): string[] {
10+
return ['**/etc/module.xml'];
11+
}
12+
13+
getCompletionProviders(): SuggestionProviders<CompletionItem> {
14+
return {
15+
attributeValue: [this.getAttributeValueCompletions.bind(this)],
16+
};
17+
}
18+
19+
private getAttributeValueCompletions({
20+
element,
21+
attribute,
22+
}: {
23+
element: XMLElement;
24+
attribute: XMLAttribute;
25+
}): CompletionItem[] {
26+
if (
27+
element.name !== 'module' ||
28+
(element.parent as XMLElement)?.name !== 'sequence' ||
29+
attribute.key !== 'name'
30+
) {
31+
return [];
32+
}
33+
34+
const value = attribute?.value || '';
35+
return this.getCompletionItems(value);
36+
}
37+
38+
private getCompletionItems(prefix: string): CompletionItem[] {
39+
const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY);
40+
41+
if (!moduleIndexData) {
42+
return [];
43+
}
44+
45+
const completions = moduleIndexData.getModulesByPrefix(prefix);
46+
47+
return completions.map(module => {
48+
return new CompletionItem(module.name, CompletionItemKind.Value);
49+
});
50+
}
51+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { CompletionItem, CompletionItemKind } from 'vscode';
2+
import { SuggestionProviders } from '@xml-tools/content-assist';
3+
import IndexManager from 'indexer/IndexManager';
4+
import AutoloadNamespaceIndexer from 'indexer/autoload-namespace/AutoloadNamespaceIndexer';
5+
import { XMLElement, XMLAttribute } from '@xml-tools/ast';
6+
import { XmlCompletionItemProvider } from './XmlCompletionItemProvider';
7+
8+
interface AttributeValueMatch {
9+
element?: string;
10+
attributeName: string;
11+
}
12+
13+
interface ElementContentMatch {
14+
element?: string;
15+
attributeName: string;
16+
attributeValue: string;
17+
}
18+
19+
export class NamespaceCompletionItemProvider extends XmlCompletionItemProvider {
20+
private static readonly ATTRIBUTE_VALUE_MATCHERS: AttributeValueMatch[] = [
21+
{
22+
element: 'preference',
23+
attributeName: 'for',
24+
},
25+
{
26+
element: 'preference',
27+
attributeName: 'type',
28+
},
29+
{
30+
element: 'type',
31+
attributeName: 'name',
32+
},
33+
{
34+
element: 'plugin',
35+
attributeName: 'type',
36+
},
37+
{
38+
element: 'virtualType',
39+
attributeName: 'type',
40+
},
41+
{
42+
attributeName: 'instance',
43+
},
44+
{
45+
attributeName: 'class',
46+
},
47+
{
48+
element: 'attribute',
49+
attributeName: 'type',
50+
},
51+
{
52+
element: 'extension_attributes',
53+
attributeName: 'for',
54+
},
55+
];
56+
57+
private static readonly ELEMENT_CONTENT_MATCHERS: ElementContentMatch[] = [
58+
{
59+
attributeName: 'xsi:type',
60+
attributeValue: 'object',
61+
},
62+
];
63+
64+
getFilePatterns(): string[] {
65+
return ['**/etc/**/*.xml'];
66+
}
67+
68+
getCompletionProviders(): SuggestionProviders<CompletionItem> {
69+
return {
70+
attributeValue: [this.getAttributeValueCompletions.bind(this)],
71+
elementContent: [this.getElementContentCompletions.bind(this)],
72+
};
73+
}
74+
75+
private getAttributeValueCompletions({
76+
element,
77+
attribute,
78+
}: {
79+
element: XMLElement;
80+
attribute: XMLAttribute;
81+
}): CompletionItem[] {
82+
const match = NamespaceCompletionItemProvider.ATTRIBUTE_VALUE_MATCHERS.find(matchElement => {
83+
if (matchElement.element && matchElement.element !== element.name) {
84+
return false;
85+
}
86+
87+
return matchElement.attributeName === attribute.key;
88+
});
89+
90+
if (!match) {
91+
return [];
92+
}
93+
94+
const value = attribute?.value || '';
95+
return this.getCompletionItems(value);
96+
}
97+
98+
private getElementContentCompletions({ element }: { element: XMLElement }): CompletionItem[] {
99+
const match = NamespaceCompletionItemProvider.ELEMENT_CONTENT_MATCHERS.find(matchElement => {
100+
if (matchElement.element && matchElement.element !== element.name) {
101+
return false;
102+
}
103+
104+
return element.attributes.some(
105+
attribute =>
106+
attribute.key === matchElement.attributeName &&
107+
attribute.value === matchElement.attributeValue
108+
);
109+
});
110+
111+
if (!match) {
112+
return [];
113+
}
114+
115+
const elementContent =
116+
element.textContents.length > 0 ? (element.textContents[0].text ?? '') : '';
117+
118+
return this.getCompletionItems(elementContent);
119+
}
120+
121+
private getCompletionItems(prefix: string): CompletionItem[] {
122+
const namespaceIndexData = IndexManager.getIndexData(AutoloadNamespaceIndexer.KEY);
123+
124+
if (!namespaceIndexData) {
125+
return [];
126+
}
127+
128+
const completions = namespaceIndexData.findNamespacesByPrefix(prefix);
129+
130+
return completions.map(namespace => {
131+
return new CompletionItem(namespace.fqn, CompletionItemKind.Value);
132+
});
133+
}
134+
}

0 commit comments

Comments
 (0)