From 09c7bb009613e1ad60eb22c3a2d8631b4d8ea502 Mon Sep 17 00:00:00 2001 From: raideer Date: Sun, 6 Apr 2025 16:30:43 +0300 Subject: [PATCH] feat: template indexer, definitions and autocomplete --- CHANGELOG.md | 1 + src/command/CopyMagentoPathCommand.ts | 68 +--------------- src/common/Context.ts | 8 +- src/common/GetMagentoPath.ts | 78 +++++++++++++++++++ .../XmlCompletionProviderProcessor.ts | 2 + .../xml/TemplateCompletionProvider.ts | 52 +++++++++++++ .../XmlDefinitionProviderProcessor.ts | 7 +- .../xml/TemplateDefinitionProvider.ts | 60 ++++++++++++++ src/indexer/IndexManager.ts | 10 ++- src/indexer/template/TemplateIndexData.ts | 21 +++++ src/indexer/template/TemplateIndexer.ts | 40 ++++++++++ src/indexer/template/types.ts | 4 + 12 files changed, 280 insertions(+), 71 deletions(-) create mode 100644 src/common/GetMagentoPath.ts create mode 100644 src/completion/xml/TemplateCompletionProvider.ts create mode 100644 src/definition/xml/TemplateDefinitionProvider.ts create mode 100644 src/indexer/template/TemplateIndexData.ts create mode 100644 src/indexer/template/TemplateIndexer.ts create mode 100644 src/indexer/template/types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 720eb03..765f8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how - Added: Module hover information - Added: Added extension config fields for enabling/disabling completions, definitions and hovers - Added: acl.xml indexer, definitions, autocomplete and hovers +- Added: template file indexer, definitions and autocomplete - Added: Index data persistance - Changed: Adjusted namespace indexer logic diff --git a/src/command/CopyMagentoPathCommand.ts b/src/command/CopyMagentoPathCommand.ts index 6a1cf9b..4b9f045 100644 --- a/src/command/CopyMagentoPathCommand.ts +++ b/src/command/CopyMagentoPathCommand.ts @@ -1,82 +1,20 @@ import { Command } from 'command/Command'; -import IndexManager from 'indexer/IndexManager'; -import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import GetMagentoPath from 'common/GetMagentoPath'; import { Uri, window, env } from 'vscode'; export default class CopyMagentoPathCommand extends Command { - public static readonly TEMPLATE_EXTENSIONS = ['.phtml']; - public static readonly WEB_EXTENSIONS = ['.css', '.js']; - public static readonly IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; - - private static readonly TEMPLATE_PATHS = [ - 'view/frontend/templates/', - 'view/adminhtml/templates/', - 'view/base/templates/', - 'templates/', - ]; - - private static readonly WEB_PATHS = [ - 'view/frontend/web/', - 'view/adminhtml/web/', - 'view/base/web/', - 'web/', - ]; - constructor() { super('magento-toolbox.copyMagentoPath'); } public async execute(file: Uri): Promise { - if (!file) { - return; - } - - const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); - - if (!moduleIndexData) { - return; - } - - const module = moduleIndexData.getModuleByUri(file); - - if (!module) { - return; - } + const magentoPath = GetMagentoPath.getMagentoPath(file); - const paths = this.getPaths(file); - - if (!paths) { + if (!magentoPath) { return; } - const pathIndex = paths.findIndex(p => file.fsPath.lastIndexOf(p) !== -1); - - if (pathIndex === -1) { - return; - } - - const endIndex = file.fsPath.lastIndexOf(paths[pathIndex]); - const offset = paths[pathIndex].length; - const relativePath = file.fsPath.slice(offset + endIndex); - - const magentoPath = `${module.name}::${relativePath}`; - await env.clipboard.writeText(magentoPath); window.showInformationMessage(`Copied: ${magentoPath}`); } - - private getPaths(file: Uri): string[] | undefined { - if (CopyMagentoPathCommand.TEMPLATE_EXTENSIONS.some(ext => file.fsPath.endsWith(ext))) { - return CopyMagentoPathCommand.TEMPLATE_PATHS; - } - - if ( - CopyMagentoPathCommand.WEB_EXTENSIONS.some(ext => file.fsPath.endsWith(ext)) || - CopyMagentoPathCommand.IMAGE_EXTENSIONS.some(ext => file.fsPath.endsWith(ext)) - ) { - return CopyMagentoPathCommand.WEB_PATHS; - } - - return undefined; - } } diff --git a/src/common/Context.ts b/src/common/Context.ts index ebeef39..b369383 100644 --- a/src/common/Context.ts +++ b/src/common/Context.ts @@ -1,6 +1,6 @@ import { commands, TextEditor } from 'vscode'; import PhpDocumentParser from './php/PhpDocumentParser'; -import CopyMagentoPathCommand from 'command/CopyMagentoPathCommand'; +import GetMagentoPath from './GetMagentoPath'; export interface EditorContext { canGeneratePlugin: boolean; @@ -47,9 +47,9 @@ class Context { canGeneratePlugin: false, canGeneratePreference: false, supportedMagentoPathExtensions: [ - ...CopyMagentoPathCommand.TEMPLATE_EXTENSIONS, - ...CopyMagentoPathCommand.WEB_EXTENSIONS, - ...CopyMagentoPathCommand.IMAGE_EXTENSIONS, + ...GetMagentoPath.TEMPLATE_EXTENSIONS, + ...GetMagentoPath.WEB_EXTENSIONS, + ...GetMagentoPath.IMAGE_EXTENSIONS, ], }; } diff --git a/src/common/GetMagentoPath.ts b/src/common/GetMagentoPath.ts new file mode 100644 index 0000000..813483f --- /dev/null +++ b/src/common/GetMagentoPath.ts @@ -0,0 +1,78 @@ +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import { Uri } from 'vscode'; + +export class GetMagentoPath { + public readonly TEMPLATE_EXTENSIONS = ['.phtml']; + public readonly WEB_EXTENSIONS = ['.css', '.js']; + public readonly IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg']; + + public readonly TEMPLATE_PATHS = [ + 'view/frontend/templates/', + 'view/adminhtml/templates/', + 'view/base/templates/', + 'templates/', + ]; + + public readonly WEB_PATHS = [ + 'view/frontend/web/', + 'view/adminhtml/web/', + 'view/base/web/', + 'web/', + ]; + + public getMagentoPath(file: Uri): string | undefined { + if (!file) { + return; + } + + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + return; + } + + const module = moduleIndexData.getModuleByUri(file, false); + + if (!module) { + return; + } + + const paths = this.getPaths(file); + + if (!paths) { + return; + } + + const pathIndex = paths.findIndex(p => file.fsPath.lastIndexOf(p) !== -1); + + if (pathIndex === -1) { + return; + } + + const endIndex = file.fsPath.lastIndexOf(paths[pathIndex]); + const offset = paths[pathIndex].length; + const relativePath = file.fsPath.slice(offset + endIndex); + + const magentoPath = `${module.name}::${relativePath}`; + + return magentoPath; + } + + private getPaths(file: Uri): string[] | undefined { + if (this.TEMPLATE_EXTENSIONS.some(ext => file.fsPath.endsWith(ext))) { + return this.TEMPLATE_PATHS; + } + + if ( + this.WEB_EXTENSIONS.some(ext => file.fsPath.endsWith(ext)) || + this.IMAGE_EXTENSIONS.some(ext => file.fsPath.endsWith(ext)) + ) { + return this.WEB_PATHS; + } + + return undefined; + } +} + +export default new GetMagentoPath(); diff --git a/src/completion/XmlCompletionProviderProcessor.ts b/src/completion/XmlCompletionProviderProcessor.ts index bd73fda..e80a21f 100644 --- a/src/completion/XmlCompletionProviderProcessor.ts +++ b/src/completion/XmlCompletionProviderProcessor.ts @@ -7,6 +7,7 @@ import { CancellationToken } from 'vscode'; import { ModuleCompletionProvider } from './xml/ModuleCompletionProvider'; import { NamespaceCompletionProvider } from './xml/NamespaceCompletionProvider'; import { AclCompletionProvider } from './xml/AclCompletionProvider'; +import { TemplateCompletionProvider } from './xml/TemplateCompletionProvider'; export class XmlCompletionProviderProcessor extends XmlSuggestionProviderProcessor @@ -17,6 +18,7 @@ export class XmlCompletionProviderProcessor new ModuleCompletionProvider(), new NamespaceCompletionProvider(), new AclCompletionProvider(), + new TemplateCompletionProvider(), ]); } diff --git a/src/completion/xml/TemplateCompletionProvider.ts b/src/completion/xml/TemplateCompletionProvider.ts new file mode 100644 index 0000000..bcdc968 --- /dev/null +++ b/src/completion/xml/TemplateCompletionProvider.ts @@ -0,0 +1,52 @@ +import { CompletionItem, Uri, Range, TextDocument } from 'vscode'; +import IndexManager from 'indexer/IndexManager'; +import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; +import { XMLElement, XMLAttribute } from '@xml-tools/ast'; +import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; +import TemplateIndexer from 'indexer/template/TemplateIndexer'; +import { ElementAttributeMatches } from 'common/xml/suggestion/condition/ElementAttributeMatches'; + +export class TemplateCompletionProvider extends XmlSuggestionProvider { + public getFilePatterns(): string[] { + return ['**/view/**/layout/*.xml', '**/etc/**/di.xml']; + } + + public getAttributeValueConditions(): CombinedCondition[] { + return [[new AttributeNameMatches('template')]]; + } + + public getElementContentMatches(): CombinedCondition[] { + return [ + [ + new ElementAttributeMatches('name', 'template'), + new ElementAttributeMatches('xsi:type', 'string'), + ], + ]; + } + + public getConfigKey(): string | undefined { + return 'provideXmlDefinitions'; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): CompletionItem[] { + const templateIndexData = IndexManager.getIndexData(TemplateIndexer.KEY); + + if (!templateIndexData) { + return []; + } + + const templates = templateIndexData.getTemplatesByPrefix(value); + + return templates.map(template => { + const item = new CompletionItem(template.magentoPath); + item.range = range; + return item; + }); + } +} diff --git a/src/definition/XmlDefinitionProviderProcessor.ts b/src/definition/XmlDefinitionProviderProcessor.ts index 61bdafa..d2af282 100644 --- a/src/definition/XmlDefinitionProviderProcessor.ts +++ b/src/definition/XmlDefinitionProviderProcessor.ts @@ -8,13 +8,18 @@ import { import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor'; import { AclDefinitionProvider } from './xml/AclDefinitionProvider'; import { ModuleDefinitionProvider } from './xml/ModuleDefinitionProvider'; +import { TemplateDefinitionProvider } from './xml/TemplateDefinitionProvider'; export class XmlDefinitionProviderProcessor extends XmlSuggestionProviderProcessor implements DefinitionProvider { public constructor() { - super([new AclDefinitionProvider(), new ModuleDefinitionProvider()]); + super([ + new AclDefinitionProvider(), + new ModuleDefinitionProvider(), + new TemplateDefinitionProvider(), + ]); } public async provideDefinition( diff --git a/src/definition/xml/TemplateDefinitionProvider.ts b/src/definition/xml/TemplateDefinitionProvider.ts new file mode 100644 index 0000000..3651789 --- /dev/null +++ b/src/definition/xml/TemplateDefinitionProvider.ts @@ -0,0 +1,60 @@ +import { LocationLink, Uri, Range, TextDocument } from 'vscode'; +import IndexManager from 'indexer/IndexManager'; +import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; +import { XMLElement, XMLAttribute } from '@xml-tools/ast'; +import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; +import TemplateIndexer from 'indexer/template/TemplateIndexer'; +import { ElementAttributeMatches } from 'common/xml/suggestion/condition/ElementAttributeMatches'; + +export class TemplateDefinitionProvider extends XmlSuggestionProvider { + public getFilePatterns(): string[] { + return ['**/view/**/layout/*.xml', '**/etc/**/di.xml']; + } + + public getAttributeValueConditions(): CombinedCondition[] { + return [[new AttributeNameMatches('template')]]; + } + + public getElementContentMatches(): CombinedCondition[] { + return [ + [ + new ElementAttributeMatches('name', 'template'), + new ElementAttributeMatches('xsi:type', 'string'), + ], + ]; + } + + public getConfigKey(): string | undefined { + return 'provideXmlDefinitions'; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): LocationLink[] { + const templateIndexData = IndexManager.getIndexData(TemplateIndexer.KEY); + + if (!templateIndexData) { + return []; + } + + const templates = templateIndexData.getTemplatesByPrefix(value); + + if (!templates.length) { + return []; + } + + return templates.map(template => { + const templateUri = Uri.file(template.path); + + return { + targetUri: templateUri, + targetRange: new Range(0, 0, 0, 0), + originSelectionRange: range, + }; + }); + } +} diff --git a/src/indexer/IndexManager.ts b/src/indexer/IndexManager.ts index cbecf67..3bb1acf 100644 --- a/src/indexer/IndexManager.ts +++ b/src/indexer/IndexManager.ts @@ -16,13 +16,16 @@ import Logger from 'util/Logger'; import { IndexerKey } from 'types/indexer'; import AclIndexer from './acl/AclIndexer'; import { AclIndexData } from './acl/AclIndexData'; +import TemplateIndexer from './template/TemplateIndexer'; +import { TemplateIndexData } from './template/TemplateIndexData'; type IndexerInstance = | DiIndexer | ModuleIndexer | AutoloadNamespaceIndexer | EventsIndexer - | AclIndexer; + | AclIndexer + | TemplateIndexer; type IndexerDataMap = { [DiIndexer.KEY]: DiIndexData; @@ -30,6 +33,7 @@ type IndexerDataMap = { [AutoloadNamespaceIndexer.KEY]: AutoloadNamespaceIndexData; [EventsIndexer.KEY]: EventsIndexData; [AclIndexer.KEY]: AclIndexData; + [TemplateIndexer.KEY]: TemplateIndexData; }; class IndexManager { @@ -43,6 +47,7 @@ class IndexManager { new AutoloadNamespaceIndexer(), new EventsIndexer(), new AclIndexer(), + new TemplateIndexer(), ]; this.indexStorage = new IndexStorage(); } @@ -173,6 +178,9 @@ class IndexManager { case AclIndexer.KEY: return new AclIndexData(data) as IndexerDataMap[T]; + case TemplateIndexer.KEY: + return new TemplateIndexData(data) as IndexerDataMap[T]; + default: return undefined; } diff --git a/src/indexer/template/TemplateIndexData.ts b/src/indexer/template/TemplateIndexData.ts new file mode 100644 index 0000000..78c13e2 --- /dev/null +++ b/src/indexer/template/TemplateIndexData.ts @@ -0,0 +1,21 @@ +import { Memoize } from 'typescript-memoize'; +import { Template } from './types'; +import { AbstractIndexData } from 'indexer/AbstractIndexData'; +import TemplateIndexer from './TemplateIndexer'; + +export class TemplateIndexData extends AbstractIndexData { + @Memoize({ + tags: [TemplateIndexer.KEY], + }) + public getTemplates(): Template[] { + return Array.from(this.data.values()).flat(); + } + + public getTemplate(magentoPath: string): Template | undefined { + return this.getTemplates().find(template => template.magentoPath === magentoPath); + } + + public getTemplatesByPrefix(prefix: string): Template[] { + return this.getTemplates().filter(template => template.magentoPath.startsWith(prefix)); + } +} diff --git a/src/indexer/template/TemplateIndexer.ts b/src/indexer/template/TemplateIndexer.ts new file mode 100644 index 0000000..5dedf18 --- /dev/null +++ b/src/indexer/template/TemplateIndexer.ts @@ -0,0 +1,40 @@ +import { RelativePattern, Uri } from 'vscode'; +import { Indexer } from 'indexer/Indexer'; +import { IndexerKey } from 'types/indexer'; +import { Template } from './types'; +import GetMagentoPath from 'common/GetMagentoPath'; + +export default class TemplateIndexer extends Indexer { + public static readonly KEY = 'template'; + + public getVersion(): number { + return 1; + } + + public getId(): IndexerKey { + return TemplateIndexer.KEY; + } + + public getName(): string { + return 'templates'; + } + + public getPattern(uri: Uri): RelativePattern { + return new RelativePattern(uri, '**/view/**/templates/**/*.phtml'); + } + + public async indexFile(uri: Uri): Promise { + const magentoPath = GetMagentoPath.getMagentoPath(uri); + + if (!magentoPath) { + return []; + } + + return [ + { + path: uri.fsPath, + magentoPath, + }, + ]; + } +} diff --git a/src/indexer/template/types.ts b/src/indexer/template/types.ts new file mode 100644 index 0000000..a96925c --- /dev/null +++ b/src/indexer/template/types.ts @@ -0,0 +1,4 @@ +export interface Template { + path: string; + magentoPath: string; +}