├── .cursor └── rules │ ├── commands.mdc │ └── tests.mdc ├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── .vscode-test.mjs ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── esbuild.js ├── esbuild.test.js ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── resources ├── icons │ ├── cron.svg │ ├── observer.svg │ └── plugin.svg └── logo.jpg ├── src ├── cache │ ├── Cache.ts │ └── DocumentCache.ts ├── codelens │ └── ObserverCodelensProvider.ts ├── command │ ├── Command.ts │ ├── CopyMagentoPathCommand.ts │ ├── GenerateAclXmlFileCommand.ts │ ├── GenerateBlockCommand.ts │ ├── GenerateConfigXmlFileCommand.ts │ ├── GenerateContextPluginCommand.ts │ ├── GenerateCronJobCommand.ts │ ├── GenerateCrontabXmlCommand.ts │ ├── GenerateDataPatchCommand.ts │ ├── GenerateDiXmlFileCommand.ts │ ├── GenerateEmailTemplatesXmlCommand.ts │ ├── GenerateEventsXmlCommand.ts │ ├── GenerateExtensionAttributesXmlFileCommand.ts │ ├── GenerateFieldsetXmlCommand.ts │ ├── GenerateGraphqlSchemaFile.ts │ ├── GenerateIndexerXmlFileCommand.ts │ ├── GenerateLayoutXmlCommand.ts │ ├── GenerateModuleCommand.ts │ ├── GenerateMviewXmlFileCommand.ts │ ├── GenerateObserverCommand.ts │ ├── GeneratePageTypesXmlCommand.ts │ ├── GeneratePreferenceCommand.ts │ ├── GenerateRoutesXmlFileCommand.ts │ ├── GenerateSectionsXmlCommand.ts │ ├── GenerateSystemXmlFileCommand.ts │ ├── GenerateViewModelCommand.ts │ ├── GenerateViewXmlFile.ts │ ├── GenerateWebapiXmlFileCommand.ts │ ├── GenerateWidgetXmlFileCommand.ts │ ├── GenerateXmlCatalogCommand.ts │ ├── IndexWorkspaceCommand.ts │ ├── JumpToModuleCommand.ts │ ├── SimpleTemplateGeneratorCommand.ts │ └── index.ts ├── common │ ├── Config.ts │ ├── Context.ts │ ├── ExtensionState.ts │ ├── GetMagentoPath.ts │ ├── MagentoCli.ts │ ├── MarkdownMessageBuilder.ts │ ├── PhpNamespace.ts │ ├── Validation.ts │ ├── php │ │ ├── ClasslikeInfo.ts │ │ ├── FileHeader.ts │ │ ├── ObserverClassInfo.ts │ │ ├── PhpDocumentParser.ts │ │ ├── PluginInfo.ts │ │ └── PluginSubjectInfo.ts │ └── xml │ │ ├── FileHeader.ts │ │ ├── XmlDocumentParser.ts │ │ ├── XmlSuggestionProvider.ts │ │ ├── XmlSuggestionProviderProcessor.ts │ │ └── suggestion │ │ └── condition │ │ ├── AttributeNameMatches.ts │ │ ├── ElementAttributeMatches.ts │ │ ├── ElementNameMatches.ts │ │ ├── MatchCondition.ts │ │ └── ParentElementNameMatches.ts ├── completion │ ├── XmlCompletionProviderProcessor.ts │ └── xml │ │ ├── AclCompletionProvider.ts │ │ ├── EventCompletionProvider.ts │ │ ├── ModuleCompletionProvider.ts │ │ ├── NamespaceCompletionProvider.ts │ │ └── TemplateCompletionProvider.ts ├── decorator │ ├── CronClassDecorationProvider.ts │ ├── ObserverInstanceDecorationProvider.ts │ ├── PluginClassDecorationProvider.ts │ └── TextDocumentDecorationProvider.ts ├── definition │ ├── XmlClasslikeDefinitionProvider.ts │ ├── XmlDefinitionProviderProcessor.ts │ └── xml │ │ ├── AclDefinitionProvider.ts │ │ ├── ModuleDefinitionProvider.ts │ │ └── TemplateDefinitionProvider.ts ├── diagnostics │ ├── DiagnosticCollectionProvider.ts │ ├── LanguageDiagnostics.ts │ └── php │ │ └── PluginDiagnostics.ts ├── extension.ts ├── generator │ ├── FileGenerator.ts │ ├── FileGeneratorManager.ts │ ├── GeneratedFile.ts │ ├── HandlebarsTemplateRenderer.ts │ ├── TemplateGenerator.ts │ ├── XmlGenerator.ts │ ├── block │ │ └── BlockClassGenerator.ts │ ├── cronJob │ │ ├── CronJobClassGenerator.ts │ │ └── CronJobXmlGenerator.ts │ ├── dataPatch │ │ └── DataPatchGenerator.ts │ ├── module │ │ ├── ModuleComposerGenerator.ts │ │ ├── ModuleLicenseGenerator.ts │ │ ├── ModuleRegistrationGenerator.ts │ │ └── ModuleXmlGenerator.ts │ ├── observer │ │ ├── ObserverClassGenerator.ts │ │ └── ObserverEventsGenerator.ts │ ├── plugin │ │ ├── PluginClassGenerator.ts │ │ └── PluginDiGenerator.ts │ ├── preference │ │ ├── PreferenceClassGenerator.ts │ │ └── PreferenceDiGenerator.ts │ ├── util │ │ ├── FindOrCreateCrontabXml.ts │ │ ├── FindOrCreateDiXml.ts │ │ └── FindOrCreateEventsXml.ts │ └── viewModel │ │ └── ViewModelClassGenerator.ts ├── hover │ ├── XmlClasslikeHoverProvider.ts │ ├── XmlHoverProviderProcessor.ts │ └── xml │ │ ├── AclHoverProvider.ts │ │ ├── CronHoverProvider.ts │ │ └── ModuleHoverProvider.ts ├── indexer │ ├── AbstractIndexData.ts │ ├── IndexDataSerializer.ts │ ├── IndexManager.ts │ ├── IndexRunner.ts │ ├── IndexStorage.ts │ ├── Indexer.ts │ ├── acl │ │ ├── AclIndexData.ts │ │ ├── AclIndexer.ts │ │ └── types.ts │ ├── autoload-namespace │ │ ├── AutoloadNamespaceIndexData.ts │ │ ├── AutoloadNamespaceIndexer.ts │ │ └── types.ts │ ├── cron │ │ ├── CronIndexData.ts │ │ ├── CronIndexer.ts │ │ └── types.ts │ ├── di │ │ ├── DiIndexData.ts │ │ ├── DiIndexer.ts │ │ └── types.ts │ ├── events │ │ ├── EventsIndexData.ts │ │ ├── EventsIndexer.ts │ │ └── types.ts │ ├── module │ │ ├── ModuleIndexData.ts │ │ ├── ModuleIndexer.ts │ │ └── types.ts │ └── template │ │ ├── TemplateIndexData.ts │ │ ├── TemplateIndexer.ts │ │ └── types.ts ├── observer │ ├── ActiveTextEditorChangeObserver.ts │ ├── ChangeTextEditorSelectionObserver.ts │ └── Observer.ts ├── parser │ └── php │ │ ├── Parser.ts │ │ ├── PhpClass.ts │ │ ├── PhpFile.ts │ │ ├── PhpInterface.ts │ │ ├── PhpMethod.ts │ │ ├── PhpNode.ts │ │ └── PhpUseItem.ts ├── test │ ├── generator │ │ ├── block │ │ │ └── BlockClassGenerator.test.ts │ │ ├── cronJob │ │ │ ├── CronJobClassGenerator.test.ts │ │ │ └── CronJobXmlGenerator.test.ts │ │ ├── dataPatch │ │ │ └── DataPatchGenerator.test.ts │ │ ├── module │ │ │ ├── ModuleComposerGenerator.test.ts │ │ │ ├── ModuleRegistrationGenerator.test.ts │ │ │ └── ModuleXmlGenerator.test.ts │ │ ├── observer │ │ │ ├── ObserverClassGenerator.test.ts │ │ │ └── ObserverEventsGenerator.test.ts │ │ ├── plugin │ │ │ ├── PluginClassGenerator.test.ts │ │ │ └── PluginDiGenerator.test.ts │ │ ├── preference │ │ │ ├── PreferenceClassGenerator.test.ts │ │ │ └── PreferenceDiGenerator.test.ts │ │ └── viewModel │ │ │ └── ViewModelClassGenerator.test.ts │ ├── setup.ts │ └── util.ts ├── types │ ├── global.ts │ ├── handlebars.ts │ ├── indexer.ts │ ├── vscode-elements.d.ts │ └── webview.ts ├── util │ ├── Common.ts │ ├── FileSystem.ts │ ├── Logger.ts │ ├── Magento.ts │ └── Position.ts ├── webview │ ├── GeneratorWizard.ts │ ├── Webview.ts │ ├── WizardFieldBuilder.ts │ ├── WizardFormBuilder.ts │ ├── WizardTabBuilder.ts │ ├── components │ │ ├── App.tsx │ │ ├── Wizard.tsx │ │ ├── Wizard │ │ │ ├── DynamicRowInput.tsx │ │ │ ├── FieldErrorMessage.tsx │ │ │ ├── FieldRenderer.tsx │ │ │ └── Renderer.tsx │ │ └── app.css │ ├── error │ │ └── WizzardClosedError.ts │ └── index.tsx └── wizard │ ├── BlockWizard.ts │ ├── CronJobWizard.ts │ ├── DataPatchWizard.ts │ ├── ModuleWizard.ts │ ├── ObserverWizard.ts │ ├── PluginContextWizard.ts │ ├── PreferenceWizard.ts │ ├── SimpleTemplateWizard.ts │ └── ViewModelWizard.ts ├── templates ├── handlebars │ ├── graphql │ │ └── blank-schema.hbs │ ├── license │ │ ├── apache2.hbs │ │ ├── gplv3.hbs │ │ ├── mit.hbs │ │ └── oslv3.hbs │ ├── php │ │ └── registration.hbs │ └── xml │ │ ├── blank-acl.hbs │ │ ├── blank-config.hbs │ │ ├── blank-crontab.hbs │ │ ├── blank-di.hbs │ │ ├── blank-email-templates.hbs │ │ ├── blank-events.hbs │ │ ├── blank-extension-attributes.hbs │ │ ├── blank-fieldset.hbs │ │ ├── blank-indexer.hbs │ │ ├── blank-layout.hbs │ │ ├── blank-mview.hbs │ │ ├── blank-page-types.hbs │ │ ├── blank-routes.hbs │ │ ├── blank-sections.hbs │ │ ├── blank-system.hbs │ │ ├── blank-view.hbs │ │ ├── blank-webapi.hbs │ │ ├── blank-widget.hbs │ │ ├── cron │ │ ├── group.hbs │ │ └── job.hbs │ │ ├── di │ │ ├── plugin.hbs │ │ ├── preference.hbs │ │ └── type.hbs │ │ └── events │ │ ├── event.hbs │ │ └── observer.hbs └── webview │ └── index.html ├── test-resources ├── reference │ └── generator │ │ ├── block │ │ ├── TestBlock.php │ │ └── TestBlockCustomPath.php │ │ ├── cronJob │ │ ├── TestCronJob.php │ │ ├── crontab-merged.xml │ │ └── crontab.xml │ │ ├── dataPatch │ │ ├── TestDataPatch.php │ │ └── TestDataPatchRevertable.php │ │ ├── module │ │ ├── composer.json │ │ ├── module-with-comment.xml │ │ ├── module-with-sequence.xml │ │ ├── module.xml │ │ ├── registration-with-comment.php │ │ └── registration.php │ │ ├── observer │ │ ├── TestObserver.php │ │ ├── TestObserverCustomPath.php │ │ ├── events-adminhtml.xml │ │ ├── events-with-comment.xml │ │ └── events.xml │ │ ├── plugin │ │ ├── SubjectClass.php │ │ ├── TestPlugin.php │ │ ├── TestPluginCustomPath.php │ │ ├── di-adminhtml.xml │ │ └── di.xml │ │ ├── preference │ │ ├── TestPreference.php │ │ ├── TestPreferenceNoInherit.php │ │ └── di.xml │ │ └── viewModel │ │ ├── TestViewModel.php │ │ └── TestViewModelCustomPath.php └── workspace │ └── app │ ├── code │ └── Foo │ │ └── Bar │ │ ├── etc │ │ └── module.xml │ │ └── registration.php │ └── etc │ ├── config.php │ ├── db_schema.xml │ └── di.xml └── tsconfig.json /.cursor/rules/commands.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Describes how to create and register new commands 3 | globs: src/command/*.ts 4 | alwaysApply: false 5 | --- 6 | - Commands are located in `src/command` directory 7 | - Commands must extend the [Command.ts](mdc:src/command/Command.ts) abstract class or any other class that inherits it 8 | - Command name must be defined in format: `magentoToolbox.commandName` 9 | - New commands must be added to [index.ts](mdc:src/command/index.ts) file so that they are loaded. Commands also need to be added to [package.json](mdc:package.json) under contributes -> commands 10 | - Commands that only generate files based on a pre-defined template can extend the [SimpleTemplateGeneratorCommand.ts](mdc:src/command/SimpleTemplateGeneratorCommand.ts) class 11 | -------------------------------------------------------------------------------- /.cursor/rules/tests.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Describes how to write tests 3 | globs: *.test.ts 4 | alwaysApply: false 5 | --- 6 | - Tests are located in the src/test directory 7 | - Utilities for tests can be found in this file [util.ts](mdc:src/test/util.ts) 8 | - Some parts of the extension require the ExtensionContext to be present. To make sure it's set, tests should run the setup function from [setup.ts](mdc:src/test/setup.ts) 9 | - Tests are written using Mocha framework (`mocha` package) in Typescript and must end with .test.ts file extension. Do NOT use `chai` 10 | - Test spies, stubs and mocks can be written using the Sinon.js library 11 | - Reference files for tests are stored in the test-resources directory 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Run CI jobs 2 | on: [push] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions/setup-node@v3 11 | with: 12 | node-version: '22' 13 | - run: npm install 14 | - run: npm run lint 15 | 16 | test: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: '22' 24 | - run: npm install 25 | - run: xvfb-run -a npm test 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Install 15 | run: npm install 16 | - name: Package 17 | run: npm run package-vsix 18 | - name: Release 19 | uses: softprops/action-gh-release@v2 20 | if: startsWith(github.ref, 'refs/tags/') 21 | with: 22 | files: '*.vsix' 23 | 24 | deploy: 25 | runs-on: ubuntu-latest 26 | needs: build 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Install 31 | run: npm install 32 | - name: Publish to VS Code Marketplace 33 | run: npm run deploy:vsce 34 | env: 35 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 36 | - name: Publish to Open VSX Registry 37 | run: npm run deploy:ovsx 38 | env: 39 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .dependencygraph -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "endOfLine": "lf" 12 | } -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: ['dist/test/setup.js', 'dist/test/**/*.test.js'], 5 | }); 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "watch", 8 | "dependsOn": ["npm: watch:esbuild"], 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "type": "npm", 19 | "script": "watch:esbuild", 20 | "group": "build", 21 | "problemMatcher": "$esbuild-watch", 22 | "isBackground": true, 23 | "label": "npm: watch:esbuild", 24 | "presentation": { 25 | "group": "watch", 26 | "reveal": "never" 27 | } 28 | }, 29 | { 30 | "type": "npm", 31 | "script": "watch-tests", 32 | "problemMatcher": "$tsc-watch", 33 | "isBackground": true, 34 | "presentation": { 35 | "reveal": "never", 36 | "group": "watchers" 37 | }, 38 | "group": "build" 39 | }, 40 | { 41 | "label": "tasks: watch-tests", 42 | "dependsOn": ["npm: watch", "npm: watch-tests"], 43 | "problemMatcher": [] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | esbuild.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/eslint.config.mjs 12 | **/*.map 13 | **/*.ts 14 | **/.vscode-test.* 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Magento Toolbox](https://github.com/user-attachments/assets/9e04952d-b869-470a-9171-dac9794a7185) 2 | 3 | 4 | # Magento Toolbox 5 | 6 | Code generation and inspection tools for Magento 2 development. 7 | 8 | **This extension is currently actively under development** 9 | 10 | ## Features 11 | 12 | Check out our [Wiki page](https://github.com/magebitcom/magento-toolbox/wiki) for information about the available features 13 | 14 | ## Installation 15 | 16 | ### From the extensions tab 17 | Open up the extensions tab and search for `Magento Toolbox` 18 | 19 | ### Via command 20 | Open up the VS Code Quick open window (`Ctrl-P`) and run this command: 21 | ``` 22 | ext install magebit.magebit-magento-toolbox 23 | ``` 24 | 25 | ## Contributing 26 | 27 | Found a bug, have a feature suggestion or just want to help in general? Contributions are very welcome! Check out the list of active issues or submit one yourself. 28 | 29 | --- 30 | ![magebit (1)](https://github.com/user-attachments/assets/cdc904ce-e839-40a0-a86f-792f7ab7961f) 31 | 32 | *Have questions or need help? Contact us at info@magebit.com* 33 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | import prettier from 'eslint-plugin-prettier'; 4 | import eslintConfigPrettier from 'eslint-config-prettier'; 5 | 6 | export default [ 7 | { 8 | files: ['src/**/*.ts', 'src/**/*.tsx'], 9 | }, 10 | { 11 | plugins: { 12 | '@typescript-eslint': typescriptEslint, 13 | prettier: prettier, 14 | }, 15 | 16 | languageOptions: { 17 | parser: tsParser, 18 | ecmaVersion: 2022, 19 | sourceType: 'module', 20 | }, 21 | 22 | rules: { 23 | '@typescript-eslint/naming-convention': [ 24 | 'warn', 25 | { 26 | selector: 'import', 27 | format: ['camelCase', 'PascalCase'], 28 | }, 29 | ], 30 | 'prettier/prettier': 'warn', 31 | curly: 'warn', 32 | eqeqeq: 'warn', 33 | 'no-throw-literal': 'warn', 34 | semi: 'warn', 35 | }, 36 | }, 37 | eslintConfigPrettier, 38 | ]; 39 | -------------------------------------------------------------------------------- /resources/icons/cron.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/icons/observer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /resources/icons/plugin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magebitcom/magento-toolbox/434fe91e78eb3d128dc9669a2019ffd50baed463/resources/logo.jpg -------------------------------------------------------------------------------- /src/cache/Cache.ts: -------------------------------------------------------------------------------- 1 | class Cache { 2 | protected cache: Map = new Map(); 3 | 4 | public get(key: string) { 5 | return this.cache.get(key); 6 | } 7 | 8 | public set(key: string, value: any) { 9 | this.cache.set(key, value); 10 | } 11 | 12 | public delete(key: string) { 13 | this.cache.delete(key); 14 | } 15 | 16 | public clear() { 17 | this.cache.clear(); 18 | } 19 | 20 | public has(key: string) { 21 | return this.cache.has(key); 22 | } 23 | } 24 | 25 | export default new Cache(); 26 | -------------------------------------------------------------------------------- /src/cache/DocumentCache.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from 'vscode'; 2 | 3 | class DocumentCache { 4 | protected cache: Map = new Map(); 5 | 6 | public get(document: TextDocument, key: string) { 7 | const cacheKey = this.getCacheKey(document, key); 8 | return this.cache.get(cacheKey); 9 | } 10 | 11 | public set(document: TextDocument, key: string, value: any) { 12 | const cacheKey = this.getCacheKey(document, key); 13 | this.cache.set(cacheKey, value); 14 | } 15 | 16 | public delete(document: TextDocument, key: string) { 17 | const cacheKey = this.getCacheKey(document, key); 18 | this.cache.delete(cacheKey); 19 | } 20 | 21 | public clear(document: TextDocument) { 22 | this.cache.forEach((value, key) => { 23 | if (key.startsWith(document.uri.fsPath)) { 24 | this.cache.delete(key); 25 | } 26 | }); 27 | } 28 | 29 | public has(document: TextDocument, key: string) { 30 | const cacheKey = this.getCacheKey(document, key); 31 | return this.cache.has(cacheKey); 32 | } 33 | 34 | protected getCacheKey(document: TextDocument, key: string) { 35 | return `${document.uri.fsPath}-${key}`; 36 | } 37 | } 38 | 39 | export default new DocumentCache(); 40 | -------------------------------------------------------------------------------- /src/codelens/ObserverCodelensProvider.ts: -------------------------------------------------------------------------------- 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 { 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: [undefined, event.name], 65 | }); 66 | 67 | codelenses.push(codelens); 68 | } 69 | 70 | return codelenses; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/command/Command.ts: -------------------------------------------------------------------------------- 1 | export abstract class Command { 2 | constructor(protected command: string) {} 3 | 4 | public abstract execute(...args: any[]): Promise; 5 | 6 | public getCommand(): string { 7 | return this.command; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/command/CopyMagentoPathCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import GetMagentoPath from 'common/GetMagentoPath'; 3 | import { Uri, window, env } from 'vscode'; 4 | 5 | export default class CopyMagentoPathCommand extends Command { 6 | constructor() { 7 | super('magento-toolbox.copyMagentoPath'); 8 | } 9 | 10 | public async execute(file: Uri): Promise { 11 | const magentoPath = GetMagentoPath.getMagentoPath(file); 12 | 13 | if (!magentoPath) { 14 | return; 15 | } 16 | 17 | await env.clipboard.writeText(magentoPath); 18 | window.showInformationMessage(`Copied: ${magentoPath}`); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/command/GenerateAclXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import { TemplatePath } from 'types/handlebars'; 4 | 5 | export default class GenerateAclXmlFileCommand extends SimpleTemplateGeneratorCommand { 6 | constructor() { 7 | super('magento-toolbox.generateAclXmlFile'); 8 | } 9 | 10 | getWizardTitle(): string { 11 | return 'ACL XML File'; 12 | } 13 | 14 | getFilePath(data: TemplateWizardData): string { 15 | const [vendor, module] = data.module.split('_'); 16 | 17 | return `app/code/${vendor}/${module}/etc/acl.xml`; 18 | } 19 | 20 | getTemplateName(data: TemplateWizardData): TemplatePath { 21 | return TemplatePath.XmlBlankAcl; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/command/GenerateBlockCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import BlockClassGenerator from 'generator/block/BlockClassGenerator'; 3 | import BlockWizard, { BlockWizardData } from 'wizard/BlockWizard'; 4 | import FileGeneratorManager from 'generator/FileGeneratorManager'; 5 | import { Uri, window } from 'vscode'; 6 | import Common from 'util/Common'; 7 | import WizzardClosedError from 'webview/error/WizzardClosedError'; 8 | import IndexManager from 'indexer/IndexManager'; 9 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 10 | 11 | export default class GenerateBlockCommand extends Command { 12 | constructor() { 13 | super('magento-toolbox.generateBlock'); 14 | } 15 | 16 | public async execute(uri?: Uri): Promise { 17 | const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY); 18 | let contextModule: string | undefined; 19 | 20 | const contextUri = uri || window.activeTextEditor?.document.uri; 21 | 22 | if (moduleIndex && contextUri) { 23 | const module = moduleIndex.getModuleByUri(contextUri); 24 | 25 | if (module) { 26 | contextModule = module.name; 27 | } 28 | } 29 | 30 | const blockWizard = new BlockWizard(); 31 | 32 | let data: BlockWizardData; 33 | 34 | try { 35 | data = await blockWizard.show(contextModule); 36 | } catch (error) { 37 | if (error instanceof WizzardClosedError) { 38 | return; 39 | } 40 | 41 | throw error; 42 | } 43 | 44 | const manager = new FileGeneratorManager([new BlockClassGenerator(data)]); 45 | 46 | const workspaceFolder = Common.getActiveWorkspaceFolder(); 47 | 48 | if (!workspaceFolder) { 49 | window.showErrorMessage('No active workspace folder'); 50 | return; 51 | } 52 | 53 | await manager.generate(workspaceFolder.uri); 54 | await manager.writeFiles(); 55 | await manager.refreshIndex(workspaceFolder); 56 | manager.openFirstFile(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/command/GenerateConfigXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateConfigXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateConfigXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Config XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/config.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankConfig; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateCronJobCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import CronJobWizard, { CronJobWizardData } from 'wizard/CronJobWizard'; 3 | import WizzardClosedError from 'webview/error/WizzardClosedError'; 4 | import FileGeneratorManager from 'generator/FileGeneratorManager'; 5 | import Common from 'util/Common'; 6 | import { Uri, window } from 'vscode'; 7 | import CronJobClassGenerator from 'generator/cronJob/CronJobClassGenerator'; 8 | import CronJobXmlGenerator from 'generator/cronJob/CronJobXmlGenerator'; 9 | import IndexManager from 'indexer/IndexManager'; 10 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 11 | 12 | export default class GenerateCronJobCommand extends Command { 13 | constructor() { 14 | super('magento-toolbox.generateCronJob'); 15 | } 16 | 17 | public async execute(uri?: Uri): Promise { 18 | const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY); 19 | let contextModule: string | undefined; 20 | 21 | const contextUri = uri || window.activeTextEditor?.document.uri; 22 | 23 | if (moduleIndex && contextUri) { 24 | const module = moduleIndex.getModuleByUri(contextUri); 25 | 26 | if (module) { 27 | contextModule = module.name; 28 | } 29 | } 30 | 31 | const cronJobWizard = new CronJobWizard(); 32 | 33 | let data: CronJobWizardData; 34 | 35 | try { 36 | data = await cronJobWizard.show(contextModule); 37 | } catch (error) { 38 | if (error instanceof WizzardClosedError) { 39 | return; 40 | } 41 | 42 | throw error; 43 | } 44 | 45 | const manager = new FileGeneratorManager([ 46 | new CronJobClassGenerator(data), 47 | new CronJobXmlGenerator(data), 48 | ]); 49 | 50 | const workspaceFolder = Common.getActiveWorkspaceFolder(); 51 | 52 | if (!workspaceFolder) { 53 | window.showErrorMessage('No active workspace folder'); 54 | return; 55 | } 56 | 57 | await manager.generate(workspaceFolder.uri); 58 | await manager.writeFiles(); 59 | await manager.refreshIndex(workspaceFolder); 60 | manager.openAllFiles(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/command/GenerateCrontabXmlCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateCrontabXmlCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateCrontabXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Crontab XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/crontab.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankCrontab; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateDataPatchCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import DataPatchWizard, { DataPatchWizardData } from 'wizard/DataPatchWizard'; 3 | import WizzardClosedError from 'webview/error/WizzardClosedError'; 4 | import FileGeneratorManager from 'generator/FileGeneratorManager'; 5 | import Common from 'util/Common'; 6 | import { Uri, window } from 'vscode'; 7 | import DataPatchGenerator from 'generator/dataPatch/DataPatchGenerator'; 8 | import IndexManager from 'indexer/IndexManager'; 9 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 10 | 11 | export default class GenerateDataPatchCommand extends Command { 12 | constructor() { 13 | super('magento-toolbox.generateDataPatch'); 14 | } 15 | 16 | public async execute(uri?: Uri): Promise { 17 | const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY); 18 | let contextModule: string | undefined; 19 | 20 | const contextUri = uri || window.activeTextEditor?.document.uri; 21 | 22 | if (moduleIndex && contextUri) { 23 | const module = moduleIndex.getModuleByUri(contextUri); 24 | 25 | if (module) { 26 | contextModule = module.name; 27 | } 28 | } 29 | 30 | const dataPatchWizard = new DataPatchWizard(); 31 | 32 | let data: DataPatchWizardData; 33 | 34 | try { 35 | data = await dataPatchWizard.show(contextModule); 36 | } catch (error) { 37 | if (error instanceof WizzardClosedError) { 38 | return; 39 | } 40 | 41 | throw error; 42 | } 43 | 44 | const manager = new FileGeneratorManager([new DataPatchGenerator(data)]); 45 | 46 | const workspaceFolder = Common.getActiveWorkspaceFolder(); 47 | 48 | if (!workspaceFolder) { 49 | window.showErrorMessage('No active workspace folder'); 50 | return; 51 | } 52 | 53 | await manager.generate(workspaceFolder.uri); 54 | await manager.writeFiles(); 55 | await manager.refreshIndex(workspaceFolder); 56 | manager.openAllFiles(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/command/GenerateDiXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { MagentoScope } from 'types/global'; 2 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 3 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateDiXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateDiXmlFile'); 9 | } 10 | 11 | getAreas(): MagentoScope[] { 12 | return [ 13 | MagentoScope.Global, 14 | MagentoScope.Adminhtml, 15 | MagentoScope.Frontend, 16 | MagentoScope.Cron, 17 | MagentoScope.WebapiRest, 18 | MagentoScope.WebapiSoap, 19 | MagentoScope.Graphql, 20 | ]; 21 | } 22 | 23 | getWizardTitle(): string { 24 | return 'DI XML File'; 25 | } 26 | 27 | getFilePath(data: TemplateWizardData): string { 28 | const [vendor, module] = data.module.split('_'); 29 | 30 | if (data.area && data.area !== MagentoScope.Global) { 31 | return `app/code/${vendor}/${module}/etc/${data.area}/di.xml`; 32 | } 33 | 34 | return `app/code/${vendor}/${module}/etc/di.xml`; 35 | } 36 | 37 | getTemplateName(data: TemplateWizardData): TemplatePath { 38 | return TemplatePath.XmlBlankDi; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/command/GenerateEmailTemplatesXmlCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateEmailTemplatesXmlCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateEmailTemplatesXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Email Templates XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/email_templates.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankEmailTemplates; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateEventsXmlCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import { MagentoScope } from 'types/global'; 4 | import FileHeader from 'common/xml/FileHeader'; 5 | import { TemplatePath } from 'types/handlebars'; 6 | 7 | export default class GenerateEventsXmlCommand extends SimpleTemplateGeneratorCommand { 8 | constructor() { 9 | super('magento-toolbox.generateEventsXml'); 10 | } 11 | 12 | getWizardTitle(): string { 13 | return 'Events XML File'; 14 | } 15 | 16 | getAreas(): MagentoScope[] { 17 | return [ 18 | MagentoScope.Global, 19 | MagentoScope.Adminhtml, 20 | MagentoScope.Frontend, 21 | MagentoScope.Cron, 22 | MagentoScope.WebapiRest, 23 | MagentoScope.WebapiSoap, 24 | MagentoScope.Graphql, 25 | ]; 26 | } 27 | 28 | getFileHeader(data: TemplateWizardData): string | undefined { 29 | return FileHeader.getHeader(data.module); 30 | } 31 | 32 | getFilePath(data: TemplateWizardData): string { 33 | const [vendor, module] = data.module.split('_'); 34 | 35 | if (data.area && data.area !== MagentoScope.Global) { 36 | return `app/code/${vendor}/${module}/etc/${data.area}/events.xml`; 37 | } 38 | 39 | return `app/code/${vendor}/${module}/etc/events.xml`; 40 | } 41 | 42 | getTemplateName(data: TemplateWizardData): TemplatePath { 43 | return TemplatePath.XmlBlankEvents; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/command/GenerateExtensionAttributesXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateExtensionAttributesXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateExtensionAttributesXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Extension Attributes XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/extension_attributes.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankExtensionAttributes; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateFieldsetXmlCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateFieldsetXmlCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateFieldsetXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Fieldset XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/fieldset.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankFieldset; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateGraphqlSchemaFile.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import { TemplatePath } from 'types/handlebars'; 4 | 5 | export default class GenerateGraphqlSchemaFileCommand extends SimpleTemplateGeneratorCommand { 6 | constructor() { 7 | super('magento-toolbox.generateGraphqlSchemaFile'); 8 | } 9 | 10 | getWizardTitle(): string { 11 | return 'GraphQL Schema File'; 12 | } 13 | 14 | getFilePath(data: TemplateWizardData): string { 15 | const [vendor, module] = data.module.split('_'); 16 | 17 | return `app/code/${vendor}/${module}/etc/schema.graphqls`; 18 | } 19 | 20 | getTemplateName(data: TemplateWizardData): TemplatePath { 21 | return TemplatePath.GraphqlBlankSchema; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/command/GenerateIndexerXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateIndexerXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateIndexerXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Indexer XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/indexer.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankIndexer; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateLayoutXmlCommand.ts: -------------------------------------------------------------------------------- 1 | import { MagentoScope } from 'types/global'; 2 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 3 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 4 | import { WizardField, WizardValidationRule } from 'types/webview'; 5 | import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; 6 | import FileHeader from 'common/xml/FileHeader'; 7 | import { TemplatePath } from 'types/handlebars'; 8 | 9 | export default class GenerateLayoutXmlCommand extends SimpleTemplateGeneratorCommand { 10 | constructor() { 11 | super('magento-toolbox.generateLayoutXmlFile'); 12 | } 13 | 14 | getAreas(): MagentoScope[] { 15 | return [ 16 | MagentoScope.Global, 17 | MagentoScope.Adminhtml, 18 | MagentoScope.Frontend, 19 | MagentoScope.Cron, 20 | MagentoScope.WebapiRest, 21 | MagentoScope.WebapiSoap, 22 | MagentoScope.Graphql, 23 | ]; 24 | } 25 | 26 | getFileHeader(data: TemplateWizardData): string | undefined { 27 | return FileHeader.getHeader(data.module); 28 | } 29 | 30 | getWizardTitle(): string { 31 | return 'Layout XML File'; 32 | } 33 | 34 | getFilePath(data: TemplateWizardData): string { 35 | const [vendor, module] = data.module.split('_'); 36 | 37 | if (data.area && data.area !== MagentoScope.Global) { 38 | return `app/code/${vendor}/${module}/view/${data.area}/layout/${data.name}.xml`; 39 | } 40 | 41 | return `app/code/${vendor}/${module}/view/base/layout/${data.name}.xml`; 42 | } 43 | 44 | getTemplateName(data: TemplateWizardData): TemplatePath { 45 | return TemplatePath.XmlBlankLayout; 46 | } 47 | 48 | getWizardFields(): WizardField[] { 49 | return [WizardFieldBuilder.text('name', 'Layout Name').build()]; 50 | } 51 | 52 | getWizardValidation(): Record { 53 | return { 54 | name: 'required', 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/command/GenerateModuleCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import ModuleXmlGenerator from 'generator/module/ModuleXmlGenerator'; 3 | import ModuleRegistrationGenerator from 'generator/module/ModuleRegistrationGenerator'; 4 | import ModuleComposerGenerator from 'generator/module/ModuleComposerGenerator'; 5 | import ModuleLicenseGenerator from 'generator/module/ModuleLicenseGenerator'; 6 | import ModuleWizard, { ModuleWizardData, ModuleWizardComposerData } from 'wizard/ModuleWizard'; 7 | import FileGeneratorManager from 'generator/FileGeneratorManager'; 8 | import { window } from 'vscode'; 9 | import Common from 'util/Common'; 10 | import WizzardClosedError from 'webview/error/WizzardClosedError'; 11 | 12 | export default class GenerateModuleCommand extends Command { 13 | constructor() { 14 | super('magento-toolbox.generateModule'); 15 | } 16 | 17 | public async execute(...args: any[]): Promise { 18 | const moduleWizard = new ModuleWizard(); 19 | 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 | } 31 | 32 | const manager = new FileGeneratorManager([ 33 | new ModuleXmlGenerator(data), 34 | new ModuleRegistrationGenerator(data), 35 | ]); 36 | 37 | if (data.composer) { 38 | manager.addGenerator(new ModuleComposerGenerator(data)); 39 | } 40 | 41 | if (data.license) { 42 | manager.addGenerator(new ModuleLicenseGenerator(data)); 43 | } 44 | 45 | const workspaceFolder = Common.getActiveWorkspaceFolder(); 46 | 47 | if (!workspaceFolder) { 48 | window.showErrorMessage('No active workspace folder'); 49 | return; 50 | } 51 | 52 | await manager.generate(workspaceFolder.uri); 53 | await manager.writeFiles(); 54 | await manager.refreshIndex(workspaceFolder); 55 | manager.openFirstFile(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/command/GenerateMviewXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateMviewXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateMviewXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'MVIEW XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/mview.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankMview; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateObserverCommand.ts: -------------------------------------------------------------------------------- 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 { Uri, window } from 'vscode'; 7 | import ObserverClassGenerator from 'generator/observer/ObserverClassGenerator'; 8 | import ObserverEventsGenerator from 'generator/observer/ObserverEventsGenerator'; 9 | import IndexManager from 'indexer/IndexManager'; 10 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 11 | 12 | export default class GenerateObserverCommand extends Command { 13 | constructor() { 14 | super('magento-toolbox.generateObserver'); 15 | } 16 | 17 | public async execute(uri?: Uri, eventName?: string): Promise { 18 | eventName = typeof eventName === 'string' ? eventName : undefined; 19 | 20 | const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY); 21 | let contextModule: string | undefined; 22 | 23 | const contextUri = uri || window.activeTextEditor?.document.uri; 24 | 25 | if (moduleIndex && contextUri) { 26 | const module = moduleIndex.getModuleByUri(contextUri); 27 | 28 | if (module) { 29 | contextModule = module.name; 30 | } 31 | } 32 | 33 | const observerWizard = new ObserverWizard(); 34 | 35 | let data: ObserverWizardData; 36 | 37 | try { 38 | data = await observerWizard.show(eventName, contextModule); 39 | } catch (error) { 40 | if (error instanceof WizzardClosedError) { 41 | return; 42 | } 43 | 44 | throw error; 45 | } 46 | 47 | const manager = new FileGeneratorManager([ 48 | new ObserverClassGenerator(data), 49 | new ObserverEventsGenerator(data), 50 | ]); 51 | 52 | const workspaceFolder = Common.getActiveWorkspaceFolder(); 53 | 54 | if (!workspaceFolder) { 55 | window.showErrorMessage('No active workspace folder'); 56 | return; 57 | } 58 | 59 | await manager.generate(workspaceFolder.uri); 60 | await manager.writeFiles(); 61 | await manager.refreshIndex(workspaceFolder); 62 | manager.openAllFiles(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/command/GeneratePageTypesXmlCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GeneratePageTypesXmlCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generatePageTypesXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Page Types XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/frontend/page_types.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankPageTypes; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateSectionsXmlCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | export default class GenerateSectionsXmlCommand extends SimpleTemplateGeneratorCommand { 6 | constructor() { 7 | super('magento-toolbox.generateSectionsXmlFile'); 8 | } 9 | 10 | getWizardTitle(): string { 11 | return 'Sections XML File'; 12 | } 13 | 14 | getFileHeader(data: TemplateWizardData): string | undefined { 15 | return FileHeader.getHeader(data.module); 16 | } 17 | 18 | getFilePath(data: TemplateWizardData): string { 19 | const [vendor, module] = data.module.split('_'); 20 | 21 | return `app/code/${vendor}/${module}/etc/frontend/sections.xml`; 22 | } 23 | 24 | getTemplateName(data: TemplateWizardData): TemplatePath { 25 | return TemplatePath.XmlBlankSections; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/command/GenerateSystemXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateSystemXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateSystemXmlFile'); 9 | } 10 | 11 | getFileHeader(data: TemplateWizardData): string | undefined { 12 | return FileHeader.getHeader(data.module); 13 | } 14 | 15 | getWizardTitle(): string { 16 | return 'System XML File'; 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/adminhtml/system.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankSystem; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateViewModelCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import ViewModelClassGenerator from 'generator/viewModel/ViewModelClassGenerator'; 3 | import ViewModelWizard, { ViewModelWizardData } from 'wizard/ViewModelWizard'; 4 | import FileGeneratorManager from 'generator/FileGeneratorManager'; 5 | import { Uri, window } from 'vscode'; 6 | import Common from 'util/Common'; 7 | import WizzardClosedError from 'webview/error/WizzardClosedError'; 8 | import IndexManager from 'indexer/IndexManager'; 9 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 10 | 11 | export default class GenerateViewModelCommand extends Command { 12 | constructor() { 13 | super('magento-toolbox.generateViewModel'); 14 | } 15 | 16 | public async execute(uri?: Uri): Promise { 17 | const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY); 18 | let contextModule: string | undefined; 19 | 20 | const contextUri = uri || window.activeTextEditor?.document.uri; 21 | 22 | if (moduleIndex && contextUri) { 23 | const module = moduleIndex.getModuleByUri(contextUri); 24 | 25 | if (module) { 26 | contextModule = module.name; 27 | } 28 | } 29 | 30 | const viewModelWizard = new ViewModelWizard(); 31 | 32 | let data: ViewModelWizardData; 33 | 34 | try { 35 | data = await viewModelWizard.show(contextModule); 36 | } catch (error) { 37 | if (error instanceof WizzardClosedError) { 38 | return; 39 | } 40 | 41 | throw error; 42 | } 43 | 44 | const manager = new FileGeneratorManager([new ViewModelClassGenerator(data)]); 45 | 46 | const workspaceFolder = Common.getActiveWorkspaceFolder(); 47 | 48 | if (!workspaceFolder) { 49 | window.showErrorMessage('No active workspace folder'); 50 | return; 51 | } 52 | 53 | await manager.generate(workspaceFolder.uri); 54 | await manager.writeFiles(); 55 | await manager.refreshIndex(workspaceFolder); 56 | manager.openFirstFile(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/command/GenerateViewXmlFile.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateViewXmlFile extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateViewXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'View XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/view.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankView; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateWebapiXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateWebapiXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateWebapiXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Webapi XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/webapi.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankWebapi; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/GenerateWidgetXmlFileCommand.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTemplateGeneratorCommand } from './SimpleTemplateGeneratorCommand'; 2 | import { TemplateWizardData } from 'wizard/SimpleTemplateWizard'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | 6 | export default class GenerateWidgetXmlFileCommand extends SimpleTemplateGeneratorCommand { 7 | constructor() { 8 | super('magento-toolbox.generateWidgetXmlFile'); 9 | } 10 | 11 | getWizardTitle(): string { 12 | return 'Widget XML File'; 13 | } 14 | 15 | getFileHeader(data: TemplateWizardData): string | undefined { 16 | return FileHeader.getHeader(data.module); 17 | } 18 | 19 | getFilePath(data: TemplateWizardData): string { 20 | const [vendor, module] = data.module.split('_'); 21 | 22 | return `app/code/${vendor}/${module}/etc/widget.xml`; 23 | } 24 | 25 | getTemplateName(data: TemplateWizardData): TemplatePath { 26 | return TemplatePath.XmlBlankWidget; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/command/IndexWorkspaceCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import IndexRunner from 'indexer/IndexRunner'; 3 | 4 | export default class IndexWorkspaceCommand extends Command { 5 | constructor() { 6 | super('magento-toolbox.indexWorkspace'); 7 | } 8 | 9 | public async execute(...args: any[]): Promise { 10 | await IndexRunner.indexWorkspace(true); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/command/JumpToModuleCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'command/Command'; 2 | import { window } from 'vscode'; 3 | import IndexManager from 'indexer/IndexManager'; 4 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 5 | import { Uri } from 'vscode'; 6 | 7 | export default class JumpToModuleCommand extends Command { 8 | constructor() { 9 | super('magento-toolbox.jumpToModule'); 10 | } 11 | 12 | public async execute(...args: any[]): Promise { 13 | const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); 14 | 15 | if (!moduleIndexData) { 16 | window.showErrorMessage('Module index data not found'); 17 | return; 18 | } 19 | 20 | const moduleName = await window.showQuickPick(moduleIndexData.getModuleOptions(), { 21 | placeHolder: 'Enter module name (e.g. Magento_Catalog)', 22 | }); 23 | 24 | if (!moduleName) { 25 | return; 26 | } 27 | 28 | const module = moduleIndexData.getModule(moduleName.label); 29 | 30 | if (!module) { 31 | window.showErrorMessage(`Module ${moduleName} not found`); 32 | return; 33 | } 34 | 35 | const moduleXmlPath = Uri.file(`${module.path}/etc/module.xml`); 36 | 37 | const document = await window.showTextDocument(moduleXmlPath); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/common/Config.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from 'vscode'; 2 | 3 | class Config { 4 | public readonly SECTION = 'magento-toolbox'; 5 | 6 | public get(key: string): T | undefined { 7 | return workspace.getConfiguration(this.SECTION).get(key); 8 | } 9 | } 10 | 11 | export default new Config(); 12 | -------------------------------------------------------------------------------- /src/common/ExtensionState.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext, WorkspaceFolder } from 'vscode'; 2 | 3 | export default class ExtensionState { 4 | private static _context: ExtensionContext; 5 | private static _magentoWorkspaces: WorkspaceFolder[] = []; 6 | 7 | public static init(context: ExtensionContext, magentoWorkspaces: WorkspaceFolder[]) { 8 | this._context = context; 9 | this._magentoWorkspaces = magentoWorkspaces; 10 | } 11 | 12 | public static get context() { 13 | return this._context; 14 | } 15 | 16 | public static get magentoWorkspaces() { 17 | return this._magentoWorkspaces; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/GetMagentoPath.ts: -------------------------------------------------------------------------------- 1 | import IndexManager from 'indexer/IndexManager'; 2 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 3 | import { Uri } from 'vscode'; 4 | 5 | export class GetMagentoPath { 6 | public readonly TEMPLATE_EXTENSIONS = ['.phtml']; 7 | public readonly WEB_EXTENSIONS = ['.css', '.js']; 8 | public readonly IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; 9 | 10 | public readonly TEMPLATE_PATHS = [ 11 | 'view/frontend/templates/', 12 | 'view/adminhtml/templates/', 13 | 'view/base/templates/', 14 | 'templates/', 15 | ]; 16 | 17 | public readonly WEB_PATHS = [ 18 | 'view/frontend/web/', 19 | 'view/adminhtml/web/', 20 | 'view/base/web/', 21 | 'web/', 22 | ]; 23 | 24 | public getMagentoPath(file: Uri): string | undefined { 25 | if (!file) { 26 | return; 27 | } 28 | 29 | const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); 30 | 31 | if (!moduleIndexData) { 32 | return; 33 | } 34 | 35 | const module = moduleIndexData.getModuleByUri(file, false); 36 | 37 | if (!module) { 38 | return; 39 | } 40 | 41 | const paths = this.getPaths(file); 42 | 43 | if (!paths) { 44 | return; 45 | } 46 | 47 | const pathIndex = paths.findIndex(p => file.fsPath.lastIndexOf(p) !== -1); 48 | 49 | if (pathIndex === -1) { 50 | return; 51 | } 52 | 53 | const endIndex = file.fsPath.lastIndexOf(paths[pathIndex]); 54 | const offset = paths[pathIndex].length; 55 | const relativePath = file.fsPath.slice(offset + endIndex); 56 | 57 | const magentoPath = `${module.name}::${relativePath}`; 58 | 59 | return magentoPath; 60 | } 61 | 62 | private getPaths(file: Uri): string[] | undefined { 63 | if (this.TEMPLATE_EXTENSIONS.some(ext => file.fsPath.endsWith(ext))) { 64 | return this.TEMPLATE_PATHS; 65 | } 66 | 67 | if ( 68 | this.WEB_EXTENSIONS.some(ext => file.fsPath.endsWith(ext)) || 69 | this.IMAGE_EXTENSIONS.some(ext => file.fsPath.endsWith(ext)) 70 | ) { 71 | return this.WEB_PATHS; 72 | } 73 | 74 | return undefined; 75 | } 76 | } 77 | 78 | export default new GetMagentoPath(); 79 | -------------------------------------------------------------------------------- /src/common/MagentoCli.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export default class MagentoCli { 4 | private static readonly TERMINAL_NAME = 'Magento Toolbox'; 5 | private static DEFAULT_CLI_PATH = 'bin/magento'; 6 | 7 | private magentoCliPath: string; 8 | 9 | public constructor() { 10 | this.magentoCliPath = 11 | vscode.workspace.getConfiguration('magento-toolbox').get('magentoCliPath') || 12 | MagentoCli.DEFAULT_CLI_PATH; 13 | } 14 | 15 | public async run(command: string, args: string[] = []): Promise { 16 | return new Promise((resolve, reject) => { 17 | const cmd = `${this.magentoCliPath} ${command} ${args.join(' ')}`; 18 | 19 | const terminal = this.getOrCreateTerminal(); 20 | let timeout: NodeJS.Timeout; 21 | 22 | terminal.show(); 23 | terminal.sendText(cmd, true); 24 | 25 | vscode.window.onDidEndTerminalShellExecution(event => { 26 | if (event.terminal.name === MagentoCli.TERMINAL_NAME) { 27 | clearTimeout(timeout); 28 | 29 | resolve(event.exitCode ?? 0); 30 | } 31 | }); 32 | 33 | timeout = setTimeout(() => { 34 | reject(new Error('Timeout')); 35 | }, 30000); 36 | }); 37 | } 38 | 39 | public dispose() { 40 | const terminal = vscode.window.terminals.find(t => t.name === MagentoCli.TERMINAL_NAME); 41 | 42 | if (terminal) { 43 | terminal.dispose(); 44 | } 45 | } 46 | 47 | private getOrCreateTerminal(): vscode.Terminal { 48 | const terminal = vscode.window.terminals.find(t => t.name === MagentoCli.TERMINAL_NAME); 49 | 50 | if (terminal) { 51 | return terminal; 52 | } 53 | 54 | return vscode.window.createTerminal(MagentoCli.TERMINAL_NAME); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/common/MarkdownMessageBuilder.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString } from 'vscode'; 2 | 3 | export default class MarkdownMessageBuilder { 4 | private string: MarkdownString; 5 | 6 | public constructor(title: string) { 7 | let t = 'Magento Toolbox'; 8 | 9 | if (title) { 10 | t += ` - ${title}`; 11 | } 12 | 13 | this.string = new MarkdownString(`**${t}**\n\n`); 14 | } 15 | 16 | public static create(title: string): MarkdownMessageBuilder { 17 | return new MarkdownMessageBuilder(title); 18 | } 19 | 20 | public build(): MarkdownString { 21 | return this.string; 22 | } 23 | 24 | public appendText(line: string): void { 25 | this.string.appendText(line); 26 | } 27 | 28 | public appendMarkdown(line: string): void { 29 | this.string.appendMarkdown(line); 30 | } 31 | 32 | public appendCodeblock(code: string, language?: string): void { 33 | this.string.appendCodeblock(code, language); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/common/PhpNamespace.ts: -------------------------------------------------------------------------------- 1 | import { trimStart } from 'lodash-es'; 2 | 3 | export default class PhpNamespace { 4 | private static readonly NS_SEPARATOR = '\\'; 5 | 6 | public constructor(private parts: string[]) {} 7 | 8 | public static fromString(ns: string): PhpNamespace { 9 | return new PhpNamespace( 10 | trimStart(ns, PhpNamespace.NS_SEPARATOR) 11 | .split(PhpNamespace.NS_SEPARATOR) 12 | .filter(part => part.length > 0) 13 | ); 14 | } 15 | 16 | public static fromParts(parts: string[]): PhpNamespace { 17 | return new PhpNamespace(parts); 18 | } 19 | 20 | public pop(): string { 21 | return this.parts.pop() as string; 22 | } 23 | 24 | public getParts(): string[] { 25 | return this.parts; 26 | } 27 | 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 | 36 | public toString(): string { 37 | return this.parts.join(PhpNamespace.NS_SEPARATOR); 38 | } 39 | 40 | public append(...parts: (string | PhpNamespace)[]): PhpNamespace { 41 | return new PhpNamespace([ 42 | ...this.parts, 43 | ...parts.flatMap(part => { 44 | if (typeof part === 'string') { 45 | return [part]; 46 | } 47 | 48 | return part.getParts(); 49 | }), 50 | ]); 51 | } 52 | 53 | public prepend(part: string | PhpNamespace): PhpNamespace { 54 | if (typeof part === 'string') { 55 | return new PhpNamespace([part, ...this.parts]); 56 | } 57 | 58 | return new PhpNamespace([...part.getParts(), ...this.parts]); 59 | } 60 | 61 | public isSubNamespaceOf(ns: PhpNamespace): boolean { 62 | if (this.parts.length < ns.parts.length) { 63 | return false; 64 | } 65 | 66 | return this.parts.slice(0, ns.parts.length).every((part, index) => part === ns.parts[index]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/common/Validation.ts: -------------------------------------------------------------------------------- 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 | } 38 | -------------------------------------------------------------------------------- /src/common/php/FileHeader.ts: -------------------------------------------------------------------------------- 1 | import Config from 'common/Config'; 2 | 3 | export default class FileHeader { 4 | public static getHeader(module: string): string | undefined { 5 | const header = Config.get('phpFileHeaderComment'); 6 | 7 | if (!header) { 8 | return undefined; 9 | } 10 | 11 | return header.replace('%module%', module); 12 | } 13 | 14 | public static getHeaderAsComment(module: string): string { 15 | const header = this.getHeader(module); 16 | 17 | if (!header) { 18 | return ''; 19 | } 20 | 21 | let comment = `/**\n`; 22 | 23 | header.split(/[\r\n]+/).forEach(line => { 24 | comment += ` * ${line}\n`; 25 | }); 26 | 27 | comment += ` */\n`; 28 | 29 | return comment; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/common/php/PhpDocumentParser.ts: -------------------------------------------------------------------------------- 1 | import DocumentCache from 'cache/DocumentCache'; 2 | import PhpParser from 'parser/php/Parser'; 3 | import { PhpFile } from 'parser/php/PhpFile'; 4 | import { TextDocument, Uri } from 'vscode'; 5 | 6 | class PhpDocumentParser { 7 | protected readonly parser: PhpParser; 8 | 9 | constructor() { 10 | this.parser = new PhpParser(); 11 | } 12 | 13 | public async parse(document: TextDocument): Promise { 14 | const cacheKey = `php-file`; 15 | 16 | if (DocumentCache.has(document, cacheKey)) { 17 | return DocumentCache.get(document, cacheKey); 18 | } 19 | 20 | const phpParser = new PhpParser(); 21 | const phpFile = await phpParser.parseDocument(document); 22 | DocumentCache.set(document, cacheKey, phpFile); 23 | return phpFile; 24 | } 25 | 26 | public async parseUri(document: TextDocument, uri: Uri): Promise { 27 | const cacheKey = `php-file-${uri.fsPath}`; 28 | 29 | if (DocumentCache.has(document, cacheKey)) { 30 | return DocumentCache.get(document, cacheKey); 31 | } 32 | 33 | const phpParser = new PhpParser(); 34 | const phpFile = await phpParser.parse(uri); 35 | DocumentCache.set(document, cacheKey, phpFile); 36 | return phpFile; 37 | } 38 | } 39 | 40 | export default new PhpDocumentParser(); 41 | -------------------------------------------------------------------------------- /src/common/php/PluginInfo.ts: -------------------------------------------------------------------------------- 1 | import { PhpClass } from 'parser/php/PhpClass'; 2 | import { PhpFile } from 'parser/php/PhpFile'; 3 | import { PhpMethod } from 'parser/php/PhpMethod'; 4 | import Magento from 'util/Magento'; 5 | 6 | export default class PluginInfo { 7 | constructor(private readonly phpFile: PhpFile) {} 8 | 9 | public getPluginMethods(phpClass: PhpClass): PhpMethod[] { 10 | return phpClass.methods.filter(method => Magento.isPluginMethod(method.name)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/common/xml/FileHeader.ts: -------------------------------------------------------------------------------- 1 | import Config from 'common/Config'; 2 | 3 | export default class FileHeader { 4 | public static getHeader(module: string): string | undefined { 5 | const header = Config.get('xmlFileHeaderComment'); 6 | 7 | if (!header) { 8 | return undefined; 9 | } 10 | 11 | let headerComment = ''; 16 | 17 | return headerComment; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/xml/XmlDocumentParser.ts: -------------------------------------------------------------------------------- 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, skipCache = false): Promise { 22 | const cacheKey = `xml-file`; 23 | 24 | if (!skipCache && 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 | 32 | if (!skipCache) { 33 | DocumentCache.set(document, cacheKey, tokenData); 34 | } 35 | 36 | return tokenData; 37 | } 38 | } 39 | 40 | export default new XmlDocumentParser(); 41 | -------------------------------------------------------------------------------- /src/common/xml/XmlSuggestionProviderProcessor.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, Position, TextDocument } from 'vscode'; 2 | import XmlDocumentParser, { TokenData } from 'common/xml/XmlDocumentParser'; 3 | import { XmlSuggestionProvider } from './XmlSuggestionProvider'; 4 | 5 | export abstract class XmlSuggestionProviderProcessor { 6 | public constructor(private providers: XmlSuggestionProvider[]) {} 7 | 8 | public async provideSuggestions( 9 | document: TextDocument, 10 | position: Position, 11 | token: CancellationToken 12 | ): Promise { 13 | if (!this.providers.some(provider => provider.canProvideSuggestions(document))) { 14 | return []; 15 | } 16 | 17 | const tokenData = await XmlDocumentParser.parse(document, true); 18 | 19 | const providerCompletionItems = await Promise.all( 20 | this.providers.map(provider => 21 | this.getProviderCompletionItems(provider, document, position, tokenData) 22 | ) 23 | ); 24 | 25 | return providerCompletionItems.flat(); 26 | } 27 | 28 | protected async getProviderCompletionItems( 29 | provider: XmlSuggestionProvider, 30 | document: TextDocument, 31 | position: Position, 32 | tokenData: TokenData 33 | ): Promise { 34 | if (!provider.canProvideSuggestions(document)) { 35 | return []; 36 | } 37 | 38 | return provider.provideSuggestions(document, position, tokenData); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/common/xml/suggestion/condition/AttributeNameMatches.ts: -------------------------------------------------------------------------------- 1 | import { XMLAttribute, XMLElement } from '@xml-tools/ast'; 2 | import { MatchCondition } from './MatchCondition'; 3 | 4 | export class AttributeNameMatches implements MatchCondition { 5 | public constructor(private readonly attributeName: string) {} 6 | 7 | public match(element: XMLElement, attribute?: XMLAttribute): boolean { 8 | if (!attribute) { 9 | return false; 10 | } 11 | 12 | return attribute.key === this.attributeName; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/common/xml/suggestion/condition/ElementAttributeMatches.ts: -------------------------------------------------------------------------------- 1 | import { XMLElement } from '@xml-tools/ast'; 2 | import { MatchCondition } from './MatchCondition'; 3 | 4 | export class ElementAttributeMatches implements MatchCondition { 5 | public constructor( 6 | private readonly attributeName: string, 7 | private readonly attributeValue: string 8 | ) {} 9 | 10 | public match(element: XMLElement): boolean { 11 | return element.attributes.some( 12 | attr => attr.key === this.attributeName && attr.value === this.attributeValue 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/common/xml/suggestion/condition/ElementNameMatches.ts: -------------------------------------------------------------------------------- 1 | import { XMLElement } from '@xml-tools/ast'; 2 | import { MatchCondition } from './MatchCondition'; 3 | 4 | export class ElementNameMatches implements MatchCondition { 5 | public constructor(private readonly elementName: string) {} 6 | 7 | public match(element: XMLElement): boolean { 8 | return element.name === this.elementName; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/common/xml/suggestion/condition/MatchCondition.ts: -------------------------------------------------------------------------------- 1 | import { XMLAttribute, XMLElement } from '@xml-tools/ast'; 2 | 3 | export interface MatchCondition { 4 | match(element: XMLElement, attribute?: XMLAttribute): boolean; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/xml/suggestion/condition/ParentElementNameMatches.ts: -------------------------------------------------------------------------------- 1 | import { XMLElement } from '@xml-tools/ast'; 2 | import { MatchCondition } from './MatchCondition'; 3 | 4 | export class ParentElementNameMatches implements MatchCondition { 5 | public constructor(private readonly elementName: string) {} 6 | 7 | public match(element: XMLElement): boolean { 8 | if (element.parent.type === 'XMLElement') { 9 | return element.parent.name === this.elementName; 10 | } 11 | 12 | return false; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/completion/XmlCompletionProviderProcessor.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemProvider } from 'vscode'; 2 | 3 | import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor'; 4 | import { CompletionItem, Position, TextDocument } from 'vscode'; 5 | 6 | import { CancellationToken } from 'vscode'; 7 | import { ModuleCompletionProvider } from './xml/ModuleCompletionProvider'; 8 | import { NamespaceCompletionProvider } from './xml/NamespaceCompletionProvider'; 9 | import { AclCompletionProvider } from './xml/AclCompletionProvider'; 10 | import { TemplateCompletionProvider } from './xml/TemplateCompletionProvider'; 11 | import { EventCompletionProvider } from './xml/EventCompletionProvider'; 12 | 13 | export class XmlCompletionProviderProcessor 14 | extends XmlSuggestionProviderProcessor 15 | implements CompletionItemProvider 16 | { 17 | public constructor() { 18 | super([ 19 | new ModuleCompletionProvider(), 20 | new NamespaceCompletionProvider(), 21 | new AclCompletionProvider(), 22 | new TemplateCompletionProvider(), 23 | new EventCompletionProvider(), 24 | ]); 25 | } 26 | 27 | public async provideCompletionItems( 28 | document: TextDocument, 29 | position: Position, 30 | token: CancellationToken 31 | ): Promise { 32 | return this.provideSuggestions(document, position, token); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/completion/xml/AclCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument, CompletionItem, CompletionItemKind, Range } from 'vscode'; 2 | import AclIndexer from 'indexer/acl/AclIndexer'; 3 | import IndexManager from 'indexer/IndexManager'; 4 | import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; 5 | import { XMLElement, XMLAttribute } from '@xml-tools/ast'; 6 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 7 | import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; 8 | 9 | export class AclCompletionProvider extends XmlSuggestionProvider { 10 | public getFilePatterns(): string[] { 11 | return ['**/etc/acl.xml', '**/etc/webapi.xml', '**/etc/**/system.xml']; 12 | } 13 | 14 | public getAttributeValueConditions(): CombinedCondition[] { 15 | return [ 16 | [new ElementNameMatches('resource'), new AttributeNameMatches('id')], 17 | [new ElementNameMatches('resource'), new AttributeNameMatches('ref')], 18 | ]; 19 | } 20 | 21 | public getElementContentMatches(): CombinedCondition[] { 22 | return [[new ElementNameMatches('resource')]]; 23 | } 24 | 25 | public getConfigKey(): string | undefined { 26 | return 'provideXmlCompletions'; 27 | } 28 | 29 | public getSuggestionItems( 30 | value: string, 31 | range: Range, 32 | document: TextDocument, 33 | element: XMLElement, 34 | attribute?: XMLAttribute 35 | ): CompletionItem[] { 36 | const aclIndexData = IndexManager.getIndexData(AclIndexer.KEY); 37 | 38 | if (!aclIndexData) { 39 | return []; 40 | } 41 | 42 | const acls = aclIndexData.getAclsByPrefix(value); 43 | 44 | if (!acls) { 45 | return []; 46 | } 47 | 48 | return acls.map(acl => { 49 | const item = new CompletionItem(acl.id, CompletionItemKind.Value); 50 | item.range = range; 51 | return item; 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/completion/xml/EventCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument, CompletionItem, CompletionItemKind, Range } from 'vscode'; 2 | import IndexManager from 'indexer/IndexManager'; 3 | import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; 4 | import { XMLElement, XMLAttribute } from '@xml-tools/ast'; 5 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 6 | import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; 7 | import EventsIndexer from 'indexer/events/EventsIndexer'; 8 | 9 | export class EventCompletionProvider extends XmlSuggestionProvider { 10 | public getFilePatterns(): string[] { 11 | return ['**/etc/events.xml']; 12 | } 13 | 14 | public getAttributeValueConditions(): CombinedCondition[] { 15 | return [[new ElementNameMatches('event'), new AttributeNameMatches('name')]]; 16 | } 17 | 18 | public getConfigKey(): string | undefined { 19 | return 'provideXmlCompletions'; 20 | } 21 | 22 | public getSuggestionItems( 23 | value: string, 24 | range: Range, 25 | document: TextDocument, 26 | element: XMLElement, 27 | attribute?: XMLAttribute 28 | ): CompletionItem[] { 29 | const eventIndexData = IndexManager.getIndexData(EventsIndexer.KEY); 30 | 31 | if (!eventIndexData) { 32 | return []; 33 | } 34 | 35 | const events = eventIndexData.getEventsByPrefix(value); 36 | 37 | if (!events) { 38 | return []; 39 | } 40 | 41 | return events.map(event => { 42 | const item = new CompletionItem(event.name, CompletionItemKind.Value); 43 | item.range = range; 44 | return item; 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/completion/xml/ModuleCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, CompletionItemKind, Range, TextDocument } from 'vscode'; 2 | import IndexManager from 'indexer/IndexManager'; 3 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 4 | import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; 5 | import { XMLAttribute } from '@xml-tools/ast'; 6 | import { XMLElement } from '@xml-tools/ast'; 7 | import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; 8 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 9 | import { ParentElementNameMatches } from 'common/xml/suggestion/condition/ParentElementNameMatches'; 10 | 11 | export class ModuleCompletionProvider extends XmlSuggestionProvider { 12 | public getFilePatterns(): string[] { 13 | return ['**/etc/module.xml']; 14 | } 15 | 16 | public getConfigKey(): string | undefined { 17 | return 'provideXmlCompletions'; 18 | } 19 | 20 | public getAttributeValueConditions(): CombinedCondition[] { 21 | return [ 22 | [new ElementNameMatches('module'), new AttributeNameMatches('name')], 23 | [new ElementNameMatches('module'), new ParentElementNameMatches('route')], 24 | ]; 25 | } 26 | 27 | public getSuggestionItems( 28 | value: string, 29 | range: Range, 30 | document: TextDocument, 31 | element: XMLElement, 32 | attribute?: XMLAttribute 33 | ): CompletionItem[] { 34 | const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); 35 | 36 | if (!moduleIndexData) { 37 | return []; 38 | } 39 | 40 | const completions = moduleIndexData.getModulesByPrefix(value); 41 | 42 | return completions.map(module => { 43 | const item = new CompletionItem(module.name, CompletionItemKind.Value); 44 | item.range = range; 45 | return item; 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/completion/xml/TemplateCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItem, Uri, Range, TextDocument } from 'vscode'; 2 | import IndexManager from 'indexer/IndexManager'; 3 | import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; 4 | import { XMLElement, XMLAttribute } from '@xml-tools/ast'; 5 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 6 | import TemplateIndexer from 'indexer/template/TemplateIndexer'; 7 | import { ElementAttributeMatches } from 'common/xml/suggestion/condition/ElementAttributeMatches'; 8 | 9 | export class TemplateCompletionProvider extends XmlSuggestionProvider { 10 | public getFilePatterns(): string[] { 11 | return ['**/view/**/layout/*.xml', '**/etc/**/di.xml']; 12 | } 13 | 14 | public getAttributeValueConditions(): CombinedCondition[] { 15 | return [[new AttributeNameMatches('template')]]; 16 | } 17 | 18 | public getElementContentMatches(): CombinedCondition[] { 19 | return [ 20 | [ 21 | new ElementAttributeMatches('name', 'template'), 22 | new ElementAttributeMatches('xsi:type', 'string'), 23 | ], 24 | ]; 25 | } 26 | 27 | public getConfigKey(): string | undefined { 28 | return 'provideXmlCompletions'; 29 | } 30 | 31 | public getSuggestionItems( 32 | value: string, 33 | range: Range, 34 | document: TextDocument, 35 | element: XMLElement, 36 | attribute?: XMLAttribute 37 | ): CompletionItem[] { 38 | const templateIndexData = IndexManager.getIndexData(TemplateIndexer.KEY); 39 | 40 | if (!templateIndexData) { 41 | return []; 42 | } 43 | 44 | const templates = templateIndexData.getTemplatesByPrefix(value); 45 | 46 | return templates.map(template => { 47 | const item = new CompletionItem(template.magentoPath); 48 | item.range = range; 49 | return item; 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/decorator/TextDocumentDecorationProvider.ts: -------------------------------------------------------------------------------- 1 | import { DecorationOptions, TextDocument, TextEditorDecorationType } from 'vscode'; 2 | 3 | export default abstract class TextDocumentDecorationProvider { 4 | public constructor(protected readonly document: TextDocument) {} 5 | 6 | public abstract getType(): TextEditorDecorationType; 7 | 8 | public abstract getDecorations(): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /src/definition/XmlDefinitionProviderProcessor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, 3 | DefinitionProvider, 4 | LocationLink, 5 | Position, 6 | TextDocument, 7 | } from 'vscode'; 8 | import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor'; 9 | import { AclDefinitionProvider } from './xml/AclDefinitionProvider'; 10 | import { ModuleDefinitionProvider } from './xml/ModuleDefinitionProvider'; 11 | import { TemplateDefinitionProvider } from './xml/TemplateDefinitionProvider'; 12 | 13 | export class XmlDefinitionProviderProcessor 14 | extends XmlSuggestionProviderProcessor 15 | implements DefinitionProvider 16 | { 17 | public constructor() { 18 | super([ 19 | new AclDefinitionProvider(), 20 | new ModuleDefinitionProvider(), 21 | new TemplateDefinitionProvider(), 22 | ]); 23 | } 24 | 25 | public async provideDefinition( 26 | document: TextDocument, 27 | position: Position, 28 | token: CancellationToken 29 | ): Promise { 30 | const definitions = await this.provideSuggestions(document, position, token); 31 | return definitions.filter(definition => definition.targetUri.fsPath !== document.uri.fsPath); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/definition/xml/AclDefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | import { LocationLink, Uri, Range, TextDocument } from 'vscode'; 2 | import AclIndexer from 'indexer/acl/AclIndexer'; 3 | import IndexManager from 'indexer/IndexManager'; 4 | import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; 5 | import { XMLElement, XMLAttribute } from '@xml-tools/ast'; 6 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 7 | import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; 8 | 9 | export class AclDefinitionProvider extends XmlSuggestionProvider { 10 | public getFilePatterns(): string[] { 11 | return ['**/etc/acl.xml', '**/etc/webapi.xml', '**/etc/**/system.xml']; 12 | } 13 | 14 | public getAttributeValueConditions(): CombinedCondition[] { 15 | return [ 16 | [new ElementNameMatches('resource'), new AttributeNameMatches('id')], 17 | [new ElementNameMatches('resource'), new AttributeNameMatches('ref')], 18 | ]; 19 | } 20 | 21 | public getElementContentMatches(): CombinedCondition[] { 22 | return [[new ElementNameMatches('resource')]]; 23 | } 24 | 25 | public getConfigKey(): string | undefined { 26 | return 'provideXmlDefinitions'; 27 | } 28 | 29 | public getSuggestionItems( 30 | value: string, 31 | range: Range, 32 | document: TextDocument, 33 | element: XMLElement, 34 | attribute?: XMLAttribute 35 | ): LocationLink[] { 36 | const aclIndexData = IndexManager.getIndexData(AclIndexer.KEY); 37 | 38 | if (!aclIndexData) { 39 | return []; 40 | } 41 | 42 | const acl = aclIndexData.getAcl(value); 43 | 44 | if (!acl) { 45 | return []; 46 | } 47 | 48 | const aclXmlUri = Uri.file(acl.path); 49 | 50 | return [ 51 | { 52 | targetUri: aclXmlUri, 53 | targetRange: new Range(0, 0, 0, 0), 54 | originSelectionRange: range, 55 | }, 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/definition/xml/ModuleDefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | import { LocationLink, Uri, Range, TextDocument } from 'vscode'; 2 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 3 | import IndexManager from 'indexer/IndexManager'; 4 | import { CombinedCondition, XmlSuggestionProvider } from 'common/xml/XmlSuggestionProvider'; 5 | import { XMLElement, XMLAttribute } from '@xml-tools/ast'; 6 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 7 | import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; 8 | 9 | export class ModuleDefinitionProvider extends XmlSuggestionProvider { 10 | public getFilePatterns(): string[] { 11 | return ['**/etc/module.xml', '**/etc/**/routes.xml']; 12 | } 13 | 14 | public getAttributeValueConditions(): CombinedCondition[] { 15 | return [[new ElementNameMatches('module'), new AttributeNameMatches('name')]]; 16 | } 17 | 18 | public getConfigKey(): string | undefined { 19 | return 'provideXmlDefinitions'; 20 | } 21 | 22 | public getSuggestionItems( 23 | value: string, 24 | range: Range, 25 | document: TextDocument, 26 | element: XMLElement, 27 | attribute?: XMLAttribute 28 | ): LocationLink[] { 29 | if (!attribute) { 30 | return []; 31 | } 32 | 33 | const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); 34 | 35 | if (!moduleIndexData) { 36 | return []; 37 | } 38 | 39 | const module = moduleIndexData.getModule(value); 40 | 41 | if (!module) { 42 | return []; 43 | } 44 | 45 | const moduleXmlUri = Uri.file(module.moduleXmlPath); 46 | 47 | return [ 48 | { 49 | targetUri: moduleXmlUri, 50 | targetRange: new Range(0, 0, 0, 0), 51 | originSelectionRange: new Range( 52 | attribute.position.startLine - 1, 53 | attribute.position.startColumn + 5, 54 | attribute.position.endLine - 1, 55 | attribute.position.endColumn - 1 56 | ), 57 | }, 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/definition/xml/TemplateDefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | import { LocationLink, Uri, Range, TextDocument } from 'vscode'; 2 | import IndexManager from 'indexer/IndexManager'; 3 | import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; 4 | import { XMLElement, XMLAttribute } from '@xml-tools/ast'; 5 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 6 | import TemplateIndexer from 'indexer/template/TemplateIndexer'; 7 | import { ElementAttributeMatches } from 'common/xml/suggestion/condition/ElementAttributeMatches'; 8 | 9 | export class TemplateDefinitionProvider extends XmlSuggestionProvider { 10 | public getFilePatterns(): string[] { 11 | return ['**/view/**/layout/*.xml', '**/etc/**/di.xml']; 12 | } 13 | 14 | public getAttributeValueConditions(): CombinedCondition[] { 15 | return [[new AttributeNameMatches('template')]]; 16 | } 17 | 18 | public getElementContentMatches(): CombinedCondition[] { 19 | return [ 20 | [ 21 | new ElementAttributeMatches('name', 'template'), 22 | new ElementAttributeMatches('xsi:type', 'string'), 23 | ], 24 | ]; 25 | } 26 | 27 | public getConfigKey(): string | undefined { 28 | return 'provideXmlDefinitions'; 29 | } 30 | 31 | public getSuggestionItems( 32 | value: string, 33 | range: Range, 34 | document: TextDocument, 35 | element: XMLElement, 36 | attribute?: XMLAttribute 37 | ): LocationLink[] { 38 | const templateIndexData = IndexManager.getIndexData(TemplateIndexer.KEY); 39 | 40 | if (!templateIndexData) { 41 | return []; 42 | } 43 | 44 | const templates = templateIndexData.getTemplatesByPrefix(value); 45 | 46 | if (!templates.length) { 47 | return []; 48 | } 49 | 50 | return templates.map(template => { 51 | const templateUri = Uri.file(template.path); 52 | 53 | return { 54 | targetUri: templateUri, 55 | targetRange: new Range(0, 0, 0, 0), 56 | originSelectionRange: range, 57 | }; 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/diagnostics/DiagnosticCollectionProvider.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, DiagnosticCollection, languages, TextDocument } from 'vscode'; 2 | import { LanguageDiagnostics } from './LanguageDiagnostics'; 3 | import PluginDiagnostics from './php/PluginDiagnostics'; 4 | 5 | class DiagnosticCollectionProvider { 6 | private readonly languageDiagnostics: LanguageDiagnostics[]; 7 | private readonly collections: Record; 8 | 9 | constructor() { 10 | this.languageDiagnostics = [new PluginDiagnostics()]; 11 | this.collections = {}; 12 | } 13 | 14 | public getCollection(language: string): DiagnosticCollection { 15 | if (!this.collections[language]) { 16 | this.collections[language] = languages.createDiagnosticCollection(language); 17 | } 18 | 19 | return this.collections[language]; 20 | } 21 | 22 | public async updateDiagnostics(document: TextDocument): Promise { 23 | const diagnostics: Record = {}; 24 | 25 | for (const languageDiagnostic of this.languageDiagnostics) { 26 | const language = languageDiagnostic.getLanguage(); 27 | 28 | if (language !== document.languageId) { 29 | continue; 30 | } 31 | 32 | const languageDiagnostics = await languageDiagnostic.updateDiagnostics(document); 33 | 34 | if (!diagnostics[language]) { 35 | diagnostics[language] = []; 36 | } 37 | 38 | diagnostics[language].push(...languageDiagnostics); 39 | } 40 | 41 | for (const language in this.collections) { 42 | this.getCollection(language).clear(); 43 | } 44 | 45 | for (const language in diagnostics) { 46 | this.getCollection(language).set(document.uri, diagnostics[language]); 47 | } 48 | } 49 | } 50 | 51 | export default new DiagnosticCollectionProvider(); 52 | -------------------------------------------------------------------------------- /src/diagnostics/LanguageDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic, TextDocument } from 'vscode'; 2 | 3 | export interface LanguageDiagnostics { 4 | getLanguage(): string; 5 | updateDiagnostics(document: TextDocument): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/diagnostics/php/PluginDiagnostics.ts: -------------------------------------------------------------------------------- 1 | import { LanguageDiagnostics } from 'diagnostics/LanguageDiagnostics'; 2 | import { Diagnostic, TextDocument } from 'vscode'; 3 | 4 | export default class PluginDiagnostics implements LanguageDiagnostics { 5 | public getLanguage(): string { 6 | return 'php'; 7 | } 8 | 9 | public async updateDiagnostics(document: TextDocument): Promise { 10 | const diagnostics: Diagnostic[] = []; 11 | 12 | // TODO 13 | 14 | return diagnostics; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/generator/FileGenerator.ts: -------------------------------------------------------------------------------- 1 | import { Uri, workspace, window } from 'vscode'; 2 | import GeneratedFile from './GeneratedFile'; 3 | 4 | export default abstract class FileGenerator { 5 | public abstract generate(workspaceUri: Uri): Promise; 6 | 7 | public async writeFile(uri: Uri, data: string): Promise { 8 | await workspace.fs.writeFile(uri, Buffer.from(data, 'utf-8')); 9 | } 10 | 11 | public openFile(uri: Uri): void { 12 | workspace.openTextDocument(uri).then(window.showTextDocument); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/generator/GeneratedFile.ts: -------------------------------------------------------------------------------- 1 | import FileSystem from 'util/FileSystem'; 2 | import { Uri, workspace, window } from 'vscode'; 3 | 4 | export default class GeneratedFile { 5 | public constructor( 6 | public readonly uri: Uri, 7 | public readonly content: string, 8 | private readonly confirmOverwrite: boolean = true 9 | ) {} 10 | 11 | public open(): void { 12 | workspace.openTextDocument(this.uri).then(window.showTextDocument); 13 | } 14 | 15 | public async write(): Promise { 16 | if (this.confirmOverwrite && (await FileSystem.fileExists(this.uri))) { 17 | const result = await window.showQuickPick( 18 | [ 19 | { label: 'Overwrite', description: 'Overwrite the existing file', picked: true }, 20 | { label: 'Skip', description: 'Skip the file' }, 21 | ], 22 | { 23 | title: `File already exists: ${this.uri.fsPath}`, 24 | } 25 | ); 26 | 27 | if (result?.label === 'Skip') { 28 | return false; 29 | } 30 | } 31 | 32 | await workspace.fs.writeFile(this.uri, Buffer.from(this.content, 'utf-8')); 33 | return true; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/generator/HandlebarsTemplateRenderer.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'handlebars'; 2 | import { resolve } from 'path'; 3 | import FileSystem from 'util/FileSystem'; 4 | import { Uri } from 'vscode'; 5 | import Logger from 'util/Logger'; 6 | import { 7 | TemplatePath, 8 | TemplateParams, 9 | TemplatePartials, 10 | BaseTemplatePartials, 11 | } from 'types/handlebars'; 12 | 13 | export default class HandlebarsTemplateRenderer { 14 | protected handlebars: typeof Handlebars; 15 | 16 | public constructor() { 17 | this.handlebars = create(); 18 | this.registerHelpers(); 19 | this.registerGlobalPartials(); 20 | } 21 | 22 | public async render( 23 | template: T, 24 | data?: TemplateParams[T], 25 | partials?: TemplatePartials[T] | BaseTemplatePartials 26 | ): Promise { 27 | try { 28 | const templatePath = this.getTemplatePath(template); 29 | const templateContent = await FileSystem.readFile(Uri.file(templatePath)); 30 | 31 | if (partials) { 32 | for (const [name, content] of Object.entries(partials)) { 33 | if (typeof content === 'string') { 34 | this.handlebars.registerPartial(name, content + '\n'); 35 | } 36 | } 37 | } 38 | 39 | const compiledTemplate = this.handlebars.compile(templateContent); 40 | const content = compiledTemplate(data); 41 | return content; 42 | } catch (error) { 43 | Logger.log('Failed to generate template', String(error)); 44 | throw error; 45 | } 46 | } 47 | 48 | public getTemplatePath(templateName: string): string { 49 | return resolve(FileSystem.getExtensionPath('templates/handlebars'), templateName + '.hbs'); 50 | } 51 | 52 | protected registerHelpers(): void { 53 | this.handlebars.registerHelper('ifeq', (a: string, b: string, options: any) => { 54 | if (a === b) { 55 | return options.fn(this); 56 | } 57 | return options.inverse(this); 58 | }); 59 | } 60 | 61 | protected registerGlobalPartials(): void { 62 | this.handlebars.registerPartial('fileHeader', '{{#if fileHeader}}\n{{{fileHeader}}}\n{{/if}}'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/generator/TemplateGenerator.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import FileGenerator from './FileGenerator'; 3 | import GeneratedFile from './GeneratedFile'; 4 | import HandlebarsTemplateRenderer from './HandlebarsTemplateRenderer'; 5 | import { TemplatePath, TemplateParams } from 'types/handlebars'; 6 | 7 | export default class TemplateGenerator< 8 | T extends TemplatePath, 9 | P extends TemplateParams[T] = TemplateParams[T], 10 | > extends FileGenerator { 11 | protected renderer: HandlebarsTemplateRenderer | undefined; 12 | 13 | public constructor( 14 | protected fileName: string, 15 | protected templateName: T, 16 | protected data: P 17 | ) { 18 | super(); 19 | } 20 | 21 | public getTemplateData(): P { 22 | return this.data; 23 | } 24 | 25 | protected getTemplateRenderer(): HandlebarsTemplateRenderer { 26 | if (!this.renderer) { 27 | this.renderer = new HandlebarsTemplateRenderer(); 28 | } 29 | 30 | return this.renderer; 31 | } 32 | 33 | protected getTemplateContent(): Promise { 34 | return this.getTemplateRenderer().render(this.templateName, this.getTemplateData()); 35 | } 36 | 37 | public async generate(workspaceUri: Uri): Promise { 38 | const content = await this.getTemplateContent(); 39 | 40 | const path = Uri.joinPath(workspaceUri, this.fileName); 41 | return new GeneratedFile(path, content); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/generator/XmlGenerator.ts: -------------------------------------------------------------------------------- 1 | import { XMLBuilder, XmlBuilderOptions, XMLParser } from 'fast-xml-parser'; 2 | 3 | export default class XmlGenerator { 4 | public constructor(protected data: any) {} 5 | 6 | public static fromString(content: string): XmlGenerator { 7 | const parser = new XMLParser(); 8 | const data = parser.parse(content); 9 | return new XmlGenerator(data); 10 | } 11 | 12 | public toString(options?: XmlBuilderOptions): string { 13 | const builder = new XMLBuilder({ 14 | attributeNamePrefix: '@_', 15 | ignoreAttributes: false, 16 | oneListGroup: true, 17 | textNodeName: '#text', 18 | indentBy: ' ', 19 | format: true, 20 | ...(options ?? {}), 21 | }); 22 | 23 | return builder.build(this.data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/generator/block/BlockClassGenerator.ts: -------------------------------------------------------------------------------- 1 | import FileHeader from 'common/php/FileHeader'; 2 | import FileGenerator from 'generator/FileGenerator'; 3 | import GeneratedFile from 'generator/GeneratedFile'; 4 | import { PhpFile, PsrPrinter } from 'node-php-generator'; 5 | import Magento from 'util/Magento'; 6 | import { Uri } from 'vscode'; 7 | import { BlockWizardData } from 'wizard/BlockWizard'; 8 | 9 | export default class BlockClassGenerator extends FileGenerator { 10 | private static readonly BLOCK_CLASS_PARENT = 'Magento\\Framework\\View\\Element\\Template'; 11 | 12 | public constructor(protected data: BlockWizardData) { 13 | super(); 14 | } 15 | 16 | public async generate(workspaceUri: Uri): Promise { 17 | const [vendor, module] = this.data.module.split('_'); 18 | const pathParts = this.data.path.split('/'); 19 | const namespaceParts = [vendor, module, ...pathParts]; 20 | const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri); 21 | 22 | const header = FileHeader.getHeader(this.data.module); 23 | 24 | const phpFile = new PhpFile(); 25 | if (header) { 26 | phpFile.addComment(header); 27 | } 28 | phpFile.setStrictTypes(true); 29 | 30 | const namespace = phpFile.addNamespace(namespaceParts.join('\\')); 31 | namespace.addUse(BlockClassGenerator.BLOCK_CLASS_PARENT); 32 | 33 | const blockClass = namespace.addClass(this.data.name); 34 | 35 | blockClass.setExtends(BlockClassGenerator.BLOCK_CLASS_PARENT); 36 | 37 | const printer = new PsrPrinter(); 38 | 39 | return new GeneratedFile( 40 | Uri.joinPath(moduleDirectory, this.data.path, `${this.data.name}.php`), 41 | printer.printFile(phpFile) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/generator/cronJob/CronJobClassGenerator.ts: -------------------------------------------------------------------------------- 1 | import FileHeader from 'common/php/FileHeader'; 2 | import PhpNamespace from 'common/PhpNamespace'; 3 | import GeneratedFile from 'generator/GeneratedFile'; 4 | import FileGenerator from 'generator/FileGenerator'; 5 | import { PhpFile, PsrPrinter } from 'node-php-generator'; 6 | import { Uri } from 'vscode'; 7 | import { CronJobWizardData } from 'wizard/CronJobWizard'; 8 | import Magento from 'util/Magento'; 9 | import * as fs from 'fs'; 10 | import * as path from 'path'; 11 | 12 | export default class CronJobClassGenerator extends FileGenerator { 13 | public constructor(protected data: CronJobWizardData) { 14 | super(); 15 | } 16 | 17 | public async generate(workspaceUri: Uri): Promise { 18 | const [vendor, module] = this.data.module.split('_'); 19 | const cronDir = 'Cron'; 20 | const namespaceParts = [vendor, module, 'Cron']; 21 | const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri); 22 | 23 | // Create cron directory if it doesn't exist 24 | const cronDirPath = path.join(moduleDirectory.fsPath, cronDir); 25 | if (!fs.existsSync(cronDirPath)) { 26 | fs.mkdirSync(cronDirPath, { recursive: true }); 27 | } 28 | 29 | const phpFile = new PhpFile(); 30 | phpFile.setStrictTypes(true); 31 | 32 | const header = FileHeader.getHeader(this.data.module); 33 | 34 | if (header) { 35 | phpFile.addComment(header); 36 | } 37 | 38 | const namespace = phpFile.addNamespace(PhpNamespace.fromParts(namespaceParts).toString()); 39 | 40 | const cronClass = namespace.addClass(this.data.className); 41 | 42 | // Add execute method 43 | const executeMethod = cronClass.addMethod('execute'); 44 | executeMethod.addComment('Execute the cron'); 45 | executeMethod.addComment('\n@return void'); 46 | executeMethod.setReturnType('void'); 47 | executeMethod.setBody('// TODO: Implement execute() method.'); 48 | 49 | const printer = new PsrPrinter(); 50 | 51 | return new GeneratedFile( 52 | Uri.joinPath(moduleDirectory, cronDir, `${this.data.className}.php`), 53 | printer.printFile(phpFile) 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/generator/module/ModuleComposerGenerator.ts: -------------------------------------------------------------------------------- 1 | import FileGenerator from 'generator/FileGenerator'; 2 | import GeneratedFile from 'generator/GeneratedFile'; 3 | import Magento from 'util/Magento'; 4 | import { Uri } from 'vscode'; 5 | import { ModuleWizardComposerData } from 'wizard/ModuleWizard'; 6 | 7 | export default class ModuleComposerGenerator extends FileGenerator { 8 | public constructor(protected data: ModuleWizardComposerData) { 9 | super(); 10 | } 11 | 12 | public async generate(workspaceUri: Uri): Promise { 13 | const content = this.getComposerContent(); 14 | const moduleDirectory = Magento.getModuleDirectory( 15 | this.data.vendor, 16 | this.data.module, 17 | workspaceUri 18 | ); 19 | const moduleFile = Uri.joinPath(moduleDirectory, 'composer.json'); 20 | return new GeneratedFile(moduleFile, content); 21 | } 22 | 23 | public getComposerContent(): string { 24 | const namespace = Magento.getModuleNamespace(this.data.vendor, this.data.module); 25 | 26 | const object: any = { 27 | name: this.data.composerName, 28 | description: this.data.composerDescription, 29 | type: 'magento2-module', 30 | license: this.data.license.toUpperCase(), 31 | 'minimum-stability': 'dev', 32 | require: {}, 33 | autoload: { 34 | files: ['registration.php'], 35 | psr4: { 36 | [namespace.toString() + '\\']: '', 37 | }, 38 | }, 39 | }; 40 | 41 | if (this.data.version) { 42 | object.version = this.data.version; 43 | } 44 | 45 | return JSON.stringify(object, null, 4); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/generator/module/ModuleLicenseGenerator.ts: -------------------------------------------------------------------------------- 1 | import GeneratedFile from 'generator/GeneratedFile'; 2 | import TemplateGenerator from 'generator/TemplateGenerator'; 3 | import Magento from 'util/Magento'; 4 | import { Uri } from 'vscode'; 5 | import { ModuleWizardComposerData, ModuleWizardData } from 'wizard/ModuleWizard'; 6 | import { TemplatePath } from 'types/handlebars'; 7 | 8 | export default class ModuleLicenseGenerator extends TemplateGenerator< 9 | | TemplatePath.LicenseMit 10 | | TemplatePath.LicenseGplv3 11 | | TemplatePath.LicenseApache20 12 | | TemplatePath.LicenseOslv3 13 | > { 14 | public constructor(protected data: ModuleWizardData | ModuleWizardComposerData) { 15 | const params = { 16 | ...data, 17 | year: new Date().getFullYear(), 18 | }; 19 | 20 | super('LICENSE.txt', TemplatePath.LicenseMit, params); 21 | } 22 | 23 | public async generate(workspaceUri: Uri): Promise { 24 | const moduleDirectory = Magento.getModuleDirectory( 25 | this.data.vendor, 26 | this.data.module, 27 | workspaceUri 28 | ); 29 | 30 | return super.generate(moduleDirectory); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/generator/module/ModuleRegistrationGenerator.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWizardComposerData, ModuleWizardData } from 'wizard/ModuleWizard'; 2 | import { Uri } from 'vscode'; 3 | import GeneratedFile from 'generator/GeneratedFile'; 4 | import Magento from 'util/Magento'; 5 | import TemplateGenerator from 'generator/TemplateGenerator'; 6 | import FileHeader from 'common/php/FileHeader'; 7 | import { TemplatePath } from 'types/handlebars'; 8 | 9 | export default class ModuleRegistrationGenerator extends TemplateGenerator { 10 | public constructor(protected data: ModuleWizardData | ModuleWizardComposerData) { 11 | super('registration.php', TemplatePath.PhpRegistration, data); 12 | } 13 | 14 | public getTemplateData(): any { 15 | const data = super.getTemplateData(); 16 | 17 | data.fileHeader = FileHeader.getHeaderAsComment(`${this.data.vendor}_${this.data.module}`); 18 | 19 | return data; 20 | } 21 | 22 | public async generate(workspaceUri: Uri): Promise { 23 | const moduleDirectory = Magento.getModuleDirectory( 24 | this.data.vendor, 25 | this.data.module, 26 | workspaceUri 27 | ); 28 | 29 | return super.generate(moduleDirectory); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/generator/module/ModuleXmlGenerator.ts: -------------------------------------------------------------------------------- 1 | import GeneratedFile from 'generator/GeneratedFile'; 2 | import XmlGenerator from 'generator/XmlGenerator'; 3 | import { Uri } from 'vscode'; 4 | import { ModuleWizardComposerData, ModuleWizardData } from 'wizard/ModuleWizard'; 5 | import FileGenerator from '../FileGenerator'; 6 | import Magento from 'util/Magento'; 7 | 8 | export default class ModuleXmlGenerator extends FileGenerator { 9 | public constructor(protected data: ModuleWizardData | ModuleWizardComposerData) { 10 | super(); 11 | } 12 | 13 | public async generate(workspaceUri: Uri): Promise { 14 | const xmlContent = this.getXmlContent(); 15 | 16 | const moduleFile = Magento.getModuleDirectory( 17 | this.data.vendor, 18 | this.data.module, 19 | workspaceUri, 20 | 'etc/module.xml' 21 | ); 22 | 23 | return new GeneratedFile(moduleFile, xmlContent); 24 | } 25 | 26 | protected getXmlContent(): string { 27 | const moduleName = Magento.getModuleName(this.data.vendor, this.data.module); 28 | const xml: any = { 29 | '?xml': { 30 | '@_version': '1.0', 31 | }, 32 | config: { 33 | '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 34 | '@_xsi:noNamespaceSchemaLocation': 'urn:magento:framework:Module/etc/module.xsd', 35 | module: { 36 | '@_name': moduleName, 37 | }, 38 | }, 39 | }; 40 | 41 | if (this.data.sequence.length > 0) { 42 | xml.config.module.sequence = this.data.sequence.map(module => ({ 43 | module: { 44 | '@_name': module, 45 | }, 46 | })); 47 | } 48 | 49 | const xmlGenerator = new XmlGenerator(xml); 50 | return xmlGenerator.toString({ 51 | unpairedTags: ['module'], 52 | suppressUnpairedNode: false, 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/generator/observer/ObserverClassGenerator.ts: -------------------------------------------------------------------------------- 1 | import FileHeader from 'common/php/FileHeader'; 2 | import PhpNamespace from 'common/PhpNamespace'; 3 | import GeneratedFile from 'generator/GeneratedFile'; 4 | import FileGenerator from 'generator/FileGenerator'; 5 | import { PhpFile, PsrPrinter } from 'node-php-generator'; 6 | import { Uri } from 'vscode'; 7 | import { ObserverWizardData } from 'wizard/ObserverWizard'; 8 | import Magento from 'util/Magento'; 9 | 10 | export default class ObserverClassGenerator extends FileGenerator { 11 | private static readonly OBSERVER_INTERFACE = 'Magento\\Framework\\Event\\ObserverInterface'; 12 | private static readonly OBSERVER_CLASS = 'Magento\\Framework\\Event\\Observer'; 13 | 14 | public constructor(protected data: ObserverWizardData) { 15 | super(); 16 | } 17 | 18 | public async generate(workspaceUri: Uri): Promise { 19 | const [vendor, module] = this.data.module.split('_'); 20 | const pathParts = this.data.directoryPath.split('/'); 21 | const namespaceParts = [vendor, module, ...pathParts]; 22 | const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri); 23 | 24 | const phpFile = new PhpFile(); 25 | phpFile.setStrictTypes(true); 26 | 27 | const header = FileHeader.getHeader(this.data.module); 28 | 29 | if (header) { 30 | phpFile.addComment(header); 31 | } 32 | 33 | const namespace = phpFile.addNamespace(PhpNamespace.fromParts(namespaceParts).toString()); 34 | namespace.addUse(ObserverClassGenerator.OBSERVER_INTERFACE); 35 | namespace.addUse(ObserverClassGenerator.OBSERVER_CLASS); 36 | 37 | const observerClass = namespace.addClass(this.data.className); 38 | observerClass.addImplement(ObserverClassGenerator.OBSERVER_INTERFACE); 39 | 40 | const observerMethod = observerClass.addMethod('execute'); 41 | observerMethod.addParameter('observer').setType(ObserverClassGenerator.OBSERVER_CLASS); 42 | observerMethod.addComment(`Observer for "${this.data.eventName}"\n`); 43 | observerMethod.addComment('@param Observer $observer'); 44 | observerMethod.addComment('@return void'); 45 | observerMethod.setBody(`$event = $observer->getEvent();\n// TODO: Observer code`); 46 | 47 | const printer = new PsrPrinter(); 48 | 49 | return new GeneratedFile( 50 | Uri.joinPath(moduleDirectory, this.data.directoryPath, `${this.data.className}.php`), 51 | printer.printFile(phpFile) 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/generator/observer/ObserverEventsGenerator.ts: -------------------------------------------------------------------------------- 1 | import GeneratedFile from 'generator/GeneratedFile'; 2 | import FileGenerator from 'generator/FileGenerator'; 3 | import { Uri } from 'vscode'; 4 | import { ObserverWizardData } from 'wizard/ObserverWizard'; 5 | import indentString from 'indent-string'; 6 | import PhpNamespace from 'common/PhpNamespace'; 7 | import FindOrCreateEventsXml from 'generator/util/FindOrCreateEventsXml'; 8 | import Magento from 'util/Magento'; 9 | import HandlebarsTemplateRenderer from 'generator/HandlebarsTemplateRenderer'; 10 | import { TemplatePath } from 'types/handlebars'; 11 | 12 | export default class ObserverEventsGenerator extends FileGenerator { 13 | public constructor(protected data: ObserverWizardData) { 14 | super(); 15 | } 16 | 17 | public async generate(workspaceUri: Uri): Promise { 18 | const [vendor, module] = this.data.module.split('_'); 19 | const etcDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri, 'etc'); 20 | const observerNamespace = PhpNamespace.fromParts([vendor, module, this.data.directoryPath]); 21 | const eventsFile = Magento.getUriWithArea(etcDirectory, 'events.xml', this.data.area); 22 | const eventsXml = await FindOrCreateEventsXml.execute( 23 | workspaceUri, 24 | vendor, 25 | module, 26 | this.data.area 27 | ); 28 | const insertPosition = this.getInsertPosition(eventsXml); 29 | 30 | const renderer = new HandlebarsTemplateRenderer(); 31 | 32 | const observerXml = await renderer.render(TemplatePath.XmlEventsObserver, { 33 | name: this.data.observerName, 34 | className: observerNamespace.append(this.data.className).toString(), 35 | }); 36 | 37 | const eventXml = await renderer.render( 38 | TemplatePath.XmlEventsEvent, 39 | { 40 | eventName: this.data.eventName, 41 | }, 42 | { 43 | eventContent: observerXml, 44 | } 45 | ); 46 | 47 | const newEventsXml = 48 | eventsXml.slice(0, insertPosition) + 49 | '\n' + 50 | indentString(eventXml, 4) + 51 | '\n' + 52 | eventsXml.slice(insertPosition); 53 | 54 | return new GeneratedFile(eventsFile, newEventsXml, false); 55 | } 56 | 57 | private getInsertPosition(diXml: string): number { 58 | return diXml.indexOf(''); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/generator/preference/PreferenceClassGenerator.ts: -------------------------------------------------------------------------------- 1 | import FileHeader from 'common/php/FileHeader'; 2 | import GeneratedFile from 'generator/GeneratedFile'; 3 | import { PhpFile, PsrPrinter } from 'node-php-generator'; 4 | import { Uri } from 'vscode'; 5 | import { PreferenceWizardData } from 'wizard/PreferenceWizard'; 6 | import FileGenerator from 'generator/FileGenerator'; 7 | import Magento from 'util/Magento'; 8 | 9 | export default class PreferenceClassGenerator extends FileGenerator { 10 | public constructor(protected data: PreferenceWizardData) { 11 | super(); 12 | } 13 | 14 | public async generate(workspaceUri: Uri): Promise { 15 | const [vendor, module] = this.data.module.split('_'); 16 | const directoryParts = this.data.directory.split('/'); 17 | const namespaceParts = [vendor, module, ...directoryParts]; 18 | const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri); 19 | 20 | const header = FileHeader.getHeader(this.data.module); 21 | 22 | const phpFile = new PhpFile(); 23 | if (header) { 24 | phpFile.addComment(header); 25 | } 26 | phpFile.setStrictTypes(true); 27 | 28 | const namespace = phpFile.addNamespace(namespaceParts.join('\\')); 29 | 30 | const preferenceClass = namespace.addClass(this.data.className); 31 | 32 | if (this.data.inheritClass && this.data.parentClass) { 33 | preferenceClass.setExtends(this.data.parentClass); 34 | } 35 | 36 | const printer = new PsrPrinter(); 37 | 38 | return new GeneratedFile( 39 | Uri.joinPath(moduleDirectory, this.data.directory, `${this.data.className}.php`), 40 | printer.printFile(phpFile) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/generator/preference/PreferenceDiGenerator.ts: -------------------------------------------------------------------------------- 1 | import GeneratedFile from 'generator/GeneratedFile'; 2 | import FindOrCreateDiXml from 'generator/util/FindOrCreateDiXml'; 3 | import { Uri } from 'vscode'; 4 | import { PreferenceWizardData } from 'wizard/PreferenceWizard'; 5 | import indentString from 'indent-string'; 6 | import Magento from 'util/Magento'; 7 | import FileGenerator from 'generator/FileGenerator'; 8 | import HandlebarsTemplateRenderer from 'generator/HandlebarsTemplateRenderer'; 9 | import { TemplatePath } from 'types/handlebars'; 10 | 11 | export default class PreferenceDiGenerator extends FileGenerator { 12 | public constructor(protected data: PreferenceWizardData) { 13 | super(); 14 | } 15 | 16 | public async generate(workspaceUri: Uri): Promise { 17 | const [vendor, module] = this.data.module.split('_'); 18 | const directoryParts = this.data.directory.split('/'); 19 | const namespaceParts = [vendor, module, ...directoryParts, this.data.className]; 20 | const typeNamespace = namespaceParts.join('\\'); 21 | 22 | const etcDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri, 'etc'); 23 | const diFile = Magento.getUriWithArea(etcDirectory, 'di.xml', this.data.area); 24 | const diXml = await FindOrCreateDiXml.execute(workspaceUri, vendor, module, this.data.area); 25 | const insertPosition = this.getInsertPosition(diXml); 26 | 27 | const renderer = new HandlebarsTemplateRenderer(); 28 | 29 | const pluginXml = await renderer.render(TemplatePath.XmlDiPreference, { 30 | forClass: this.data.parentClass, 31 | typeClass: typeNamespace, 32 | }); 33 | 34 | const newDiXml = 35 | diXml.slice(0, insertPosition) + 36 | '\n' + 37 | indentString(pluginXml, 4) + 38 | '\n' + 39 | diXml.slice(insertPosition); 40 | 41 | return new GeneratedFile(diFile, newDiXml, false); 42 | } 43 | 44 | private getInsertPosition(diXml: string): number { 45 | return diXml.indexOf(''); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/generator/util/FindOrCreateCrontabXml.ts: -------------------------------------------------------------------------------- 1 | import Magento from 'util/Magento'; 2 | import { Uri } from 'vscode'; 3 | import { MagentoScope } from 'types/global'; 4 | import { TemplatePath } from 'types/handlebars'; 5 | import HandlebarsTemplateRenderer from 'generator/HandlebarsTemplateRenderer'; 6 | import FileSystem from 'util/FileSystem'; 7 | import FileHeader from 'common/xml/FileHeader'; 8 | 9 | export default class FindOrCreateCrontabXml { 10 | public static async execute( 11 | workspaceUri: Uri, 12 | vendor: string, 13 | module: string, 14 | area: MagentoScope = MagentoScope.Global 15 | ): Promise { 16 | const modulePath = Magento.getModuleDirectory(vendor, module, workspaceUri, 'etc'); 17 | const crontabFile = Magento.getUriWithArea(modulePath, 'crontab.xml', area); 18 | 19 | if (await FileSystem.fileExists(crontabFile)) { 20 | return await FileSystem.readFile(crontabFile); 21 | } 22 | 23 | const fileHeader = FileHeader.getHeader(module); 24 | 25 | const renderer = new HandlebarsTemplateRenderer(); 26 | 27 | return await renderer.render(TemplatePath.XmlBlankCrontab, { 28 | fileHeader, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/generator/util/FindOrCreateDiXml.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import FileSystem from 'util/FileSystem'; 3 | import FileHeader from 'common/xml/FileHeader'; 4 | import { MagentoScope } from 'types/global'; 5 | import Magento from 'util/Magento'; 6 | import HandlebarsTemplateRenderer from '../HandlebarsTemplateRenderer'; 7 | import { TemplatePath } from 'types/handlebars'; 8 | 9 | export default class FindOrCreateDiXml { 10 | public static async execute( 11 | workspaceUri: Uri, 12 | vendor: string, 13 | module: string, 14 | area: MagentoScope = MagentoScope.Global 15 | ): Promise { 16 | const modulePath = Magento.getModuleDirectory(vendor, module, workspaceUri, 'etc'); 17 | const diFile = Magento.getUriWithArea(modulePath, 'di.xml', area); 18 | 19 | if (await FileSystem.fileExists(diFile)) { 20 | return await FileSystem.readFile(diFile); 21 | } 22 | 23 | const fileHeader = FileHeader.getHeader(module); 24 | 25 | const renderer = new HandlebarsTemplateRenderer(); 26 | 27 | return await renderer.render(TemplatePath.XmlBlankDi, { 28 | fileHeader, 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/generator/util/FindOrCreateEventsXml.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import FileSystem from 'util/FileSystem'; 3 | import { MagentoScope } from 'types/global'; 4 | import FileHeader from 'common/xml/FileHeader'; 5 | import HandlebarsTemplateRenderer from '../HandlebarsTemplateRenderer'; 6 | import { TemplatePath } from 'types/handlebars'; 7 | 8 | export default class FindOrCreateEventsXml { 9 | public static async execute( 10 | workspaceUri: Uri, 11 | vendor: string, 12 | module: string, 13 | area: MagentoScope 14 | ): Promise { 15 | const areaPath = area === MagentoScope.Global ? '' : area; 16 | const eventsFile = Uri.joinPath( 17 | workspaceUri, 18 | 'app', 19 | 'code', 20 | vendor, 21 | module, 22 | 'etc', 23 | areaPath, 24 | 'events.xml' 25 | ); 26 | 27 | if (await FileSystem.fileExists(eventsFile)) { 28 | return await FileSystem.readFile(eventsFile); 29 | } 30 | 31 | const renderer = new HandlebarsTemplateRenderer(); 32 | 33 | const fileHeader = FileHeader.getHeader(`${vendor}_${module}`); 34 | 35 | return await renderer.render(TemplatePath.XmlBlankEvents, { 36 | fileHeader, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/generator/viewModel/ViewModelClassGenerator.ts: -------------------------------------------------------------------------------- 1 | import FileHeader from 'common/php/FileHeader'; 2 | import FileGenerator from 'generator/FileGenerator'; 3 | import GeneratedFile from 'generator/GeneratedFile'; 4 | import { PhpFile, PsrPrinter } from 'node-php-generator'; 5 | import Magento from 'util/Magento'; 6 | import { Uri } from 'vscode'; 7 | import { ViewModelWizardData } from 'wizard/ViewModelWizard'; 8 | 9 | export default class ViewModelClassGenerator extends FileGenerator { 10 | private static readonly ARGUMENT_INTERFACE = 11 | 'Magento\\Framework\\View\\Element\\Block\\ArgumentInterface'; 12 | 13 | public constructor(protected data: ViewModelWizardData) { 14 | super(); 15 | } 16 | 17 | public async generate(workspaceUri: Uri): Promise { 18 | const [vendor, module] = this.data.module.split('_'); 19 | const pathParts = this.data.directory.split('/'); 20 | const namespaceParts = [vendor, module, ...pathParts]; 21 | const moduleDirectory = Magento.getModuleDirectory(vendor, module, workspaceUri); 22 | 23 | const header = FileHeader.getHeader(this.data.module); 24 | 25 | const phpFile = new PhpFile(); 26 | if (header) { 27 | phpFile.addComment(header); 28 | } 29 | phpFile.setStrictTypes(true); 30 | 31 | const namespace = phpFile.addNamespace(namespaceParts.join('\\')); 32 | namespace.addUse(ViewModelClassGenerator.ARGUMENT_INTERFACE); 33 | 34 | const viewModelClass = namespace.addClass(this.data.className); 35 | 36 | viewModelClass.setImplements([ViewModelClassGenerator.ARGUMENT_INTERFACE]); 37 | 38 | const printer = new PsrPrinter(); 39 | 40 | return new GeneratedFile( 41 | Uri.joinPath(moduleDirectory, this.data.directory, `${this.data.className}.php`), 42 | printer.printFile(phpFile) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/hover/XmlClasslikeHoverProvider.ts: -------------------------------------------------------------------------------- 1 | import Config from 'common/Config'; 2 | import { ClasslikeInfo } from 'common/php/ClasslikeInfo'; 3 | import PhpDocumentParser from 'common/php/PhpDocumentParser'; 4 | import PhpNamespace from 'common/PhpNamespace'; 5 | import AutoloadNamespaceIndexer from 'indexer/autoload-namespace/AutoloadNamespaceIndexer'; 6 | import IndexManager from 'indexer/IndexManager'; 7 | import { Hover, HoverProvider, Position, Range, TextDocument } from 'vscode'; 8 | 9 | export default class XmlClasslikeHoverProvider implements HoverProvider { 10 | public async provideHover(document: TextDocument, position: Position): Promise { 11 | const provideXmlHovers = Config.get('provideXmlHovers'); 12 | 13 | if (!provideXmlHovers) { 14 | return null; 15 | } 16 | 17 | const range = document.getWordRangeAtPosition( 18 | position, 19 | /((?:\\{1,2}\w+|\w+\\{1,2})(?:\w+\\{0,2})+)/ 20 | ); 21 | 22 | if (!range) { 23 | return null; 24 | } 25 | 26 | const word = document.getText(range); 27 | 28 | const namespaceIndexData = IndexManager.getIndexData(AutoloadNamespaceIndexer.KEY); 29 | 30 | if (!namespaceIndexData) { 31 | return null; 32 | } 33 | 34 | const potentialNamespace = word.split(':').shift()?.trim(); 35 | 36 | if (!potentialNamespace) { 37 | return null; 38 | } 39 | 40 | const classUri = await namespaceIndexData.findUriByNamespace( 41 | PhpNamespace.fromString(potentialNamespace) 42 | ); 43 | 44 | if (!classUri) { 45 | return null; 46 | } 47 | 48 | const phpFile = await PhpDocumentParser.parseUri(document, classUri); 49 | const classLikeInfo = new ClasslikeInfo(phpFile); 50 | 51 | return new Hover(classLikeInfo.getHover(), range); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/hover/XmlHoverProviderProcessor.ts: -------------------------------------------------------------------------------- 1 | import { CancellationToken, Hover, Position, TextDocument } from 'vscode'; 2 | import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor'; 3 | import { AclHoverProvider } from 'hover/xml/AclHoverProvider'; 4 | import { ModuleHoverProvider } from 'hover/xml/ModuleHoverProvider'; 5 | import { CronHoverProvider } from 'hover/xml/CronHoverProvider'; 6 | 7 | export class XmlHoverProviderProcessor extends XmlSuggestionProviderProcessor { 8 | public constructor() { 9 | super([new AclHoverProvider(), new ModuleHoverProvider(), new CronHoverProvider()]); 10 | } 11 | 12 | public async provideHover( 13 | document: TextDocument, 14 | position: Position, 15 | token: CancellationToken 16 | ): Promise { 17 | const suggestions = await this.provideSuggestions(document, position, token); 18 | 19 | const suggestion = suggestions.length > 0 ? suggestions[0] : null; 20 | 21 | if (!suggestion) { 22 | return null; 23 | } 24 | 25 | return suggestion; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/hover/xml/AclHoverProvider.ts: -------------------------------------------------------------------------------- 1 | import { Hover, MarkdownString, Uri, Range, TextDocument } from 'vscode'; 2 | import AclIndexer from 'indexer/acl/AclIndexer'; 3 | import IndexManager from 'indexer/IndexManager'; 4 | import { CombinedCondition, XmlSuggestionProvider } from 'common/xml/XmlSuggestionProvider'; 5 | import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; 6 | import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; 7 | import { XMLElement, XMLAttribute } from '@xml-tools/ast'; 8 | 9 | export class AclHoverProvider extends XmlSuggestionProvider { 10 | public getAttributeValueConditions(): CombinedCondition[] { 11 | return [ 12 | [new ElementNameMatches('resource'), new AttributeNameMatches('id')], 13 | [new ElementNameMatches('resource'), new AttributeNameMatches('ref')], 14 | ]; 15 | } 16 | 17 | public getElementContentMatches(): CombinedCondition[] { 18 | return [[new ElementNameMatches('resource')]]; 19 | } 20 | 21 | public getConfigKey(): string | undefined { 22 | return 'provideXmlHovers'; 23 | } 24 | 25 | public getFilePatterns(): string[] { 26 | return ['**/etc/acl.xml', '**/etc/webapi.xml', '**/etc/**/system.xml']; 27 | } 28 | 29 | public getSuggestionItems( 30 | value: string, 31 | range: Range, 32 | document: TextDocument, 33 | element: XMLElement, 34 | attribute?: XMLAttribute 35 | ): Hover[] { 36 | const aclIndexData = IndexManager.getIndexData(AclIndexer.KEY); 37 | 38 | if (!aclIndexData) { 39 | return []; 40 | } 41 | 42 | const acl = aclIndexData.getAcl(value); 43 | 44 | if (!acl) { 45 | return []; 46 | } 47 | 48 | const markdown = new MarkdownString(); 49 | markdown.appendMarkdown(`**ACL**: ${acl.title}\n\n`); 50 | markdown.appendMarkdown(`- ID: \`${acl.id}\`\n\n`); 51 | 52 | if (acl.description) { 53 | markdown.appendMarkdown(`${acl.description}\n\n`); 54 | } 55 | 56 | if (acl.disabled) { 57 | markdown.appendMarkdown(`- **Disabled**\n\n`); 58 | } 59 | 60 | if (acl.sortOrder) { 61 | markdown.appendMarkdown(`- Sort Order: ${acl.sortOrder}\n\n`); 62 | } 63 | 64 | if (acl.parent) { 65 | markdown.appendMarkdown(`- Parent ID: \`${acl.parent}\`\n\n`); 66 | } 67 | 68 | markdown.appendMarkdown(`[acl.xml](${Uri.file(acl.path)})`); 69 | 70 | return [new Hover(markdown, range)]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/hover/xml/CronHoverProvider.ts: -------------------------------------------------------------------------------- 1 | import { Hover, MarkdownString, Range } from 'vscode'; 2 | import { CombinedCondition, XmlSuggestionProvider } from 'common/xml/XmlSuggestionProvider'; 3 | import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; 4 | import cronstrue from 'cronstrue'; 5 | 6 | export class CronHoverProvider extends XmlSuggestionProvider { 7 | public getElementContentMatches(): CombinedCondition[] { 8 | return [[new ElementNameMatches('schedule')]]; 9 | } 10 | 11 | public getConfigKey(): string | undefined { 12 | return 'provideXmlHovers'; 13 | } 14 | 15 | public getFilePatterns(): string[] { 16 | return ['**/etc/crontab.xml']; 17 | } 18 | 19 | public getSuggestionItems(value: string, range: Range): Hover[] { 20 | const readable = cronstrue.toString(value); 21 | 22 | if (!readable) { 23 | return []; 24 | } 25 | 26 | const markdown = new MarkdownString(); 27 | markdown.appendMarkdown(`**Cron**: ${readable}`); 28 | 29 | return [new Hover(markdown, range)]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/indexer/AbstractIndexData.ts: -------------------------------------------------------------------------------- 1 | import { Memoize } from 'typescript-memoize'; 2 | import { IndexedFilePath } from 'types/indexer'; 3 | 4 | export abstract class AbstractIndexData { 5 | public constructor(protected data: Map) {} 6 | 7 | @Memoize() 8 | public getValues(): T[] { 9 | return Array.from(this.data.values()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/indexer/IndexDataSerializer.ts: -------------------------------------------------------------------------------- 1 | import { SavedIndex } from 'types/indexer'; 2 | 3 | export class IndexDataSerializer { 4 | public serialize(data: SavedIndex): string { 5 | return JSON.stringify(data, this.replacer); 6 | } 7 | 8 | public deserialize(data: string): SavedIndex { 9 | return JSON.parse(data, this.reviver); 10 | } 11 | 12 | private replacer(key: string, value: any) { 13 | if (value instanceof Map) { 14 | return { __type: 'Map', value: Array.from(value.entries()) }; 15 | } 16 | return value; 17 | } 18 | 19 | private reviver(key: string, value: any) { 20 | if (value && typeof value === 'object' && value.__type === 'Map') { 21 | return new Map(value.value); 22 | } 23 | return value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/indexer/IndexRunner.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import IndexManager from './IndexManager'; 3 | import ExtensionState from 'common/ExtensionState'; 4 | import Common from 'util/Common'; 5 | 6 | class IndexRunner { 7 | public async indexWorkspace(force: boolean = false): Promise { 8 | if (ExtensionState.magentoWorkspaces.length === 0) { 9 | return; 10 | } 11 | 12 | for (const workspaceFolder of ExtensionState.magentoWorkspaces) { 13 | await vscode.window.withProgress( 14 | { 15 | location: vscode.ProgressLocation.Window, 16 | title: `Magento Toolbox v${Common.getVersion()}`, 17 | }, 18 | async progress => { 19 | await IndexManager.indexWorkspace(workspaceFolder, progress, force); 20 | } 21 | ); 22 | } 23 | } 24 | 25 | public async indexFile(workspaceFolder: vscode.WorkspaceFolder, file: vscode.Uri): Promise { 26 | await IndexManager.indexFile(workspaceFolder, file); 27 | } 28 | 29 | public async indexFiles( 30 | workspaceFolder: vscode.WorkspaceFolder, 31 | files: vscode.Uri[] 32 | ): Promise { 33 | await IndexManager.indexFiles(workspaceFolder, files); 34 | } 35 | } 36 | 37 | export default new IndexRunner(); 38 | -------------------------------------------------------------------------------- /src/indexer/Indexer.ts: -------------------------------------------------------------------------------- 1 | import { type GlobPattern, type Uri } from 'vscode'; 2 | import { IndexerKey } from 'types/indexer'; 3 | 4 | export abstract class Indexer { 5 | public abstract getId(): IndexerKey; 6 | public abstract getName(): string; 7 | public abstract getPattern(uri: Uri): GlobPattern; 8 | public abstract indexFile(uri: Uri): Promise; 9 | public abstract getVersion(): number; 10 | } 11 | -------------------------------------------------------------------------------- /src/indexer/acl/AclIndexData.ts: -------------------------------------------------------------------------------- 1 | import { Memoize } from 'typescript-memoize'; 2 | import { Acl } from './types'; 3 | import { AbstractIndexData } from 'indexer/AbstractIndexData'; 4 | import AclIndexer from './AclIndexer'; 5 | 6 | export class AclIndexData extends AbstractIndexData { 7 | @Memoize({ 8 | tags: [AclIndexer.KEY], 9 | }) 10 | public getAcls(): Acl[] { 11 | return Array.from(this.data.values()).flat(); 12 | } 13 | 14 | public getAcl(aclId: string): Acl | undefined { 15 | return this.getAcls().find(acl => acl.id === aclId); 16 | } 17 | 18 | public getAclsByPrefix(prefix: string): Acl[] { 19 | return this.getAcls().filter(acl => acl.id.startsWith(prefix)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/indexer/acl/types.ts: -------------------------------------------------------------------------------- 1 | export interface Acl { 2 | id: string; 3 | path: string; 4 | title: string; 5 | description?: string; 6 | sortOrder?: number; 7 | disabled?: boolean; 8 | parent?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/indexer/autoload-namespace/AutoloadNamespaceIndexData.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import PhpNamespace from 'common/PhpNamespace'; 3 | import { AbstractIndexData } from 'indexer/AbstractIndexData'; 4 | import { Memoize } from 'typescript-memoize'; 5 | import AutoloadNamespaceIndexer from './AutoloadNamespaceIndexer'; 6 | import { Namespace } from './types'; 7 | 8 | export class AutoloadNamespaceIndexData extends AbstractIndexData { 9 | private static readonly SPECIAL_CLASSNAMES = ['Proxy', 'Factory']; 10 | 11 | @Memoize({ 12 | tags: [AutoloadNamespaceIndexer.KEY], 13 | }) 14 | public getNamespaces(): Namespace[] { 15 | return Array.from(this.data.values()).flat(); 16 | } 17 | 18 | @Memoize({ 19 | tags: [AutoloadNamespaceIndexer.KEY], 20 | hashFunction: (namespace: PhpNamespace) => namespace.toString(), 21 | }) 22 | public async findUriByNamespace(phpNamespace: PhpNamespace): Promise { 23 | const namespaces = this.getNamespaces(); 24 | 25 | if (AutoloadNamespaceIndexData.SPECIAL_CLASSNAMES.includes(phpNamespace.getTail())) { 26 | phpNamespace.pop(); 27 | } 28 | 29 | const namespace = namespaces.find(n => n.fqn === phpNamespace.toString()); 30 | 31 | if (!namespace) { 32 | return undefined; 33 | } 34 | 35 | return Uri.file(namespace.path); 36 | } 37 | 38 | public findNamespacesByPrefix(prefix: string): Namespace[] { 39 | const namespaces = this.getNamespaces(); 40 | return namespaces.filter(namespace => namespace.fqn.startsWith(prefix)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/indexer/autoload-namespace/types.ts: -------------------------------------------------------------------------------- 1 | export interface Namespace { 2 | fqn: string; 3 | prefix: string; 4 | baseDirectory: string; 5 | path: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/indexer/cron/CronIndexData.ts: -------------------------------------------------------------------------------- 1 | import { Memoize } from 'typescript-memoize'; 2 | import { Job } from './types'; 3 | import { AbstractIndexData } from 'indexer/AbstractIndexData'; 4 | import CronIndexer from './CronIndexer'; 5 | 6 | export class CronIndexData extends AbstractIndexData { 7 | @Memoize({ 8 | tags: [CronIndexer.KEY], 9 | }) 10 | public getJobs(): Job[] { 11 | return this.getValues().flatMap(data => data); 12 | } 13 | 14 | public findJobByName(group: string, name: string): Job | undefined { 15 | return this.getJobs().find(job => job.group === group && job.name === name); 16 | } 17 | 18 | public findJobsByGroup(group: string): Job[] { 19 | return this.getJobs().filter(job => job.group === group); 20 | } 21 | 22 | public findJobsByInstance(instance: string): Job[] { 23 | return this.getJobs().filter(job => job.instance === instance); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/indexer/cron/CronIndexer.ts: -------------------------------------------------------------------------------- 1 | import { RelativePattern, Uri } from 'vscode'; 2 | import { XMLParser } from 'fast-xml-parser'; 3 | import { get } from 'lodash-es'; 4 | import FileSystem from 'util/FileSystem'; 5 | import { Job } from './types'; 6 | import { Indexer } from 'indexer/Indexer'; 7 | import { IndexerKey } from 'types/indexer'; 8 | 9 | export default class CronIndexer extends Indexer { 10 | public static readonly KEY = 'cron'; 11 | 12 | protected xmlParser: XMLParser; 13 | 14 | public constructor() { 15 | super(); 16 | 17 | this.xmlParser = new XMLParser({ 18 | ignoreAttributes: false, 19 | attributeNamePrefix: '@_', 20 | isArray: (_name, jpath) => { 21 | return ['config.group', 'config.group.job'].includes(jpath); 22 | }, 23 | }); 24 | } 25 | 26 | public getVersion(): number { 27 | return 2; 28 | } 29 | 30 | public getId(): IndexerKey { 31 | return CronIndexer.KEY; 32 | } 33 | 34 | public getName(): string { 35 | return 'crontab.xml'; 36 | } 37 | 38 | public getPattern(uri: Uri): RelativePattern { 39 | return new RelativePattern(uri, '**/etc/crontab.xml'); 40 | } 41 | 42 | public async indexFile(uri: Uri): Promise { 43 | const xml = await FileSystem.readFile(uri); 44 | const parsed = this.xmlParser.parse(xml); 45 | const config = get(parsed, 'config', {}); 46 | 47 | const data: Job[] = []; 48 | 49 | // Index groups 50 | const groups = get(config, 'group', []); 51 | 52 | for (const group of groups) { 53 | const jobs = get(group, 'job', []); 54 | 55 | for (const job of jobs) { 56 | data.push({ 57 | name: job['@_name'], 58 | instance: job['@_instance'], 59 | method: job['@_method'], 60 | schedule: job['schedule'], 61 | config_path: job['config_path'], 62 | path: uri.fsPath, 63 | group: group['@_id'], 64 | }); 65 | } 66 | } 67 | 68 | return data; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/indexer/cron/types.ts: -------------------------------------------------------------------------------- 1 | export interface Job { 2 | name: string; 3 | instance: string; 4 | method: string; 5 | schedule?: string; 6 | config_path?: string; 7 | path: string; 8 | group: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/indexer/di/DiIndexData.ts: -------------------------------------------------------------------------------- 1 | import { Memoize } from 'typescript-memoize'; 2 | import { DiData, DiPlugin, DiPreference, DiType, DiVirtualType } from './types'; 3 | import { AbstractIndexData } from 'indexer/AbstractIndexData'; 4 | import DiIndexer from './DiIndexer'; 5 | 6 | export class DiIndexData extends AbstractIndexData { 7 | @Memoize({ 8 | tags: [DiIndexer.KEY], 9 | }) 10 | public getTypes(): DiType[] { 11 | return this.getValues().flatMap(data => data.types); 12 | } 13 | 14 | @Memoize({ 15 | tags: [DiIndexer.KEY], 16 | }) 17 | public getPreferences(): DiPreference[] { 18 | return this.getValues().flatMap(data => data.preferences); 19 | } 20 | 21 | @Memoize({ 22 | tags: [DiIndexer.KEY], 23 | }) 24 | public getVirtualTypes(): DiVirtualType[] { 25 | return this.getValues().flatMap(data => data.virtualTypes); 26 | } 27 | 28 | public findTypesByName(name: string): DiType[] { 29 | return this.getTypes().filter(type => type.name === name); 30 | } 31 | 32 | public findVirtualTypeByName(name: string): DiVirtualType | undefined { 33 | return this.getVirtualTypes().find(type => type.name === name); 34 | } 35 | 36 | public findPreferencesByType(type: string): DiPreference[] { 37 | return this.getPreferences().filter(pref => pref.for === type); 38 | } 39 | 40 | public findPluginsForType(type: string): DiPlugin[] { 41 | const typeData = this.findTypesByName(type); 42 | 43 | return typeData.flatMap(type => type.plugins); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/indexer/di/types.ts: -------------------------------------------------------------------------------- 1 | export interface DiData { 2 | types: DiType[]; 3 | preferences: DiPreference[]; 4 | virtualTypes: DiVirtualType[]; 5 | plugins: DiPlugin[]; 6 | } 7 | 8 | export interface DiPlugin { 9 | name: string; 10 | type: string; 11 | before?: string; 12 | after?: string; 13 | disabled?: boolean; 14 | sortOrder?: number; 15 | diPath: string; 16 | } 17 | 18 | export interface DiArguments { 19 | [key: string]: any; 20 | } 21 | 22 | export interface DiPreference { 23 | for: string; 24 | type: string; 25 | diPath: string; 26 | } 27 | 28 | export interface DiBaseType { 29 | name: string; 30 | shared?: boolean; 31 | arguments?: DiArguments; 32 | diPath: string; 33 | } 34 | 35 | export interface DiType extends DiBaseType { 36 | plugins: DiPlugin[]; 37 | } 38 | 39 | export interface DiVirtualType extends DiBaseType { 40 | type: string; 41 | } 42 | -------------------------------------------------------------------------------- /src/indexer/events/EventsIndexData.ts: -------------------------------------------------------------------------------- 1 | import { Memoize } from 'typescript-memoize'; 2 | import EventsIndexer from './EventsIndexer'; 3 | import { Event } from './types'; 4 | import { AbstractIndexData } from 'indexer/AbstractIndexData'; 5 | import { uniqBy } from 'lodash-es'; 6 | 7 | export class EventsIndexData extends AbstractIndexData { 8 | @Memoize({ tags: [EventsIndexer.KEY] }) 9 | public getEvents(): Event[] { 10 | return this.getValues().flat(); 11 | } 12 | 13 | public getEventNames(): string[] { 14 | return this.getEvents().map(event => event.name); 15 | } 16 | 17 | public getEventByName(eventName: string): Event | undefined { 18 | return this.getEvents().find(event => event.name === eventName); 19 | } 20 | 21 | public getEventsByPrefix(prefix: string): Event[] { 22 | const events = this.getEvents().filter(event => event.name.startsWith(prefix)); 23 | return uniqBy(events, 'name'); 24 | } 25 | 26 | public findEventsByObserverInstance(observerInstance: string): Event[] { 27 | return this.getEvents().filter(event => 28 | event.observers.some(observer => observer.instance === observerInstance) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/indexer/events/EventsIndexer.ts: -------------------------------------------------------------------------------- 1 | import { RelativePattern, Uri } from 'vscode'; 2 | import { XMLParser } from 'fast-xml-parser'; 3 | import { get } from 'lodash-es'; 4 | import { Indexer } from 'indexer/Indexer'; 5 | import FileSystem from 'util/FileSystem'; 6 | import { IndexerKey } from 'types/indexer'; 7 | 8 | export default class EventsIndexer extends Indexer { 9 | public static readonly KEY = 'events'; 10 | 11 | private xmlParser: XMLParser; 12 | 13 | public constructor() { 14 | super(); 15 | 16 | this.xmlParser = new XMLParser({ 17 | ignoreAttributes: false, 18 | attributeNamePrefix: '@_', 19 | isArray: (name, jpath) => { 20 | return ['config.event', 'config.event.observer'].includes(jpath); 21 | }, 22 | }); 23 | } 24 | 25 | public getVersion(): number { 26 | return 1; 27 | } 28 | 29 | public getId(): IndexerKey { 30 | return EventsIndexer.KEY; 31 | } 32 | 33 | public getName(): string { 34 | return 'events.xml'; 35 | } 36 | 37 | public getPattern(uri: Uri): RelativePattern { 38 | return new RelativePattern(uri, '**/etc/events.xml'); 39 | } 40 | 41 | public async indexFile(uri: Uri): Promise { 42 | const xml = await FileSystem.readFile(uri); 43 | 44 | const parsed = this.xmlParser.parse(xml); 45 | 46 | const events = get(parsed, 'config.event', []); 47 | 48 | return events.map((event: any) => ({ 49 | name: event['@_name'], 50 | diPath: uri.fsPath, 51 | observers: 52 | event.observer?.map((observer: any) => ({ 53 | name: observer['@_name'], 54 | instance: observer['@_instance'], 55 | })) || [], 56 | })); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/indexer/events/types.ts: -------------------------------------------------------------------------------- 1 | export interface Event { 2 | name: string; 3 | observers: Observer[]; 4 | diPath: string; 5 | } 6 | 7 | export interface Observer { 8 | name: string; 9 | instance: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/indexer/module/ModuleIndexData.ts: -------------------------------------------------------------------------------- 1 | import { WizardSelectOption } from 'types/webview'; 2 | import { Module } from './types'; 3 | import { Uri } from 'vscode'; 4 | import { AbstractIndexData } from 'indexer/AbstractIndexData'; 5 | 6 | export class ModuleIndexData extends AbstractIndexData { 7 | public getModuleOptions(filter?: (module: Module) => boolean): WizardSelectOption[] { 8 | return this.getValues() 9 | .filter(module => !filter || filter(module)) 10 | .map(module => ({ 11 | label: module.name, 12 | value: module.name, 13 | })); 14 | } 15 | 16 | public getModule(name: string, caseSensitive = false): Module | undefined { 17 | return this.getValues().find(module => 18 | caseSensitive ? module.name === name : module.name.toLowerCase() === name.toLowerCase() 19 | ); 20 | } 21 | 22 | public getModulesByPrefix(prefix: string): Module[] { 23 | return this.getValues().filter(module => module.name.startsWith(prefix)); 24 | } 25 | 26 | public getModuleByUri(uri: Uri, appOnly = true): Module | undefined { 27 | const module = this.getValues().find(module => { 28 | return uri.fsPath.startsWith(module.path) && (!appOnly || module.location === 'app'); 29 | }); 30 | 31 | return module; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/indexer/module/ModuleIndexer.ts: -------------------------------------------------------------------------------- 1 | import { RelativePattern, Uri } from 'vscode'; 2 | import { XMLParser } from 'fast-xml-parser'; 3 | import { get } from 'lodash-es'; 4 | import { Module } from './types'; 5 | import { Indexer } from 'indexer/Indexer'; 6 | import FileSystem from 'util/FileSystem'; 7 | import { IndexerKey } from 'types/indexer'; 8 | 9 | export default class ModuleIndexer extends Indexer { 10 | public static readonly KEY = 'module'; 11 | 12 | private xmlParser: XMLParser; 13 | 14 | public constructor() { 15 | super(); 16 | 17 | this.xmlParser = new XMLParser({ 18 | ignoreAttributes: false, 19 | attributeNamePrefix: '@_', 20 | isArray: (name, jpath) => { 21 | return jpath === 'config.module.sequence.module'; 22 | }, 23 | }); 24 | } 25 | 26 | public getVersion(): number { 27 | return 1; 28 | } 29 | 30 | public getId(): IndexerKey { 31 | return ModuleIndexer.KEY; 32 | } 33 | 34 | public getName(): string { 35 | return 'module.xml'; 36 | } 37 | 38 | public getPattern(uri: Uri): RelativePattern { 39 | return new RelativePattern(uri, '**/etc/module.xml'); 40 | } 41 | 42 | public async indexFile(uri: Uri): Promise { 43 | const xml = await FileSystem.readFile(uri); 44 | 45 | const parsed = this.xmlParser.parse(xml); 46 | 47 | const moduleName = get(parsed, 'config.module.@_name'); 48 | const setupVersion = get(parsed, 'config.module.@_setup_version'); 49 | const sequence = get(parsed, 'config.module.sequence.module', []); 50 | 51 | return { 52 | name: moduleName, 53 | version: setupVersion, 54 | sequence: sequence.map((module: any) => module['@_name']), 55 | moduleXmlPath: uri.fsPath, 56 | path: Uri.joinPath(uri, '..', '..').fsPath, 57 | location: uri.fsPath.includes('vendor') ? 'vendor' : 'app', 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/indexer/module/types.ts: -------------------------------------------------------------------------------- 1 | export interface Module { 2 | name: string; 3 | version?: string; 4 | sequence: string[]; 5 | path: string; 6 | moduleXmlPath: string; 7 | location: 'vendor' | 'app'; 8 | } 9 | -------------------------------------------------------------------------------- /src/indexer/template/TemplateIndexData.ts: -------------------------------------------------------------------------------- 1 | import { Memoize } from 'typescript-memoize'; 2 | import { Template } from './types'; 3 | import { AbstractIndexData } from 'indexer/AbstractIndexData'; 4 | import TemplateIndexer from './TemplateIndexer'; 5 | 6 | export class TemplateIndexData extends AbstractIndexData { 7 | @Memoize({ 8 | tags: [TemplateIndexer.KEY], 9 | }) 10 | public getTemplates(): Template[] { 11 | return Array.from(this.data.values()).flat(); 12 | } 13 | 14 | public getTemplate(magentoPath: string): Template | undefined { 15 | return this.getTemplates().find(template => template.magentoPath === magentoPath); 16 | } 17 | 18 | public getTemplatesByPrefix(prefix: string): Template[] { 19 | return this.getTemplates().filter(template => template.magentoPath.startsWith(prefix)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/indexer/template/TemplateIndexer.ts: -------------------------------------------------------------------------------- 1 | import { RelativePattern, Uri } from 'vscode'; 2 | import { Indexer } from 'indexer/Indexer'; 3 | import { IndexerKey } from 'types/indexer'; 4 | import { Template } from './types'; 5 | import GetMagentoPath from 'common/GetMagentoPath'; 6 | 7 | export default class TemplateIndexer extends Indexer { 8 | public static readonly KEY = 'template'; 9 | 10 | public getVersion(): number { 11 | return 1; 12 | } 13 | 14 | public getId(): IndexerKey { 15 | return TemplateIndexer.KEY; 16 | } 17 | 18 | public getName(): string { 19 | return 'templates'; 20 | } 21 | 22 | public getPattern(uri: Uri): RelativePattern { 23 | return new RelativePattern(uri, '**/view/**/templates/**/*.phtml'); 24 | } 25 | 26 | public async indexFile(uri: Uri): Promise { 27 | const magentoPath = GetMagentoPath.getMagentoPath(uri); 28 | 29 | if (!magentoPath) { 30 | return []; 31 | } 32 | 33 | return [ 34 | { 35 | path: uri.fsPath, 36 | magentoPath, 37 | }, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/indexer/template/types.ts: -------------------------------------------------------------------------------- 1 | export interface Template { 2 | path: string; 3 | magentoPath: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/observer/ActiveTextEditorChangeObserver.ts: -------------------------------------------------------------------------------- 1 | import { TextEditor } from 'vscode'; 2 | import Observer from './Observer'; 3 | import PluginClassDecorationProvider from 'decorator/PluginClassDecorationProvider'; 4 | import Context from 'common/Context'; 5 | import ObserverInstanceDecorationProvider from 'decorator/ObserverInstanceDecorationProvider'; 6 | import CronClassDecorationProvider from 'decorator/CronClassDecorationProvider'; 7 | 8 | export default class ActiveTextEditorChangeObserver extends Observer { 9 | public async execute(textEditor: TextEditor | undefined): Promise { 10 | await Context.updateContext('editor', textEditor); 11 | 12 | if (textEditor && textEditor.document.languageId === 'php') { 13 | const pluginProvider = new PluginClassDecorationProvider(textEditor.document); 14 | const observerProvider = new ObserverInstanceDecorationProvider(textEditor.document); 15 | const cronProvider = new CronClassDecorationProvider(textEditor.document); 16 | 17 | const [pluginDecorations, observerDecorations, cronDecorations] = await Promise.all([ 18 | pluginProvider.getDecorations(), 19 | observerProvider.getDecorations(), 20 | cronProvider.getDecorations(), 21 | ]); 22 | 23 | textEditor.setDecorations(pluginProvider.getType(), pluginDecorations); 24 | textEditor.setDecorations(observerProvider.getType(), observerDecorations); 25 | textEditor.setDecorations(cronProvider.getType(), cronDecorations); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/observer/ChangeTextEditorSelectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { TextEditorSelectionChangeEvent } from 'vscode'; 2 | import Observer from './Observer'; 3 | import Context from 'common/Context'; 4 | 5 | export default class ChangeTextEditorSelectionObserver extends Observer { 6 | public async execute(e: TextEditorSelectionChangeEvent): Promise { 7 | await Context.updateContext('selection', e.textEditor); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/observer/Observer.ts: -------------------------------------------------------------------------------- 1 | export default abstract class Observer { 2 | public abstract execute(...args: any[]): void; 3 | } 4 | -------------------------------------------------------------------------------- /src/parser/php/Parser.ts: -------------------------------------------------------------------------------- 1 | import * as php from 'php-parser'; 2 | import { PhpFile } from './PhpFile'; 3 | import { TextDocument, Uri, workspace } from 'vscode'; 4 | 5 | export enum NodeKind { 6 | Program = 'program', 7 | Namespace = 'namespace', 8 | Class = 'class', 9 | Interface = 'interface', 10 | UseGroup = 'usegroup', 11 | UseItem = 'useitem', 12 | Method = 'method', 13 | Call = 'call', 14 | String = 'string', 15 | } 16 | 17 | export type KindType = K extends NodeKind.Program 18 | ? php.Program 19 | : K extends NodeKind.Namespace 20 | ? php.Namespace 21 | : K extends NodeKind.Class 22 | ? php.Class 23 | : K extends NodeKind.Method 24 | ? php.Method 25 | : K extends NodeKind.UseGroup 26 | ? php.UseGroup 27 | : K extends NodeKind.UseItem 28 | ? php.UseItem 29 | : K extends NodeKind.Interface 30 | ? php.Interface 31 | : never; 32 | 33 | export default class PhpParser { 34 | private parser: php.Engine; 35 | 36 | public constructor() { 37 | this.parser = new php.Engine({ 38 | ast: { 39 | withPositions: true, 40 | }, 41 | parser: { 42 | extractDoc: true, 43 | }, 44 | }); 45 | } 46 | 47 | public async parse(uri: Uri) { 48 | const code = await workspace.fs.readFile(uri); 49 | const ast = this.parser.parseCode(code.toString(), uri.fsPath); 50 | return new PhpFile(ast, uri); 51 | } 52 | 53 | public async parseDocument(document: TextDocument) { 54 | const code = document.getText(); 55 | const ast = this.parser.parseCode(code, document.uri.fsPath); 56 | return new PhpFile(ast, document.uri); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/parser/php/PhpClass.ts: -------------------------------------------------------------------------------- 1 | import { Class } from 'php-parser'; 2 | import { NodeKind } from './Parser'; 3 | import { PhpNode } from './PhpNode'; 4 | import { PhpFile } from './PhpFile'; 5 | import { PhpMethod } from './PhpMethod'; 6 | 7 | export class PhpClass extends PhpNode { 8 | constructor( 9 | ast: Class, 10 | public parent: PhpFile 11 | ) { 12 | super(ast); 13 | } 14 | 15 | public get name() { 16 | return this.getIdentifierName(this.ast.name); 17 | } 18 | 19 | public get namespace() { 20 | return this.parent.namespace; 21 | } 22 | 23 | public get methods(): PhpMethod[] { 24 | return this.searchAst(NodeKind.Method).map(ast => new PhpMethod(ast, this)); 25 | } 26 | 27 | public get extends(): string | undefined { 28 | if (!this.ast.extends) { 29 | return; 30 | } 31 | 32 | return this.getIdentifierName(this.ast.extends); 33 | } 34 | 35 | public get implements(): string[] { 36 | return this.ast.implements?.map(impl => this.getIdentifierName(impl)) ?? []; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/parser/php/PhpFile.ts: -------------------------------------------------------------------------------- 1 | import { Program } from 'php-parser'; 2 | import { first } from 'lodash-es'; 3 | import { Uri } from 'vscode'; 4 | import { PhpNode } from './PhpNode'; 5 | import { NodeKind } from './Parser'; 6 | import { PhpClass } from './PhpClass'; 7 | import { PhpInterface } from './PhpInterface'; 8 | import { PhpUseItem } from './PhpUseItem'; 9 | 10 | export class PhpFile extends PhpNode { 11 | constructor( 12 | ast: Program, 13 | public readonly uri: Uri 14 | ) { 15 | super(ast); 16 | } 17 | 18 | public get namespace() { 19 | const namespace = first(this.searchAst(NodeKind.Namespace)); 20 | 21 | if (!namespace) { 22 | throw new Error('No namespace found'); 23 | } 24 | 25 | return namespace.name; 26 | } 27 | 28 | public get classes() { 29 | return this.searchAst(NodeKind.Class).map(ast => new PhpClass(ast, this)); 30 | } 31 | 32 | public get interfaces() { 33 | return this.searchAst(NodeKind.Interface).map(ast => new PhpInterface(ast, this)); 34 | } 35 | 36 | public get useItems() { 37 | return this.searchAst(NodeKind.UseItem).map(ast => new PhpUseItem(ast, this)); 38 | } 39 | 40 | public get comments(): string[] { 41 | return this.ast.comments?.map(comment => comment.value) ?? []; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/parser/php/PhpInterface.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from 'php-parser'; 2 | import { NodeKind } from './Parser'; 3 | import { PhpNode } from './PhpNode'; 4 | import { PhpFile } from './PhpFile'; 5 | import { PhpMethod } from './PhpMethod'; 6 | 7 | export class PhpInterface extends PhpNode { 8 | constructor( 9 | ast: Interface, 10 | public parent: PhpFile 11 | ) { 12 | super(ast); 13 | } 14 | 15 | public get name() { 16 | return this.getIdentifierName(this.ast.name); 17 | } 18 | 19 | public get namespace() { 20 | return this.parent.namespace; 21 | } 22 | 23 | public get methods(): PhpMethod[] { 24 | return this.searchAst(NodeKind.Method).map(ast => new PhpMethod(ast, this)); 25 | } 26 | 27 | public get extends(): string[] { 28 | return this.ast.extends?.map(ext => this.getIdentifierName(ext)) ?? []; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/parser/php/PhpMethod.ts: -------------------------------------------------------------------------------- 1 | import { Method } from 'php-parser'; 2 | import { NodeKind } from './Parser'; 3 | import { PhpClass } from './PhpClass'; 4 | import { PhpNode } from './PhpNode'; 5 | import { PhpInterface } from './PhpInterface'; 6 | 7 | export class PhpMethod extends PhpNode { 8 | constructor( 9 | ast: Method, 10 | public parent: PhpClass | PhpInterface 11 | ) { 12 | super(ast); 13 | } 14 | 15 | public get name() { 16 | return this.getIdentifierName(this.ast.name); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/parser/php/PhpNode.ts: -------------------------------------------------------------------------------- 1 | import { KindType, NodeKind } from './Parser'; 2 | import { Identifier } from 'php-parser'; 3 | 4 | export abstract class PhpNode { 5 | constructor(public ast: KindType) {} 6 | 7 | public searchAst(kind: K) { 8 | const results: KindType[] = []; 9 | 10 | const search = (node: any) => { 11 | if (!node) return; 12 | 13 | if (Array.isArray(node)) { 14 | for (const item of node) { 15 | search(item); 16 | } 17 | } else { 18 | if (node.kind === kind) { 19 | results.push(node); 20 | } 21 | 22 | for (const key in node) { 23 | const value = node[key]; 24 | 25 | if (value !== node && ['object', 'array'].includes(typeof value)) { 26 | search(value); 27 | } 28 | } 29 | } 30 | }; 31 | 32 | search(this.ast); 33 | return results; 34 | } 35 | 36 | public getIdentifierName(node: Identifier | string) { 37 | if (typeof node === 'string') { 38 | return node; 39 | } 40 | 41 | return node.name; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/parser/php/PhpUseItem.ts: -------------------------------------------------------------------------------- 1 | import { UseItem } from 'php-parser'; 2 | import { NodeKind } from './Parser'; 3 | import { PhpFile } from './PhpFile'; 4 | import { PhpNode } from './PhpNode'; 5 | import last from 'lodash-es/last'; 6 | 7 | export class PhpUseItem extends PhpNode { 8 | constructor( 9 | ast: UseItem, 10 | public parent: PhpFile 11 | ) { 12 | super(ast); 13 | } 14 | 15 | public get fullName() { 16 | return this.ast.name; 17 | } 18 | 19 | public get name() { 20 | const parts = this.ast.name.split('\\'); 21 | return last(parts)!; 22 | } 23 | 24 | public get alias() { 25 | return this.ast.alias?.name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/generator/cronJob/CronJobClassGenerator.test.ts: -------------------------------------------------------------------------------- 1 | import { CronJobWizardData } from 'wizard/CronJobWizard'; 2 | import * as assert from 'assert'; 3 | import { Uri } from 'vscode'; 4 | import CronJobClassGenerator from 'generator/cronJob/CronJobClassGenerator'; 5 | import { describe, it, before, afterEach } from 'mocha'; 6 | import { setup } from 'test/setup'; 7 | import { getReferenceFile, getTestWorkspaceUri } from 'test/util'; 8 | import FileHeader from 'common/php/FileHeader'; 9 | import sinon from 'sinon'; 10 | 11 | describe('CronJobClassGenerator Tests', () => { 12 | const cronJobWizardData: CronJobWizardData = { 13 | module: 'Foo_Bar', 14 | className: 'TestCronJob', 15 | cronName: 'foo_bar_test_cron_job', 16 | cronGroup: 'default', 17 | cronSchedule: '* * * * *', 18 | }; 19 | 20 | before(async () => { 21 | await setup(); 22 | }); 23 | 24 | afterEach(() => { 25 | sinon.restore(); 26 | }); 27 | 28 | it('should generate cron job class file', async () => { 29 | // Mock the FileHeader.getHeader method to return a consistent header 30 | sinon.stub(FileHeader, 'getHeader').returns('Foo_Bar'); 31 | 32 | // Create the generator with test data 33 | const generator = new CronJobClassGenerator(cronJobWizardData); 34 | 35 | // Use a test workspace URI 36 | const workspaceUri = getTestWorkspaceUri(); 37 | 38 | // Generate the file 39 | const generatedFile = await generator.generate(workspaceUri); 40 | 41 | // Get the reference file content 42 | const referenceContent = getReferenceFile('generator/cronJob/TestCronJob.php'); 43 | 44 | // Compare the generated content with reference 45 | assert.strictEqual(generatedFile.content, referenceContent); 46 | }); 47 | 48 | it('should generate file in correct location', async () => { 49 | // Create the generator with test data 50 | const generator = new CronJobClassGenerator(cronJobWizardData); 51 | 52 | // Use a test workspace URI 53 | const workspaceUri = getTestWorkspaceUri(); 54 | 55 | // Generate the file 56 | const generatedFile = await generator.generate(workspaceUri); 57 | 58 | // Expected path 59 | const expectedPath = Uri.joinPath(workspaceUri, 'app/code/Foo/Bar/Cron/TestCronJob.php').fsPath; 60 | 61 | assert.strictEqual(generatedFile.uri.fsPath, expectedPath); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/test/generator/module/ModuleComposerGenerator.test.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWizardComposerData } from 'wizard/ModuleWizard'; 2 | import { License } from 'types/global'; 3 | import * as assert from 'assert'; 4 | import ModuleComposerGenerator from 'generator/module/ModuleComposerGenerator'; 5 | import { describe, it, before } from 'mocha'; 6 | import { setup } from 'test/setup'; 7 | import { getReferenceFile, getTestWorkspaceUri } from 'test/util'; 8 | 9 | describe('ModuleComposerGenerator Tests', () => { 10 | const moduleWizardData: ModuleWizardComposerData = { 11 | vendor: 'Foo', 12 | module: 'Bar', 13 | sequence: [], 14 | license: License.MIT, 15 | version: '1.0.0', 16 | copyright: 'Test Copyright', 17 | composer: true, 18 | composerName: 'foo/bar-module', 19 | composerDescription: 'A test module', 20 | }; 21 | 22 | before(async () => { 23 | await setup(); 24 | }); 25 | 26 | it('should generate composer.json', async () => { 27 | // Create the generator with modified test data 28 | const generator = new ModuleComposerGenerator(moduleWizardData); 29 | 30 | // Use a test workspace URI 31 | const workspaceUri = getTestWorkspaceUri(); 32 | 33 | // Generate the file 34 | const generatedFile = await generator.generate(workspaceUri); 35 | 36 | // Get the reference file content 37 | const referenceContent = getReferenceFile('generator/module/composer.json'); 38 | 39 | // Compare the generated content with reference 40 | assert.strictEqual(generatedFile.content, referenceContent); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/test/generator/module/ModuleRegistrationGenerator.test.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWizardData } from 'wizard/ModuleWizard'; 2 | import { License } from 'types/global'; 3 | import * as assert from 'assert'; 4 | import ModuleRegistrationGenerator from 'generator/module/ModuleRegistrationGenerator'; 5 | import FileHeader from 'common/php/FileHeader'; 6 | import { describe, it, after, before } from 'mocha'; 7 | import sinon from 'sinon'; 8 | import { getReferenceFile, getTestWorkspaceUri } from 'test/util'; 9 | import { setup } from 'test/setup'; 10 | 11 | describe('ModuleRegistrationGenerator Tests', () => { 12 | const moduleWizardData: ModuleWizardData = { 13 | vendor: 'Foo', 14 | module: 'Bar', 15 | sequence: [], 16 | license: License.None, 17 | version: '1.0.0', 18 | copyright: 'Test Copyright', 19 | composer: false, 20 | }; 21 | 22 | before(async () => { 23 | await setup(); 24 | }); 25 | 26 | after(async () => { 27 | sinon.restore(); 28 | }); 29 | 30 | it('should generate registration.php', async () => { 31 | // Create the generator with test data 32 | const generator = new ModuleRegistrationGenerator(moduleWizardData); 33 | 34 | // Use a test workspace URI 35 | const workspaceUri = getTestWorkspaceUri(); 36 | 37 | // Generate the file 38 | const generatedFile = await generator.generate(workspaceUri); 39 | 40 | // Get the content of the reference file 41 | const referenceContent = getReferenceFile('generator/module/registration.php'); 42 | 43 | assert.strictEqual(generatedFile.content, referenceContent); 44 | }); 45 | 46 | it('should generate registration.php with comment', async () => { 47 | // Create the generator with test data 48 | const generator = new ModuleRegistrationGenerator(moduleWizardData); 49 | 50 | // Mock the FileHeader.getHeaderAsComment method to return a test comment 51 | sinon.stub(FileHeader, 'getHeader').returns('This is a test comment'); 52 | 53 | // Use a test workspace URI 54 | const workspaceUri = getTestWorkspaceUri(); 55 | 56 | // Generate the file 57 | const generatedFile = await generator.generate(workspaceUri); 58 | 59 | // Get the content of the reference file with comments 60 | const referenceContent = getReferenceFile('generator/module/registration-with-comment.php'); 61 | 62 | assert.strictEqual(generatedFile.content, referenceContent); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import ExtensionState from 'common/ExtensionState'; 2 | import { extensions } from 'vscode'; 3 | import Common from 'util/Common'; 4 | 5 | export async function setup() { 6 | const extension = extensions.getExtension(Common.EXTENSION_ID); 7 | const context = await extension?.activate(); 8 | 9 | ExtensionState.init(context, []); 10 | } 11 | -------------------------------------------------------------------------------- /src/test/util.ts: -------------------------------------------------------------------------------- 1 | import FileSystem from 'util/FileSystem'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { Uri } from 'vscode'; 5 | 6 | export function getReferenceFilePath(filePath: string) { 7 | const resourcePath = FileSystem.getExtensionPath('test-resources'); 8 | return path.resolve(resourcePath, 'reference', filePath); 9 | } 10 | 11 | export function getReferenceFile(filePath: string) { 12 | const refFilePath = getReferenceFilePath(filePath); 13 | return fs.readFileSync(refFilePath, 'utf8'); 14 | } 15 | 16 | export function getTestWorkspaceUri() { 17 | return Uri.file(FileSystem.getExtensionPath('test-resources/workspace')); 18 | } 19 | -------------------------------------------------------------------------------- /src/types/global.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export type Command = (context: vscode.ExtensionContext) => void; 4 | 5 | export enum License { 6 | None = 'none', 7 | APACHE2 = 'apache2', 8 | MIT = 'mit', 9 | GPL_V3 = 'gplv3', 10 | OSL_V3 = 'oslv3', 11 | } 12 | 13 | export enum MagentoScope { 14 | Global = 'global', 15 | Frontend = 'frontend', 16 | Adminhtml = 'adminhtml', 17 | WebapiRest = 'webapi_rest', 18 | WebapiSoap = 'webapi_soap', 19 | Graphql = 'graphql', 20 | Cron = 'cron', 21 | } 22 | -------------------------------------------------------------------------------- /src/types/indexer.ts: -------------------------------------------------------------------------------- 1 | export type IndexerKey = string; 2 | export type IndexedFilePath = string; 3 | type WorkspacePath = string; 4 | 5 | export type IndexerStorage = Record< 6 | WorkspacePath, 7 | Record> 8 | >; 9 | 10 | export interface SavedIndex { 11 | version: number; 12 | data: Map; 13 | } 14 | -------------------------------------------------------------------------------- /src/util/Common.ts: -------------------------------------------------------------------------------- 1 | import ExtensionState from 'common/ExtensionState'; 2 | import { workspace, WorkspaceFolder, window } from 'vscode'; 3 | 4 | export default class Common { 5 | public static EXTENSION_ID = 'magebit.magebit-magento-toolbox'; 6 | 7 | public static getActiveWorkspaceFolder(): WorkspaceFolder | undefined { 8 | if (!workspace.workspaceFolders) { 9 | throw new Error('Workspace is empty'); 10 | } 11 | 12 | if (window.activeTextEditor?.document.uri) { 13 | return workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); 14 | } 15 | 16 | if (ExtensionState.magentoWorkspaces.length > 0) { 17 | return ExtensionState.magentoWorkspaces[0]; 18 | } 19 | 20 | return undefined; 21 | } 22 | 23 | public static getVersion(): string { 24 | return ExtensionState.context.extension.packageJSON.version; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/util/FileSystem.ts: -------------------------------------------------------------------------------- 1 | import { FileType, RelativePattern, Uri, workspace } from 'vscode'; 2 | import * as path from 'path'; 3 | import ExtensionState from 'common/ExtensionState'; 4 | 5 | export default class FileSystem { 6 | public static async fileExists(uri: Uri): Promise { 7 | return workspace.fs 8 | .stat(uri) 9 | .then( 10 | () => true, 11 | () => false 12 | ) 13 | .then(exists => { 14 | return exists; 15 | }); 16 | } 17 | 18 | public static async readFile(uri: Uri): Promise { 19 | const content = await workspace.fs.readFile(uri); 20 | return content.toString(); 21 | } 22 | 23 | public static async readDirectory(uri: Uri): Promise { 24 | const files = await workspace.fs.readDirectory(uri); 25 | return files.map(([name]) => name); 26 | } 27 | 28 | public static async readDirectoryRecursive(uri: Uri): Promise { 29 | const files = await workspace.findFiles(new RelativePattern(uri, '**/*.php')); 30 | return files.map(file => path.relative(uri.fsPath, file.fsPath)); 31 | } 32 | 33 | public static async writeFile(uri: Uri, content: string): Promise { 34 | await workspace.fs.writeFile(uri, Buffer.from(content)); 35 | } 36 | 37 | public static getExtensionPath(dir: string): string { 38 | ExtensionState.context.extensionPath; 39 | return path.join(ExtensionState.context.extensionPath, 'dist', dir); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/util/Logger.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | 3 | export class Logger { 4 | private channel = window.createOutputChannel('Magento Toolbox'); 5 | 6 | public log(...message: string[]) { 7 | this.channel.appendLine(message.join(' ')); 8 | } 9 | 10 | public logWithTime(...message: string[]) { 11 | this.log(new Date().toISOString(), ...message); 12 | } 13 | } 14 | 15 | export default new Logger(); 16 | -------------------------------------------------------------------------------- /src/util/Magento.ts: -------------------------------------------------------------------------------- 1 | import PhpNamespace from 'common/PhpNamespace'; 2 | import lowerFirst from 'lodash-es/lowerFirst'; 3 | import { MagentoScope } from 'types/global'; 4 | import { Uri, WorkspaceFolder } from 'vscode'; 5 | import FileSystem from './FileSystem'; 6 | 7 | export default class Magento { 8 | public static isPluginMethod(method: string) { 9 | return /^around|^before|^after/.test(method); 10 | } 11 | 12 | public static pluginMethodToMethodName(method: string): string { 13 | return lowerFirst(method.replace(/^around|^before|^after/, '')); 14 | } 15 | 16 | public static getUriWithArea( 17 | baseUri: Uri, 18 | filePath: string, 19 | area: MagentoScope = MagentoScope.Global 20 | ) { 21 | if (area === MagentoScope.Global) { 22 | return Uri.joinPath(baseUri, filePath); 23 | } 24 | 25 | return Uri.joinPath(baseUri, area, filePath); 26 | } 27 | 28 | public static getModuleDirectory( 29 | vendor: string, 30 | module: string, 31 | baseUri: Uri, 32 | path: string = '' 33 | ): Uri { 34 | return Uri.joinPath(baseUri, 'app', 'code', vendor, module, path); 35 | } 36 | 37 | public static getModuleName(vendor: string, module: string): string { 38 | return `${vendor}_${module}`; 39 | } 40 | 41 | public static getModuleNamespace(vendor: string, module: string): PhpNamespace { 42 | return PhpNamespace.fromParts([vendor, module]); 43 | } 44 | 45 | public static async isMagentoWorkspace(workspaceFolder: WorkspaceFolder): Promise { 46 | const diXmlPath = Uri.joinPath(workspaceFolder.uri, 'app/etc/di.xml'); 47 | // Check if the signature Magento file exists in the workspace 48 | try { 49 | return await FileSystem.fileExists(diXmlPath); 50 | } catch (error) { 51 | return false; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/util/Position.ts: -------------------------------------------------------------------------------- 1 | import { Position as PhpAstPosition, Location as PhpAstLocation } from 'php-parser'; 2 | import { Position as VsCodePosition, Range } from 'vscode'; 3 | 4 | export default class Position { 5 | public static phpAstPositionToVsCodePosition(phpAstPosition: PhpAstPosition): VsCodePosition { 6 | return new VsCodePosition(Math.max(phpAstPosition.line - 1, 0), phpAstPosition.column); 7 | } 8 | 9 | public static phpAstLocationToVsCodeRange(phpAstLocation: PhpAstLocation): Range { 10 | return new Range( 11 | this.phpAstPositionToVsCodePosition(phpAstLocation.start), 12 | this.phpAstPositionToVsCodePosition(phpAstLocation.end) 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/webview/Webview.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import ExtensionState from 'common/ExtensionState'; 5 | 6 | export class Webview { 7 | protected panel?: vscode.WebviewPanel; 8 | 9 | protected open( 10 | type: string, 11 | title: string, 12 | column: vscode.ViewColumn = vscode.ViewColumn.One, 13 | options: vscode.WebviewOptions = {}, 14 | filename: string = 'index.html' 15 | ) { 16 | this.panel = vscode.window.createWebviewPanel(type, title, column, options); 17 | 18 | this.panel.webview.html = this.getHtml(filename); 19 | } 20 | 21 | protected getHtml(filename: string) { 22 | return fs.readFileSync( 23 | path.join(ExtensionState.context.extensionPath, 'templates', 'webview', filename), 24 | 'utf8' 25 | ); 26 | } 27 | 28 | protected close() { 29 | this.panel?.dispose(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/webview/WizardFormBuilder.ts: -------------------------------------------------------------------------------- 1 | import { ErrorMessages, Rules, TypeCheckingRule } from 'validatorjs'; 2 | import { Wizard, WizardTab } from 'types/webview'; 3 | 4 | export class WizardFormBuilder { 5 | private title?: string; 6 | private description?: string; 7 | private tabs: WizardTab[] = []; 8 | private validation: Rules = {}; 9 | private validationMessages: ErrorMessages = {}; 10 | 11 | public static new(): WizardFormBuilder { 12 | return new WizardFormBuilder(); 13 | } 14 | 15 | public addTab(tab: WizardTab): void { 16 | this.tabs.push(tab); 17 | } 18 | 19 | public setTitle(title: string): void { 20 | this.title = title; 21 | } 22 | 23 | public setDescription(description: string): void { 24 | this.description = description; 25 | } 26 | 27 | public addValidation( 28 | field: string, 29 | rule: string | Array | Rules 30 | ): void { 31 | this.validation[field] = rule; 32 | } 33 | 34 | public addValidationMessage(field: string, message: string): void { 35 | this.validationMessages[field] = message; 36 | } 37 | 38 | public build(): Wizard { 39 | if (!this.title) { 40 | throw new Error('Title is required'); 41 | } 42 | 43 | if (!this.tabs.length) { 44 | throw new Error('Tabs are required'); 45 | } 46 | 47 | return { 48 | title: this.title, 49 | description: this.description, 50 | tabs: this.tabs, 51 | validation: this.validation, 52 | validationMessages: this.validationMessages, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/webview/WizardTabBuilder.ts: -------------------------------------------------------------------------------- 1 | import { WizardField, WizardTab } from 'types/webview'; 2 | 3 | export class WizardTabBuilder { 4 | private id?: string; 5 | private title?: string; 6 | private description?: string; 7 | private fields: WizardField[] = []; 8 | 9 | public static new(): WizardTabBuilder { 10 | return new WizardTabBuilder(); 11 | } 12 | 13 | public setId(id: string): void { 14 | this.id = id; 15 | } 16 | 17 | public setTitle(title: string): void { 18 | this.title = title; 19 | } 20 | 21 | public setDescription(description: string): void { 22 | this.description = description; 23 | } 24 | 25 | public addField(field: WizardField): void { 26 | this.fields.push(field); 27 | } 28 | 29 | public build(): WizardTab { 30 | if (!this.id) { 31 | throw new Error('Id is required'); 32 | } 33 | 34 | if (!this.title) { 35 | throw new Error('Title is required'); 36 | } 37 | 38 | if (!this.fields.length) { 39 | throw new Error('Fields are required'); 40 | } 41 | 42 | return { 43 | id: this.id, 44 | title: this.title, 45 | description: this.description, 46 | fields: this.fields, 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/webview/components/App.tsx: -------------------------------------------------------------------------------- 1 | import '@vscode-elements/elements'; 2 | import './app.css'; 3 | import { useEffect, useState } from 'react'; 4 | import Wizard from './Wizard'; 5 | import { Command, Page, Wizard as WizardType } from 'types/webview'; 6 | 7 | const vscode = (window as any).acquireVsCodeApi(); 8 | 9 | const App: React.FC = () => { 10 | const [page, setPage] = useState(null); 11 | const [pageData, setPageData] = useState(null); 12 | 13 | useEffect(() => { 14 | window.addEventListener('message', event => { 15 | const message = event.data; 16 | 17 | switch (message.command) { 18 | case Command.ShowWizard: 19 | setPage(Page.Wizard); 20 | setPageData(message.data); 21 | 22 | break; 23 | } 24 | }); 25 | 26 | vscode.postMessage({ 27 | command: Command.Ready, 28 | }); 29 | }, []); 30 | 31 | return ( 32 |
33 | {page === Page.Wizard && !!pageData && } 34 |
35 | ); 36 | }; 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /src/webview/components/Wizard.tsx: -------------------------------------------------------------------------------- 1 | import { WebviewApi } from 'vscode-webview'; 2 | import { Wizard as WizardType } from 'types/webview'; 3 | import { Renderer } from './Wizard/Renderer'; 4 | 5 | interface WizardProps { 6 | data: WizardType; 7 | vscode: WebviewApi; 8 | } 9 | 10 | const Wizard: React.FC = ({ data, vscode }) => { 11 | return ( 12 |
13 |

{data.title}

14 |

{data.description}

15 |
16 | 17 |
18 | ); 19 | }; 20 | 21 | export default Wizard; 22 | -------------------------------------------------------------------------------- /src/webview/components/Wizard/DynamicRowInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { WizardDynamicRowField, WizardField } from 'types/webview'; 3 | import { FieldRenderer } from './FieldRenderer'; 4 | import { FieldArray, useFormikContext } from 'formik'; 5 | 6 | interface Props { 7 | field: WizardDynamicRowField; 8 | } 9 | 10 | export const DynamicRowInput: React.FC = ({ field }) => { 11 | const { values } = useFormikContext(); 12 | const rows = values[field.id] ?? ([] as Record[]); 13 | 14 | return ( 15 | { 18 | return ( 19 | <> 20 | {/* @ts-ignore */} 21 | 22 | 23 | {field.fields.map(field => ( 24 | 25 | {field.label} 26 | 27 | ))} 28 | Action 29 | 30 | 31 | 32 | {rows.map((row: any, index: number) => ( 33 | 34 | {field.fields.map(childField => ( 35 | 39 | 40 | 41 | ))} 42 | 43 | arrayHelpers.remove(index)}> 44 | Remove 45 | 46 | 47 | 48 | ))} 49 | 50 | 51 | arrayHelpers.push({})}> 52 | Add Row 53 | 54 | 55 | ); 56 | }} 57 | > 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/webview/components/Wizard/FieldErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Field, FormikProps, getIn } from 'formik'; 2 | 3 | export const FieldErrorMessage: React.FC<{ name: string }> = ({ name }) => { 4 | return ( 5 | }) => { 8 | const error = form.errors[name]; 9 | const touch = getIn(form.touched, name); 10 | 11 | return touch && error ? error : null; 12 | }} 13 | /> 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/webview/components/app.css: -------------------------------------------------------------------------------- 1 | .app { 2 | padding: 16px; 3 | } 4 | 5 | .dynamic-row-cell { 6 | vertical-align: top; 7 | padding-top: 8px; 8 | padding-bottom: 6px; 9 | } 10 | 11 | .dynamic-row-title { 12 | width: 100%; 13 | text-align: center; 14 | font-size: 14px; 15 | font-weight: 600; 16 | margin-bottom: 12px; 17 | } 18 | 19 | .dynamic-row-add-row { 20 | margin-top: 12px; 21 | } 22 | 23 | .tab-panel { 24 | padding: 16px; 25 | overflow: visible; 26 | } 27 | -------------------------------------------------------------------------------- /src/webview/error/WizzardClosedError.ts: -------------------------------------------------------------------------------- 1 | export default class WizzardClosedError extends Error {} 2 | -------------------------------------------------------------------------------- /src/webview/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './components/App'; 3 | 4 | const root = createRoot(document.getElementById('root') as HTMLElement); 5 | 6 | root.render(); 7 | -------------------------------------------------------------------------------- /src/wizard/BlockWizard.ts: -------------------------------------------------------------------------------- 1 | import IndexManager from 'indexer/IndexManager'; 2 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 3 | import { GeneratorWizard } from 'webview/GeneratorWizard'; 4 | import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; 5 | import { WizardFormBuilder } from 'webview/WizardFormBuilder'; 6 | import { WizardTabBuilder } from 'webview/WizardTabBuilder'; 7 | 8 | export interface BlockWizardData { 9 | module: string; 10 | name: string; 11 | path: string; 12 | } 13 | 14 | export default class BlockWizard extends GeneratorWizard { 15 | public async show(contextModule?: string): Promise { 16 | const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); 17 | 18 | if (!moduleIndexData) { 19 | throw new Error('Module index data not found'); 20 | } 21 | 22 | const modules = moduleIndexData.getModuleOptions(m => m.location === 'app'); 23 | 24 | const builder = new WizardFormBuilder(); 25 | 26 | builder.setTitle('Generate a new block'); 27 | builder.setDescription('Generates a new Magento2 block class.'); 28 | 29 | const tab = new WizardTabBuilder(); 30 | tab.setId('block'); 31 | tab.setTitle('Block'); 32 | 33 | tab.addField( 34 | WizardFieldBuilder.select('module', 'Module*') 35 | .setOptions(modules) 36 | .setInitialValue(contextModule || modules[0].value) 37 | .build() 38 | ); 39 | 40 | tab.addField( 41 | WizardFieldBuilder.text('name', 'Block Name*').setPlaceholder('Block class name').build() 42 | ); 43 | 44 | tab.addField( 45 | WizardFieldBuilder.text('path', 'Block Directory*') 46 | .setPlaceholder('Block/Path') 47 | .setInitialValue('Block') 48 | .build() 49 | ); 50 | 51 | builder.addTab(tab.build()); 52 | 53 | builder.addValidation('module', 'required'); 54 | builder.addValidation('name', 'required|min:1'); 55 | builder.addValidation('path', 'required|min:1'); 56 | 57 | const data = await this.openWizard(builder.build()); 58 | 59 | return data; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/wizard/DataPatchWizard.ts: -------------------------------------------------------------------------------- 1 | import IndexManager from 'indexer/IndexManager'; 2 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 3 | import { GeneratorWizard } from 'webview/GeneratorWizard'; 4 | import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; 5 | import { WizardFormBuilder } from 'webview/WizardFormBuilder'; 6 | import { WizardTabBuilder } from 'webview/WizardTabBuilder'; 7 | import Validation from 'common/Validation'; 8 | 9 | export interface DataPatchWizardData { 10 | module: string; 11 | className: string; 12 | revertable: boolean; 13 | } 14 | 15 | export default class DataPatchWizard extends GeneratorWizard { 16 | public async show(contextModule?: string): Promise { 17 | const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); 18 | 19 | if (!moduleIndexData) { 20 | throw new Error('Module index data not found'); 21 | } 22 | 23 | const modules = moduleIndexData.getModuleOptions(module => module.location === 'app'); 24 | 25 | const builder = new WizardFormBuilder(); 26 | 27 | builder.setTitle('Generate a new Data Patch'); 28 | builder.setDescription('Generates a new Data Patch for a module.'); 29 | 30 | const tab = new WizardTabBuilder(); 31 | tab.setId('dataPatch'); 32 | tab.setTitle('Data Patch'); 33 | 34 | tab.addField( 35 | WizardFieldBuilder.select('module', 'Module') 36 | .setDescription(['Module where data patch will be generated in']) 37 | .setOptions(modules) 38 | .setInitialValue(contextModule || modules[0].value) 39 | .build() 40 | ); 41 | 42 | tab.addField( 43 | WizardFieldBuilder.text('className', 'Class Name') 44 | .setDescription(['The class name for the data patch']) 45 | .setPlaceholder('YourPatchName') 46 | .build() 47 | ); 48 | 49 | tab.addField( 50 | WizardFieldBuilder.checkbox('revertable', 'Revertable').setInitialValue(false).build() 51 | ); 52 | 53 | builder.addTab(tab.build()); 54 | 55 | builder.addValidation('module', 'required'); 56 | builder.addValidation('className', [ 57 | 'required', 58 | `regex:/${Validation.CLASS_NAME_REGEX.source}/`, 59 | ]); 60 | 61 | const data = await this.openWizard(builder.build()); 62 | 63 | return data; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/wizard/ViewModelWizard.ts: -------------------------------------------------------------------------------- 1 | import IndexManager from 'indexer/IndexManager'; 2 | import ModuleIndexer from 'indexer/module/ModuleIndexer'; 3 | import { GeneratorWizard } from 'webview/GeneratorWizard'; 4 | import { WizardFieldBuilder } from 'webview/WizardFieldBuilder'; 5 | import { WizardFormBuilder } from 'webview/WizardFormBuilder'; 6 | import { WizardTabBuilder } from 'webview/WizardTabBuilder'; 7 | 8 | export interface ViewModelWizardData { 9 | module: string; 10 | className: string; 11 | directory: string; 12 | } 13 | 14 | export default class ViewModelWizard extends GeneratorWizard { 15 | public async show(contextModule?: string): Promise { 16 | const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); 17 | 18 | if (!moduleIndexData) { 19 | throw new Error('Module index data not found'); 20 | } 21 | 22 | const modules = moduleIndexData.getModuleOptions(m => m.location === 'app'); 23 | 24 | const builder = new WizardFormBuilder(); 25 | 26 | builder.setTitle('Generate a new ViewModel'); 27 | builder.setDescription('Generates a new Magento2 ViewModel class.'); 28 | 29 | const tab = new WizardTabBuilder(); 30 | tab.setId('viewModel'); 31 | tab.setTitle('ViewModel'); 32 | 33 | tab.addField( 34 | WizardFieldBuilder.select('module', 'Module*') 35 | .setOptions(modules) 36 | .setInitialValue(contextModule || modules[0].value) 37 | .build() 38 | ); 39 | 40 | tab.addField( 41 | WizardFieldBuilder.text('className', 'Class Name*') 42 | .setPlaceholder('ViewModel class name') 43 | .build() 44 | ); 45 | 46 | tab.addField( 47 | WizardFieldBuilder.text('directory', 'Directory*') 48 | .setPlaceholder('ViewModel/Path') 49 | .setInitialValue('ViewModel') 50 | .build() 51 | ); 52 | 53 | builder.addTab(tab.build()); 54 | 55 | builder.addValidation('module', 'required'); 56 | builder.addValidation('className', 'required|min:1'); 57 | builder.addValidation('directory', 'required|min:1'); 58 | 59 | const data = await this.openWizard(builder.build()); 60 | 61 | return data; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /templates/handlebars/graphql/blank-schema.hbs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magebitcom/magento-toolbox/434fe91e78eb3d128dc9669a2019ffd50baed463/templates/handlebars/graphql/blank-schema.hbs -------------------------------------------------------------------------------- /templates/handlebars/license/mit.hbs: -------------------------------------------------------------------------------- 1 | Copyright © {{ year }}-present {{ copyright }} 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /templates/handlebars/php/registration.hbs: -------------------------------------------------------------------------------- 1 | fileHeader}} 4 | declare(strict_types=1); 5 | 6 | use Magento\Framework\Component\ComponentRegistrar; 7 | 8 | ComponentRegistrar::register(ComponentRegistrar::MODULE, '{{ vendor }}_{{ module }}', __DIR__); 9 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-acl.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-config.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-crontab.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-di.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-email-templates.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-events.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-extension-attributes.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-fieldset.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-indexer.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-mview.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-page-types.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-routes.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-sections.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-system.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-view.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-webapi.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/blank-widget.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> fileHeader}} 3 | 5 | 6 | -------------------------------------------------------------------------------- /templates/handlebars/xml/cron/group.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> groupContent}} 3 | -------------------------------------------------------------------------------- /templates/handlebars/xml/cron/job.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{cronSchedule}} 3 | -------------------------------------------------------------------------------- /templates/handlebars/xml/di/plugin.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/handlebars/xml/di/preference.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/handlebars/xml/di/type.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> typeContent }} 3 | -------------------------------------------------------------------------------- /templates/handlebars/xml/events/event.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{> eventContent }} 3 | -------------------------------------------------------------------------------- /templates/handlebars/xml/events/observer.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/webview/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Magento Toolbox 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /test-resources/reference/generator/block/TestBlock.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | * * * * * 8 | 9 | 10 | 11 | * * * * * 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test-resources/reference/generator/cronJob/crontab.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | * * * * * 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test-resources/reference/generator/dataPatch/TestDataPatch.php: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test-resources/reference/generator/module/module-with-sequence.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test-resources/reference/generator/module/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test-resources/reference/generator/module/registration-with-comment.php: -------------------------------------------------------------------------------- 1 | getEvent(); 25 | // TODO: Observer code 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test-resources/reference/generator/observer/TestObserverCustomPath.php: -------------------------------------------------------------------------------- 1 | getEvent(); 25 | // TODO: Observer code 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test-resources/reference/generator/observer/events-adminhtml.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test-resources/reference/generator/observer/events-with-comment.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test-resources/reference/generator/observer/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test-resources/reference/generator/plugin/SubjectClass.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test-resources/reference/generator/plugin/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test-resources/reference/generator/preference/TestPreference.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test-resources/reference/generator/viewModel/TestViewModel.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test-resources/workspace/app/code/Foo/Bar/registration.php: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "bundler", 5 | "target": "ES2022", 6 | "lib": ["ES2022", "DOM"], 7 | "jsx": "react-jsx", 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "baseUrl": "src", 11 | "skipLibCheck": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "strict": true 15 | }, 16 | "include": ["src"] 17 | } 18 | --------------------------------------------------------------------------------