Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,36 @@

# Magento Toolbox

Code generation and inspection tools for Magento 2 development.
Code generation, inspection, and intelligence tools for **Magento 2 & Adobe Commerce** — right inside VS Code, Cursor, and other VS Code–compatible editors.

## Features

Check out our [Wiki page](https://github.com/magebitcom/magento-toolbox/wiki) for information about the available features
| Feature | What it does |
| --- | --- |
| **Code Generation** | Scaffold modules, blocks, view models, observers, plugins, preferences, data patches, cron jobs, and 20+ sample `etc/` XML config files from the command palette or context menu. |
| **Autocomplete** | Context-aware completions in XML for PHP classes, ACL resources, module names, template paths, layout block & container names, and parent menu items — plus handy XML snippets. |
| **Diagnostics** | Catch broken layout references, unresolved DI types, duplicate or disabled plugins and observers, invalid ACL nodes, broken Web API services, and FPC-killing `cacheable="false"` in default layouts. |
| **Definitions & Navigation** | <kbd>Ctrl</kbd>-click to jump to classes, modules, ACL IDs, theme parents, and layout symbols — with rich hover info, find-all-references, and rename across area-specific files. |
| **Code Decorations** | Gutter icons and inline hints that link plugins, observers, dispatched events, cron jobs, and ACL rules straight to their declarations. |
| **Code Lenses** | Inline actions where you need them — such as _Create an Observer_ right above a dispatched event. |
| **Utilities** | Jump to any module, copy a file's Magento path (`Module::path/to/file.phtml`), and generate the XML URN catalog for schema validation. |

📖 For the full feature breakdown and configuration options, see the **[Wiki](https://github.com/magebitcom/magento-toolbox/wiki)**.

## Installation

### From the extensions tab
Open up the extensions tab and search for `Magento Toolbox`
Open the Extensions tab and search for `Magento Toolbox`.

### Via command
Open up the VS Code Quick open window (`Ctrl-P`) and run this command:
Open the VS Code Quick Open window (<kbd>Ctrl</kbd>-<kbd>P</kbd>) and run:
```
ext install magebit.magebit-magento-toolbox
```

## Contributing

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.
Found a bug, have a feature suggestion, or just want to help out? Contributions are very welcome! Check out the list of active [issues](https://github.com/magebitcom/magento-toolbox/issues) or open one yourself.

---
![magebit (1)](https://github.com/user-attachments/assets/cdc904ce-e839-40a0-a86f-792f7ab7961f)
Expand Down
77 changes: 77 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@
}
],
"commands": [
{
"command": "magento-toolbox.generate",
"title": "Generate…",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateModule",
"title": "Generate Module",
Expand Down Expand Up @@ -296,6 +301,46 @@
"title": "Generate Cron Job",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateCliCommand",
"title": "Generate CLI Command",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateProductEavAttributePatch",
"title": "Generate Product EAV Attribute Patch",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateCategoryEavAttributePatch",
"title": "Generate Category EAV Attribute Patch",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateCustomerEavAttributePatch",
"title": "Generate Customer EAV Attribute Patch",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateSystemConfig",
"title": "Generate System Configuration",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateGraphqlResolver",
"title": "Generate GraphQL Resolver",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateCronGroup",
"title": "Generate Cron Group",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.generateController",
"title": "Generate Controller",
"category": "Magento Toolbox"
},
{
"command": "magento-toolbox.jumpToModule",
"title": "Jump to Module",
Expand Down Expand Up @@ -437,6 +482,38 @@
{
"command": "magento-toolbox.generateCronJob",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateCliCommand",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateProductEavAttributePatch",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateCategoryEavAttributePatch",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateCustomerEavAttributePatch",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateSystemConfig",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateGraphqlResolver",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateCronGroup",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
},
{
"command": "magento-toolbox.generateController",
"when": "resourcePath =~ /app\\/code\\/.+\\/.+/i"
}
]
}
Expand Down
Binary file modified resources/logo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 23 additions & 12 deletions src/cache/DocumentCache.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
import { TextDocument } from 'vscode';

interface CacheEntry {
version: number;
value: any;
}

class DocumentCache {
protected cache: Map<string, any> = new Map();
protected cache: Map<string, CacheEntry> = new Map();

public get(document: TextDocument, key: string) {
const cacheKey = this.getCacheKey(document, key);
return this.cache.get(cacheKey);
const entry = this.cache.get(this.getCacheKey(document, key));

if (!entry || entry.version !== document.version) {
return undefined;
}

return entry.value;
}

public set(document: TextDocument, key: string, value: any) {
const cacheKey = this.getCacheKey(document, key);
this.cache.set(cacheKey, value);
this.cache.set(this.getCacheKey(document, key), { version: document.version, value });
}

public delete(document: TextDocument, key: string) {
const cacheKey = this.getCacheKey(document, key);
this.cache.delete(cacheKey);
this.cache.delete(this.getCacheKey(document, key));
}

public clear(document: TextDocument) {
this.cache.forEach((value, key) => {
if (key.startsWith(document.uri.fsPath)) {
const prefix = `${document.uri.fsPath}-`;

for (const key of this.cache.keys()) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
}
});
}
}

public has(document: TextDocument, key: string) {
const cacheKey = this.getCacheKey(document, key);
return this.cache.has(cacheKey);
const entry = this.cache.get(this.getCacheKey(document, key));

return entry !== undefined && entry.version === document.version;
}

protected getCacheKey(document: TextDocument, key: string) {
Expand Down
132 changes: 132 additions & 0 deletions src/command/AbstractWizardCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Uri, window, workspace, WorkspaceFolder } from 'vscode';
import { Command } from './Command';
import Common from 'util/Common';
import FileGenerator from 'generator/FileGenerator';
import FileGeneratorManager from 'generator/FileGeneratorManager';
import FileSystem from 'util/FileSystem';
import IndexManager from 'indexer/IndexManager';
import ModuleIndexer from 'indexer/module/ModuleIndexer';
import WizzardClosedError from 'webview/error/WizzardClosedError';
import { GeneratorWizard } from 'webview/GeneratorWizard';
import { PreviewResult } from 'types/webview';
import CommandAbortError from './CommandAbortError';

export type OpenStrategy = 'first' | 'last' | 'all' | 'none';

export abstract class AbstractWizardCommand<TData> extends Command {
protected abstract showWizard(
contextModule: string | undefined,
uri: Uri | undefined,
args: unknown[]
): Promise<TData>;

protected abstract buildGenerators(data: TData): FileGenerator[];

protected openStrategy(): OpenStrategy {
return 'first';
}

public async execute(uri?: Uri, ...args: unknown[]): Promise<void> {
const contextModule = this.resolveContextModule(uri);

let data: TData;
try {
data = await this.showWizard(contextModule, uri, args);
} catch (error) {
if (error instanceof WizzardClosedError || error instanceof CommandAbortError) {
return;
}
throw error;
}

const workspaceFolder = this.requireWorkspaceFolder();
if (!workspaceFolder) {
return;
}

const manager = new FileGeneratorManager(this.buildGenerators(data));
await manager.generate(workspaceFolder.uri);
await manager.writeFiles();
await manager.refreshIndex(workspaceFolder);
this.openFiles(manager);
}

protected resolveContextModule(uri: Uri | undefined): string | undefined {
const contextUri = uri ?? window.activeTextEditor?.document.uri;
if (!contextUri) {
return undefined;
}

const moduleIndex = IndexManager.getIndexData(ModuleIndexer.KEY);
if (!moduleIndex) {
return undefined;
}

return moduleIndex.getModuleByUri(contextUri)?.name;
}

protected requireWorkspaceFolder(): WorkspaceFolder | undefined {
const folder = Common.getActiveWorkspaceFolder();
if (!folder) {
window.showErrorMessage('No active workspace folder');
return undefined;
}
return folder;
}

protected openFiles(manager: FileGeneratorManager): void {
switch (this.openStrategy()) {
case 'all':
manager.openAllFiles();
return;
case 'last':
manager.openLastFile();
return;
case 'none':
return;
case 'first':
default:
manager.openFirstFile();
}
}

/**
* Attach the live-preview handler to a freshly-constructed wizard. Commands
* should call this on any wizard before invoking show().
*/
protected attachPreview<W extends GeneratorWizard>(wizard: W): W {
wizard.setPreviewHandler(formData => this.buildPreview(formData as TData));
return wizard;
}

/**
* Dry-run the generators for the given form data and return a list of
* files that would be created or modified. Catches generator errors (e.g.
* partial input) and returns an empty list so the webview can show a neutral
* state until the form is complete enough to generate.
*/
protected async buildPreview(data: TData): Promise<PreviewResult> {
const workspaceFolder = Common.getActiveWorkspaceFolder();
if (!workspaceFolder) {
return { files: [] };
}

try {
const manager = new FileGeneratorManager(this.buildGenerators(data));
const generated = await manager.generate(workspaceFolder.uri);

const files = await Promise.all(
generated.map(async file => ({
path: workspace.asRelativePath(file.uri),
action: ((await FileSystem.fileExists(file.uri)) ? 'modify' : 'create') as
| 'create'
| 'modify',
}))
);

return { files };
} catch (error) {
return { files: [], error: error instanceof Error ? error.message : String(error) };
}
}
}
11 changes: 11 additions & 0 deletions src/command/CommandAbortError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Thrown from inside a command's showWizard() to cleanly abort before any files
* are generated. The base command swallows it silently — callers should show
* their own error message before throwing.
*/
export default class CommandAbortError extends Error {
constructor(message?: string) {
super(message ?? 'Command aborted');
this.name = 'CommandAbortError';
}
}
Loading
Loading