Skip to content

Commit 3a4dc4f

Browse files
committed
Merge branch 'master' of github-magebit:magebitcom/magento-toolbox into feature/xml-snippets
2 parents 6cda66f + b317e87 commit 3a4dc4f

14 files changed

+296
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ All notable changes to the "magento-toolbox" extension will be documented in thi
44

55
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
66

7-
## [Unreleased]
7+
## [1.6.0] - 2025-04-09
88

99
- Added: Event name autocomplete
10+
- Added: Hovering CRON job schedules will show a human readable version
11+
- Added: Cron job indexer and instance class decorations
12+
- Changed: Implemented batching for the indexer to reduce load
1013

1114
## [1.5.0] - 2025-04-06
1215
- Added: Class namespace autocomplete in XML files

package-lock.json

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Magento 2 code generation, inspection and utility tools",
55
"publisher": "magebit",
66
"icon": "resources/logo.jpg",
7-
"version": "1.5.0",
7+
"version": "1.6.0",
88
"engines": {
99
"vscode": "^1.93.1"
1010
},
@@ -427,6 +427,7 @@
427427
"@xml-tools/parser": "^1.0.11",
428428
"@xml-tools/simple-schema": "^3.0.5",
429429
"@xml-tools/validation": "^1.0.16",
430+
"cronstrue": "^2.59.0",
430431
"fast-xml-parser": "^4.5.1",
431432
"formik": "^2.4.6",
432433
"glob": "^11.0.1",

resources/icons/cron.svg

Lines changed: 7 additions & 0 deletions
Loading
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { DecorationOptions, TextEditorDecorationType, Uri, window } from 'vscode';
2+
import path from 'path';
3+
import TextDocumentDecorationProvider from './TextDocumentDecorationProvider';
4+
import { PhpClass } from 'parser/php/PhpClass';
5+
import { PhpInterface } from 'parser/php/PhpInterface';
6+
import PhpDocumentParser from 'common/php/PhpDocumentParser';
7+
import { ClasslikeInfo } from 'common/php/ClasslikeInfo';
8+
import MarkdownMessageBuilder from 'common/MarkdownMessageBuilder';
9+
import IndexManager from 'indexer/IndexManager';
10+
import CronIndexer from 'indexer/cron/CronIndexer';
11+
import { Job } from 'indexer/cron/types';
12+
import cronstrue from 'cronstrue';
13+
14+
export default class CronClassDecorationProvider extends TextDocumentDecorationProvider {
15+
public getType(): TextEditorDecorationType {
16+
return window.createTextEditorDecorationType({
17+
gutterIconPath: path.join(__dirname, 'resources', 'icons', 'cron.svg'),
18+
gutterIconSize: '80%',
19+
borderColor: 'rgba(0, 188, 202, 0.5)',
20+
borderStyle: 'dotted',
21+
borderWidth: '0 0 1px 0',
22+
});
23+
}
24+
25+
public async getDecorations(): Promise<DecorationOptions[]> {
26+
const decorations: DecorationOptions[] = [];
27+
const phpFile = await PhpDocumentParser.parse(this.document);
28+
29+
const classLikeNode: PhpClass | PhpInterface | undefined =
30+
phpFile.classes[0] || phpFile.interfaces[0];
31+
32+
if (!classLikeNode) {
33+
return decorations;
34+
}
35+
36+
const classlikeInfo = new ClasslikeInfo(phpFile);
37+
38+
const cronIndexData = IndexManager.getIndexData(CronIndexer.KEY);
39+
40+
if (!cronIndexData) {
41+
return decorations;
42+
}
43+
44+
const jobs = cronIndexData.findJobsByInstance(classlikeInfo.getNamespace());
45+
46+
if (jobs.length === 0) {
47+
return decorations;
48+
}
49+
50+
decorations.push(...this.getCronInstanceDecorations(jobs, classlikeInfo));
51+
52+
return decorations;
53+
}
54+
55+
private getCronInstanceDecorations(
56+
jobs: Job[],
57+
classlikeInfo: ClasslikeInfo
58+
): DecorationOptions[] {
59+
const decorations: DecorationOptions[] = [];
60+
61+
const nameRange = classlikeInfo.getNameRange();
62+
63+
if (!nameRange) {
64+
return decorations;
65+
}
66+
67+
const hoverMessage = MarkdownMessageBuilder.create('Cron Jobs');
68+
69+
for (const job of jobs) {
70+
hoverMessage.appendMarkdown(`- [${job.name}](${Uri.file(job.path)})\n`);
71+
hoverMessage.appendMarkdown(` - Method: \`${job.method}\`\n`);
72+
73+
if (job.schedule) {
74+
hoverMessage.appendMarkdown(
75+
` - \`${job.schedule}\` (${cronstrue.toString(job.schedule)})\n`
76+
);
77+
}
78+
79+
if (job.config_path) {
80+
hoverMessage.appendMarkdown(` - Config: \`${job.config_path}\`\n`);
81+
}
82+
}
83+
84+
decorations.push({
85+
range: nameRange,
86+
hoverMessage: hoverMessage.build(),
87+
});
88+
89+
return decorations;
90+
}
91+
}

src/hover/XmlHoverProviderProcessor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { CancellationToken, Hover, Position, TextDocument } from 'vscode';
22
import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor';
33
import { AclHoverProvider } from 'hover/xml/AclHoverProvider';
44
import { ModuleHoverProvider } from 'hover/xml/ModuleHoverProvider';
5+
import { CronHoverProvider } from 'hover/xml/CronHoverProvider';
56

67
export class XmlHoverProviderProcessor extends XmlSuggestionProviderProcessor<Hover> {
78
public constructor() {
8-
super([new AclHoverProvider(), new ModuleHoverProvider()]);
9+
super([new AclHoverProvider(), new ModuleHoverProvider(), new CronHoverProvider()]);
910
}
1011

1112
public async provideHover(

src/hover/xml/CronHoverProvider.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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<Hover> {
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+
}

src/indexer/IndexManager.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ import AclIndexer from './acl/AclIndexer';
1818
import { AclIndexData } from './acl/AclIndexData';
1919
import TemplateIndexer from './template/TemplateIndexer';
2020
import { TemplateIndexData } from './template/TemplateIndexData';
21+
import CronIndexer from './cron/CronIndexer';
22+
import { CronIndexData } from './cron/CronIndexData';
2123

2224
type IndexerInstance =
2325
| DiIndexer
2426
| ModuleIndexer
2527
| AutoloadNamespaceIndexer
2628
| EventsIndexer
2729
| AclIndexer
28-
| TemplateIndexer;
30+
| TemplateIndexer
31+
| CronIndexer;
2932

3033
type IndexerDataMap = {
3134
[DiIndexer.KEY]: DiIndexData;
@@ -34,9 +37,12 @@ type IndexerDataMap = {
3437
[EventsIndexer.KEY]: EventsIndexData;
3538
[AclIndexer.KEY]: AclIndexData;
3639
[TemplateIndexer.KEY]: TemplateIndexData;
40+
[CronIndexer.KEY]: CronIndexData;
3741
};
3842

3943
class IndexManager {
44+
private static readonly INDEX_BATCH_SIZE = 50;
45+
4046
protected indexers: IndexerInstance[] = [];
4147
protected indexStorage: IndexStorage;
4248

@@ -48,6 +54,7 @@ class IndexManager {
4854
new EventsIndexer(),
4955
new AclIndexer(),
5056
new TemplateIndexer(),
57+
new CronIndexer(),
5158
];
5259
this.indexStorage = new IndexStorage();
5360
}
@@ -86,23 +93,27 @@ class IndexManager {
8693
let doneCount = 0;
8794
const totalCount = files.length;
8895

89-
await Promise.all(
90-
files.map(async file => {
91-
const data = await indexer.indexFile(file);
96+
for (let i = 0; i < files.length; i += IndexManager.INDEX_BATCH_SIZE) {
97+
const batch = files.slice(i, i + IndexManager.INDEX_BATCH_SIZE);
98+
99+
await Promise.all(
100+
batch.map(async file => {
101+
const data = await indexer.indexFile(file);
92102

93-
if (data !== undefined) {
94-
indexData.set(file.fsPath, data);
95-
}
103+
if (data !== undefined) {
104+
indexData.set(file.fsPath, data);
105+
}
96106

97-
doneCount++;
98-
const pct = Math.round((doneCount / totalCount) * 100);
107+
doneCount++;
108+
const pct = Math.round((doneCount / totalCount) * 100);
99109

100-
progress.report({
101-
message: `Indexing - ${indexer.getName()} [${doneCount}/${totalCount}]`,
102-
increment: pct,
103-
});
104-
})
105-
);
110+
progress.report({
111+
message: `Indexing - ${indexer.getName()} [${doneCount}/${totalCount}]`,
112+
increment: pct,
113+
});
114+
})
115+
);
116+
}
106117

107118
this.indexStorage.set(workspaceFolder, indexer.getId(), indexData);
108119
this.indexStorage.saveIndex(workspaceFolder, indexer.getId(), indexer.getVersion());
@@ -181,6 +192,9 @@ class IndexManager {
181192
case TemplateIndexer.KEY:
182193
return new TemplateIndexData(data) as IndexerDataMap[T];
183194

195+
case CronIndexer.KEY:
196+
return new CronIndexData(data) as IndexerDataMap[T];
197+
184198
default:
185199
return undefined;
186200
}

src/indexer/IndexRunner.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import * as vscode from 'vscode';
22
import IndexManager from './IndexManager';
3+
import ExtensionState from 'common/ExtensionState';
4+
import Common from 'util/Common';
35

46
class IndexRunner {
57
public async indexWorkspace(force: boolean = false): Promise<void> {
6-
if (!vscode.workspace.workspaceFolders) {
8+
if (ExtensionState.magentoWorkspaces.length === 0) {
79
return;
810
}
911

10-
for (const workspaceFolder of vscode.workspace.workspaceFolders) {
12+
for (const workspaceFolder of ExtensionState.magentoWorkspaces) {
1113
await vscode.window.withProgress(
1214
{
1315
location: vscode.ProgressLocation.Window,
14-
title: '[Magento Toolbox]',
16+
title: `Magento Toolbox v${Common.getVersion()}`,
1517
},
1618
async progress => {
1719
await IndexManager.indexWorkspace(workspaceFolder, progress, force);

src/indexer/cron/CronIndexData.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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<Job[]> {
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+
}

0 commit comments

Comments
 (0)