Skip to content

Commit 35e807e

Browse files
committed
feat: observer command and code lens
1 parent 0082b69 commit 35e807e

18 files changed

+430
-17
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@
5757
{
5858
"command": "magento-toolbox.generateXmlCatalog",
5959
"title": "Magento Toolbox: Generate XML URN Catalog"
60+
},
61+
{
62+
"command": "magento-toolbox.generateObserver",
63+
"title": "Magento Toolbox: Generate Observer"
6064
}
6165
],
6266
"menus": {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import ObserverClassInfo from 'common/php/ObserverClassInfo';
2+
import PhpDocumentParser from 'common/php/PhpDocumentParser';
3+
import EventsIndexer from 'indexer/events/EventsIndexer';
4+
import IndexManager from 'indexer/IndexManager';
5+
import { NodeKind } from 'parser/php/Parser';
6+
import { Call } from 'php-parser';
7+
import Position from 'util/Position';
8+
import { CodeLens, CodeLensProvider, TextDocument } from 'vscode';
9+
10+
export default class ObserverCodelensProvider implements CodeLensProvider {
11+
public async provideCodeLenses(document: TextDocument): Promise<CodeLens[]> {
12+
const codelenses: CodeLens[] = [];
13+
14+
const phpFile = await PhpDocumentParser.parse(document);
15+
16+
const observerClassInfo = new ObserverClassInfo(phpFile);
17+
18+
const eventDispatchCalls = observerClassInfo.getEventDispatchCalls();
19+
20+
if (eventDispatchCalls.length === 0) {
21+
return codelenses;
22+
}
23+
24+
codelenses.push(...this.getEventDispatchCodeLenses(eventDispatchCalls));
25+
26+
return codelenses;
27+
}
28+
29+
private getEventDispatchCodeLenses(eventDispatchCalls: Call[]): CodeLens[] {
30+
const eventsIndexData = IndexManager.getIndexData(EventsIndexer.KEY);
31+
32+
if (!eventsIndexData) {
33+
return [];
34+
}
35+
36+
const codelenses: CodeLens[] = [];
37+
38+
for (const eventDispatchCall of eventDispatchCalls) {
39+
const args = eventDispatchCall.arguments;
40+
41+
if (args.length === 0) {
42+
continue;
43+
}
44+
45+
const firstArg = args[0];
46+
47+
if (!firstArg || firstArg.kind !== NodeKind.String || !firstArg.loc) {
48+
continue;
49+
}
50+
51+
const eventName = (firstArg as any).value;
52+
53+
const event = eventsIndexData.getEventByName(eventName);
54+
55+
if (!event) {
56+
continue;
57+
}
58+
59+
const range = Position.phpAstLocationToVsCodeRange(firstArg.loc);
60+
61+
const codelens = new CodeLens(range, {
62+
title: 'Create an Observer',
63+
command: 'magento-toolbox.generateObserver',
64+
arguments: [event.name],
65+
});
66+
67+
codelenses.push(codelens);
68+
}
69+
70+
return codelenses;
71+
}
72+
}

src/command/GenerateModuleCommand.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import ModuleXmlGenerator from 'generator/module/ModuleXmlGenerator';
33
import ModuleRegistrationGenerator from 'generator/module/ModuleRegistrationGenerator';
44
import ModuleComposerGenerator from 'generator/module/ModuleComposerGenerator';
55
import ModuleLicenseGenerator from 'generator/module/ModuleLicenseGenerator';
6-
import ModuleWizard from 'wizard/ModuleWizard';
6+
import ModuleWizard, { ModuleWizardData, ModuleWizardComposerData } from 'wizard/ModuleWizard';
77
import FileGeneratorManager from 'generator/FileGeneratorManager';
88
import { window } from 'vscode';
99
import Common from 'util/Common';
10+
import WizzardClosedError from 'webview/error/WizzardClosedError';
1011

1112
export default class GenerateModuleCommand extends Command {
1213
constructor() {
@@ -16,7 +17,17 @@ export default class GenerateModuleCommand extends Command {
1617
public async execute(...args: any[]): Promise<void> {
1718
const moduleWizard = new ModuleWizard();
1819

19-
const data = await moduleWizard.show();
20+
let data: ModuleWizardData | ModuleWizardComposerData;
21+
22+
try {
23+
data = await moduleWizard.show();
24+
} catch (error) {
25+
if (error instanceof WizzardClosedError) {
26+
return;
27+
}
28+
29+
throw error;
30+
}
2031

2132
const manager = new FileGeneratorManager([
2233
new ModuleXmlGenerator(data),
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Command } from 'command/Command';
2+
import ObserverWizard, { ObserverWizardData } from 'wizard/ObserverWizard';
3+
import WizzardClosedError from 'webview/error/WizzardClosedError';
4+
import FileGeneratorManager from 'generator/FileGeneratorManager';
5+
import Common from 'util/Common';
6+
import { window } from 'vscode';
7+
import ObserverClassGenerator from 'generator/observer/ObserverClassGenerator';
8+
import ObserverEventsGenerator from 'generator/observer/ObserverEventsGenerator';
9+
10+
export default class GenerateObserverCommand extends Command {
11+
constructor() {
12+
super('magento-toolbox.generateObserver');
13+
}
14+
15+
public async execute(eventName?: string): Promise<void> {
16+
const observerWizard = new ObserverWizard();
17+
18+
let data: ObserverWizardData;
19+
20+
try {
21+
data = await observerWizard.show(eventName);
22+
} catch (error) {
23+
if (error instanceof WizzardClosedError) {
24+
return;
25+
}
26+
27+
throw error;
28+
}
29+
30+
const manager = new FileGeneratorManager([
31+
new ObserverClassGenerator(data),
32+
new ObserverEventsGenerator(data),
33+
]);
34+
35+
const workspaceFolder = Common.getActiveWorkspaceFolder();
36+
37+
if (!workspaceFolder) {
38+
window.showErrorMessage('No active workspace folder');
39+
return;
40+
}
41+
42+
await manager.generate(workspaceFolder.uri);
43+
await manager.writeFiles();
44+
await manager.refreshIndex(workspaceFolder);
45+
manager.openFirstFile();
46+
}
47+
}

src/common/Validation.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export interface ValidationResult {
2+
isValid: boolean;
3+
errors?: string[];
4+
}
5+
6+
export default class Validation {
7+
public static readonly MODULE_NAME_REGEX = /^[A-Z][a-z0-9]*_[A-Z][a-z0-9]*$/;
8+
public static readonly CLASS_NAME_REGEX = /^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/;
9+
public static readonly SNAKE_CASE_REGEX = /^[a-z0-9_]+$/;
10+
11+
public static isValidModuleName(name: string): ValidationResult {
12+
if (!Validation.MODULE_NAME_REGEX.test(name)) {
13+
return {
14+
isValid: false,
15+
errors: ['Module name must be in format "Vendor_Module"'],
16+
};
17+
}
18+
19+
return { isValid: true };
20+
}
21+
22+
public static isValidClassName(name: string): ValidationResult {
23+
if (!Validation.CLASS_NAME_REGEX.test(name)) {
24+
return { isValid: false, errors: ['Class name must be in format "ClassName"'] };
25+
}
26+
27+
return { isValid: true };
28+
}
29+
30+
public static isSnakeCase(name: string): ValidationResult {
31+
if (!Validation.SNAKE_CASE_REGEX.test(name)) {
32+
return { isValid: false, errors: ['Name must be in snake_case format'] };
33+
}
34+
35+
return { isValid: true };
36+
}
37+
}

src/extension.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import CopyMagentoPathCommand from 'command/CopyMagentoPathCommand';
1515
import Common from 'util/Common';
1616
import GenerateXmlCatalogCommand from 'command/GenerateXmlCatalogCommand';
1717
import XmlClasslikeHoverProvider from 'hover/XmlClasslikeHoverProvider';
18+
import ObserverCodelensProvider from 'codelens/ObserverCodelensProvider';
19+
import GenerateObserverCommand from 'command/GenerateObserverCommand';
1820

1921
// This method is called when your extension is activated
2022
// Your extension is activated the very first time the command is executed
@@ -27,6 +29,7 @@ export async function activate(context: vscode.ExtensionContext) {
2729
GenerateContextPluginCommand,
2830
CopyMagentoPathCommand,
2931
GenerateXmlCatalogCommand,
32+
GenerateObserverCommand,
3033
];
3134

3235
ExtensionState.init(context);
@@ -86,6 +89,11 @@ export async function activate(context: vscode.ExtensionContext) {
8689
vscode.languages.registerDefinitionProvider('xml', new XmlClasslikeDefinitionProvider())
8790
);
8891

92+
// codelens providers
93+
context.subscriptions.push(
94+
vscode.languages.registerCodeLensProvider('php', new ObserverCodelensProvider())
95+
);
96+
8997
// hover providers
9098
context.subscriptions.push(
9199
vscode.languages.registerHoverProvider('xml', new XmlClasslikeHoverProvider())
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import PhpNamespace from 'common/PhpNamespace';
2+
import GeneratedFile from 'generator/GeneratedFile';
3+
import ModuleFileGenerator from 'generator/ModuleFileGenerator';
4+
import { PhpFile, PsrPrinter } from 'node-php-generator';
5+
import { Uri } from 'vscode';
6+
import { ObserverWizardData } from 'wizard/ObserverWizard';
7+
8+
export default class ObserverClassGenerator extends ModuleFileGenerator {
9+
private static readonly OBSERVER_INTERFACE = 'Magento\\Framework\\Event\\ObserverInterface';
10+
private static readonly OBSERVER_CLASS = 'Magento\\Framework\\Event\\Observer';
11+
12+
public constructor(protected data: ObserverWizardData) {
13+
super();
14+
}
15+
16+
public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
17+
const [vendor, module] = this.data.module.split('_');
18+
const namespaceParts = [vendor, module, this.data.directoryPath];
19+
const moduleDirectory = this.getModuleDirectory(vendor, module, workspaceUri);
20+
21+
const phpFile = new PhpFile();
22+
phpFile.setStrictTypes(true);
23+
24+
const namespace = phpFile.addNamespace(PhpNamespace.fromParts(namespaceParts).toString());
25+
namespace.addUse(ObserverClassGenerator.OBSERVER_INTERFACE);
26+
namespace.addUse(ObserverClassGenerator.OBSERVER_CLASS);
27+
28+
const observerClass = namespace.addClass(this.data.className);
29+
observerClass.addImplement(ObserverClassGenerator.OBSERVER_INTERFACE);
30+
31+
const observerMethod = observerClass.addMethod('execute');
32+
observerMethod.addParameter('observer').setType(ObserverClassGenerator.OBSERVER_CLASS);
33+
observerMethod.addComment(`Observer for "${this.data.eventName}"\n`);
34+
observerMethod.addComment('@param Observer $observer');
35+
observerMethod.addComment('@return void');
36+
observerMethod.setBody(`$event = $observer->getEvent();\n// TODO: Observer code`);
37+
38+
const printer = new PsrPrinter();
39+
40+
return new GeneratedFile(
41+
Uri.joinPath(moduleDirectory, this.data.directoryPath, `${this.data.className}.php`),
42+
printer.printFile(phpFile)
43+
);
44+
}
45+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import GeneratedFile from 'generator/GeneratedFile';
2+
import ModuleFileGenerator from 'generator/ModuleFileGenerator';
3+
import GenerateFromTemplate from 'generator/util/GenerateFromTemplate';
4+
import { Uri } from 'vscode';
5+
import { ObserverWizardData } from 'wizard/ObserverWizard';
6+
import indentString from 'indent-string';
7+
import PhpNamespace from 'common/PhpNamespace';
8+
import FindOrCreateEventsXml from 'generator/util/FindOrCreateEventsXml';
9+
import { MagentoScope } from 'types';
10+
11+
export default class ObserverDiGenerator extends ModuleFileGenerator {
12+
public constructor(protected data: ObserverWizardData) {
13+
super();
14+
}
15+
16+
public async generate(workspaceUri: Uri): Promise<GeneratedFile> {
17+
const [vendor, module] = this.data.module.split('_');
18+
const moduleDirectory = this.getModuleDirectory(vendor, module, workspaceUri);
19+
const observerNamespace = PhpNamespace.fromParts([vendor, module, this.data.directoryPath]);
20+
const areaPath = this.data.area === MagentoScope.Global ? '' : this.data.area;
21+
22+
const eventsFile = Uri.joinPath(moduleDirectory, 'etc', areaPath, 'events.xml');
23+
const eventsXml = await FindOrCreateEventsXml.execute(
24+
workspaceUri,
25+
vendor,
26+
module,
27+
this.data.area
28+
);
29+
const insertPosition = this.getInsertPosition(eventsXml);
30+
31+
const observerXml = await GenerateFromTemplate.generate('xml/observer', {
32+
name: this.data.observerName,
33+
className: observerNamespace.append(this.data.className).toString(),
34+
eventName: this.data.eventName,
35+
});
36+
37+
const newEventsXml =
38+
eventsXml.slice(0, insertPosition) +
39+
'\n' +
40+
indentString(observerXml, 4) +
41+
'\n' +
42+
eventsXml.slice(insertPosition);
43+
44+
return new GeneratedFile(eventsFile, newEventsXml, false);
45+
}
46+
47+
private getInsertPosition(diXml: string): number {
48+
return diXml.indexOf('</config>');
49+
}
50+
}

src/generator/util/FindOrCreateDiXml.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Uri } from 'vscode';
22
import FileSystem from 'util/FileSystem';
33
import GenerateFromTemplate from './GenerateFromTemplate';
44

5-
class FindOrCreateDiXml {
6-
public async execute(workspaceUri: Uri, vendor: string, module: string): Promise<string> {
5+
export default class FindOrCreateDiXml {
6+
public static async execute(workspaceUri: Uri, vendor: string, module: string): Promise<string> {
77
const diFile = Uri.joinPath(workspaceUri, 'app', 'code', vendor, module, 'etc', 'di.xml');
88

99
if (await FileSystem.fileExists(diFile)) {
@@ -13,5 +13,3 @@ class FindOrCreateDiXml {
1313
return await GenerateFromTemplate.generate('xml/blank-di');
1414
}
1515
}
16-
17-
export default new FindOrCreateDiXml();
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Uri } from 'vscode';
2+
import FileSystem from 'util/FileSystem';
3+
import GenerateFromTemplate from './GenerateFromTemplate';
4+
import { MagentoScope } from 'types';
5+
6+
export default class FindOrCreateEventsXml {
7+
public static async execute(
8+
workspaceUri: Uri,
9+
vendor: string,
10+
module: string,
11+
area: MagentoScope
12+
): Promise<string> {
13+
const areaPath = area === MagentoScope.Global ? '' : area;
14+
const eventsFile = Uri.joinPath(
15+
workspaceUri,
16+
'app',
17+
'code',
18+
vendor,
19+
module,
20+
'etc',
21+
areaPath,
22+
'events.xml'
23+
);
24+
25+
if (await FileSystem.fileExists(eventsFile)) {
26+
return await FileSystem.readFile(eventsFile);
27+
}
28+
29+
return await GenerateFromTemplate.generate('xml/blank-events');
30+
}
31+
}

0 commit comments

Comments
 (0)