diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b52498..720eb03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] - Added: Class namespace autocomplete in XML files - Added: Module name autocomplete in module.xml files +- 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: Index data persistance - Changed: Adjusted namespace indexer logic diff --git a/src/common/xml/XmlSuggestionProvider.ts b/src/common/xml/XmlSuggestionProvider.ts new file mode 100644 index 0000000..73f763e --- /dev/null +++ b/src/common/xml/XmlSuggestionProvider.ts @@ -0,0 +1,146 @@ +import { minimatch } from 'minimatch'; +import { Position, TextDocument, Range } from 'vscode'; +import { + getSuggestions, + SuggestionProviders, + AttributeValueCompletionOptions, + ElementContentCompletionOptions, +} from '@xml-tools/content-assist'; +import Config from 'common/Config'; +import { TokenData } from 'common/xml/XmlDocumentParser'; +import { XMLElement } from '@xml-tools/ast'; +import { XMLAttribute } from '@xml-tools/ast'; +import { MatchCondition } from './suggestion/condition/MatchCondition'; + +export type CombinedCondition = MatchCondition[]; + +export abstract class XmlSuggestionProvider { + public abstract getFilePatterns(): string[]; + public abstract getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): T[]; + + public getConfigKey(): string | undefined { + return undefined; + } + + public getAttributeValueConditions(): CombinedCondition[] { + return []; + } + + public getElementContentMatches(): CombinedCondition[] { + return []; + } + + public async provideSuggestions( + document: TextDocument, + position: Position, + tokenData: TokenData + ): Promise { + if (!this.canProvideSuggestions(document)) { + return []; + } + + return this.processSuggestions(document, position, tokenData); + } + + public getSuggestionProviders(document: TextDocument): SuggestionProviders { + return { + attributeValue: [options => this.getAttributeValueSuggestionProviders(document, options)], + elementContent: [options => this.getElementContentSuggestionProviders(document, options)], + }; + } + + public getAttributeValueSuggestionProviders( + document: TextDocument, + { element, attribute }: AttributeValueCompletionOptions + ): T[] { + const match = this.getAttributeValueConditions().find(matchElement => { + return this.matchesConditions(matchElement, element, attribute); + }); + + if (!match) { + return []; + } + + const value = attribute?.value || ''; + + const range = attribute + ? new Range( + attribute.position.startLine - 1, + attribute.position.startColumn + 1 + (attribute.key?.length ?? 0), + attribute.position.endLine - 1, + attribute.position.endColumn - 1 + ) + : new Range(0, 0, 0, 0); + + return this.getSuggestionItems(value, range, document, element, attribute); + } + + public getElementContentSuggestionProviders( + document: TextDocument, + { element }: ElementContentCompletionOptions + ): T[] { + const match = this.getElementContentMatches().find(matchElement => { + return this.matchesConditions(matchElement, element); + }); + + if (!match) { + return []; + } + + const textContents = element.textContents.length > 0 ? element.textContents[0] : null; + const elementValue = textContents?.text ?? ''; + + const range = textContents + ? new Range( + textContents.position.startLine - 1, + textContents.position.startColumn - 1, + textContents.position.endLine - 1, + textContents.position.endColumn + ) + : new Range(0, 0, 0, 0); + + return this.getSuggestionItems(elementValue, range, document, element); + } + + protected matchesConditions( + conditions: CombinedCondition, + element: XMLElement, + attribute?: XMLAttribute + ): boolean { + return conditions.every(condition => condition.match(element, attribute)); + } + + protected processSuggestions( + document: TextDocument, + position: Position, + tokenData: TokenData + ): T[] { + const suggestions = getSuggestions({ + ...tokenData, + offset: document.offsetAt(position), + providers: this.getSuggestionProviders(document), + }); + + return suggestions; + } + + public canProvideSuggestions(document: TextDocument): boolean { + if (this.getConfigKey()) { + const provideXmlSuggestions = Config.get(this.getConfigKey()!); + + if (!provideXmlSuggestions) { + return false; + } + } + + return this.getFilePatterns().some(pattern => + minimatch(document.uri.fsPath, pattern, { matchBase: true }) + ); + } +} diff --git a/src/common/xml/XmlSuggestionProviderProcessor.ts b/src/common/xml/XmlSuggestionProviderProcessor.ts new file mode 100644 index 0000000..bbc3587 --- /dev/null +++ b/src/common/xml/XmlSuggestionProviderProcessor.ts @@ -0,0 +1,40 @@ +import { CancellationToken, Position, TextDocument } from 'vscode'; +import XmlDocumentParser, { TokenData } from 'common/xml/XmlDocumentParser'; +import { XmlSuggestionProvider } from './XmlSuggestionProvider'; + +export abstract class XmlSuggestionProviderProcessor { + public constructor(private providers: XmlSuggestionProvider[]) {} + + public async provideSuggestions( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + if (!this.providers.some(provider => provider.canProvideSuggestions(document))) { + return []; + } + + const tokenData = await XmlDocumentParser.parse(document, true); + + const providerCompletionItems = await Promise.all( + this.providers.map(provider => + this.getProviderCompletionItems(provider, document, position, tokenData) + ) + ); + + return providerCompletionItems.flat(); + } + + protected async getProviderCompletionItems( + provider: XmlSuggestionProvider, + document: TextDocument, + position: Position, + tokenData: TokenData + ): Promise { + if (!provider.canProvideSuggestions(document)) { + return []; + } + + return provider.provideSuggestions(document, position, tokenData); + } +} diff --git a/src/common/xml/suggestion/condition/AttributeNameMatches.ts b/src/common/xml/suggestion/condition/AttributeNameMatches.ts new file mode 100644 index 0000000..f177e28 --- /dev/null +++ b/src/common/xml/suggestion/condition/AttributeNameMatches.ts @@ -0,0 +1,14 @@ +import { XMLAttribute, XMLElement } from '@xml-tools/ast'; +import { MatchCondition } from './MatchCondition'; + +export class AttributeNameMatches implements MatchCondition { + public constructor(private readonly attributeName: string) {} + + public match(element: XMLElement, attribute?: XMLAttribute): boolean { + if (!attribute) { + return false; + } + + return attribute.key === this.attributeName; + } +} diff --git a/src/common/xml/suggestion/condition/ElementAttributeMatches.ts b/src/common/xml/suggestion/condition/ElementAttributeMatches.ts new file mode 100644 index 0000000..49141a4 --- /dev/null +++ b/src/common/xml/suggestion/condition/ElementAttributeMatches.ts @@ -0,0 +1,15 @@ +import { XMLElement } from '@xml-tools/ast'; +import { MatchCondition } from './MatchCondition'; + +export class ElementAttributeMatches implements MatchCondition { + public constructor( + private readonly attributeName: string, + private readonly attributeValue: string + ) {} + + public match(element: XMLElement): boolean { + return element.attributes.some( + attr => attr.key === this.attributeName && attr.value === this.attributeValue + ); + } +} diff --git a/src/common/xml/suggestion/condition/ElementNameMatches.ts b/src/common/xml/suggestion/condition/ElementNameMatches.ts new file mode 100644 index 0000000..2ade1fa --- /dev/null +++ b/src/common/xml/suggestion/condition/ElementNameMatches.ts @@ -0,0 +1,10 @@ +import { XMLElement } from '@xml-tools/ast'; +import { MatchCondition } from './MatchCondition'; + +export class ElementNameMatches implements MatchCondition { + public constructor(private readonly elementName: string) {} + + public match(element: XMLElement): boolean { + return element.name === this.elementName; + } +} diff --git a/src/common/xml/suggestion/condition/MatchCondition.ts b/src/common/xml/suggestion/condition/MatchCondition.ts new file mode 100644 index 0000000..bc9ed5e --- /dev/null +++ b/src/common/xml/suggestion/condition/MatchCondition.ts @@ -0,0 +1,5 @@ +import { XMLAttribute, XMLElement } from '@xml-tools/ast'; + +export interface MatchCondition { + match(element: XMLElement, attribute?: XMLAttribute): boolean; +} diff --git a/src/common/xml/suggestion/condition/ParentElementNameMatches.ts b/src/common/xml/suggestion/condition/ParentElementNameMatches.ts new file mode 100644 index 0000000..bdcb9c5 --- /dev/null +++ b/src/common/xml/suggestion/condition/ParentElementNameMatches.ts @@ -0,0 +1,14 @@ +import { XMLElement } from '@xml-tools/ast'; +import { MatchCondition } from './MatchCondition'; + +export class ParentElementNameMatches implements MatchCondition { + public constructor(private readonly elementName: string) {} + + public match(element: XMLElement): boolean { + if (element.parent.type === 'XMLElement') { + return element.parent.name === this.elementName; + } + + return false; + } +} diff --git a/src/completion/XmlCompletionProvider.ts b/src/completion/XmlCompletionProvider.ts deleted file mode 100644 index ab06965..0000000 --- a/src/completion/XmlCompletionProvider.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { minimatch } from 'minimatch'; -import { - CancellationToken, - CompletionItem, - CompletionItemProvider, - CompletionList, - Position, - TextDocument, - Range, -} from 'vscode'; -import { getSuggestions, SuggestionProviders } from '@xml-tools/content-assist'; -import XmlDocumentParser, { TokenData } from 'common/xml/XmlDocumentParser'; -import Config from 'common/Config'; -import { ModuleCompletionItemProvider } from './xml/ModuleCompletionItemProvider'; -import { NamespaceCompletionItemProvider } from './xml/NamespaceCompletionItemProvider'; -import { XmlCompletionItemProvider } from './xml/XmlCompletionItemProvider'; - -export class XmlCompletionProvider implements CompletionItemProvider { - private readonly providers: XmlCompletionItemProvider[]; - - public constructor() { - this.providers = [new ModuleCompletionItemProvider(), new NamespaceCompletionItemProvider()]; - } - - public async provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken - ): Promise { - if (!this.providers.some(provider => provider.canProvideCompletion(document))) { - return []; - } - - const tokenData = await XmlDocumentParser.parse(document, true); - - const providerCompletionItems = await Promise.all( - this.providers.map(provider => - this.getProviderCompletionItems(provider, document, position, tokenData) - ) - ); - - return providerCompletionItems.flat(); - } - - private async getProviderCompletionItems( - provider: XmlCompletionItemProvider, - document: TextDocument, - position: Position, - tokenData: TokenData - ): Promise { - if (!provider.canProvideCompletion(document)) { - return []; - } - - return provider.getCompletions(document, position, tokenData); - } -} diff --git a/src/completion/XmlCompletionProviderProcessor.ts b/src/completion/XmlCompletionProviderProcessor.ts new file mode 100644 index 0000000..bd73fda --- /dev/null +++ b/src/completion/XmlCompletionProviderProcessor.ts @@ -0,0 +1,30 @@ +import { CompletionItemProvider } from 'vscode'; + +import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor'; +import { CompletionItem, Position, TextDocument } from 'vscode'; + +import { CancellationToken } from 'vscode'; +import { ModuleCompletionProvider } from './xml/ModuleCompletionProvider'; +import { NamespaceCompletionProvider } from './xml/NamespaceCompletionProvider'; +import { AclCompletionProvider } from './xml/AclCompletionProvider'; + +export class XmlCompletionProviderProcessor + extends XmlSuggestionProviderProcessor + implements CompletionItemProvider +{ + public constructor() { + super([ + new ModuleCompletionProvider(), + new NamespaceCompletionProvider(), + new AclCompletionProvider(), + ]); + } + + public async provideCompletionItems( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + return this.provideSuggestions(document, position, token); + } +} diff --git a/src/completion/xml/AclCompletionProvider.ts b/src/completion/xml/AclCompletionProvider.ts new file mode 100644 index 0000000..ee39bb2 --- /dev/null +++ b/src/completion/xml/AclCompletionProvider.ts @@ -0,0 +1,54 @@ +import { TextDocument, CompletionItem, CompletionItemKind, Range } from 'vscode'; +import AclIndexer from 'indexer/acl/AclIndexer'; +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 { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; + +export class AclCompletionProvider extends XmlSuggestionProvider { + public getFilePatterns(): string[] { + return ['**/etc/acl.xml', '**/etc/webapi.xml', '**/etc/**/system.xml']; + } + + public getAttributeValueConditions(): CombinedCondition[] { + return [ + [new ElementNameMatches('resource'), new AttributeNameMatches('id')], + [new ElementNameMatches('resource'), new AttributeNameMatches('ref')], + ]; + } + + public getElementContentMatches(): CombinedCondition[] { + return [[new ElementNameMatches('resource')]]; + } + + public getConfigKey(): string | undefined { + return 'provideXmlDefinitions'; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): CompletionItem[] { + const aclIndexData = IndexManager.getIndexData(AclIndexer.KEY); + + if (!aclIndexData) { + return []; + } + + const acls = aclIndexData.getAclsByPrefix(value); + + if (!acls) { + return []; + } + + return acls.map(acl => { + const item = new CompletionItem(acl.id, CompletionItemKind.Value); + item.range = range; + return item; + }); + } +} diff --git a/src/completion/xml/ModuleCompletionItemProvider.ts b/src/completion/xml/ModuleCompletionItemProvider.ts deleted file mode 100644 index 78bfed0..0000000 --- a/src/completion/xml/ModuleCompletionItemProvider.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { CompletionItem, CompletionItemKind } from 'vscode'; -import { SuggestionProviders } from '@xml-tools/content-assist'; -import IndexManager from 'indexer/IndexManager'; -import ModuleIndexer from 'indexer/module/ModuleIndexer'; -import { XMLElement, XMLAttribute } from '@xml-tools/ast'; -import { XmlCompletionItemProvider } from './XmlCompletionItemProvider'; - -export class ModuleCompletionItemProvider extends XmlCompletionItemProvider { - getFilePatterns(): string[] { - return ['**/etc/module.xml']; - } - - getCompletionProviders(): SuggestionProviders { - return { - attributeValue: [this.getAttributeValueCompletions.bind(this)], - }; - } - - private getAttributeValueCompletions({ - element, - attribute, - }: { - element: XMLElement; - attribute: XMLAttribute; - }): CompletionItem[] { - if ( - element.name !== 'module' || - (element.parent as XMLElement)?.name !== 'sequence' || - attribute.key !== 'name' - ) { - return []; - } - - const value = attribute?.value || ''; - return this.getCompletionItems(value); - } - - private getCompletionItems(prefix: string): CompletionItem[] { - const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); - - if (!moduleIndexData) { - return []; - } - - const completions = moduleIndexData.getModulesByPrefix(prefix); - - return completions.map(module => { - return new CompletionItem(module.name, CompletionItemKind.Value); - }); - } -} diff --git a/src/completion/xml/ModuleCompletionProvider.ts b/src/completion/xml/ModuleCompletionProvider.ts new file mode 100644 index 0000000..d3ddd38 --- /dev/null +++ b/src/completion/xml/ModuleCompletionProvider.ts @@ -0,0 +1,48 @@ +import { CompletionItem, CompletionItemKind, Range, TextDocument } from 'vscode'; +import IndexManager from 'indexer/IndexManager'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import { XmlSuggestionProvider, CombinedCondition } from 'common/xml/XmlSuggestionProvider'; +import { XMLAttribute } from '@xml-tools/ast'; +import { XMLElement } from '@xml-tools/ast'; +import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; +import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; +import { ParentElementNameMatches } from 'common/xml/suggestion/condition/ParentElementNameMatches'; + +export class ModuleCompletionProvider extends XmlSuggestionProvider { + public getFilePatterns(): string[] { + return ['**/etc/module.xml']; + } + + public getConfigKey(): string | undefined { + return 'provideXmlCompletions'; + } + + public getAttributeValueConditions(): CombinedCondition[] { + return [ + [new ElementNameMatches('module'), new AttributeNameMatches('name')], + [new ElementNameMatches('module'), new ParentElementNameMatches('route')], + ]; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): CompletionItem[] { + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + return []; + } + + const completions = moduleIndexData.getModulesByPrefix(value); + + return completions.map(module => { + const item = new CompletionItem(module.name, CompletionItemKind.Value); + item.range = range; + return item; + }); + } +} diff --git a/src/completion/xml/NamespaceCompletionItemProvider.ts b/src/completion/xml/NamespaceCompletionItemProvider.ts deleted file mode 100644 index fe2b624..0000000 --- a/src/completion/xml/NamespaceCompletionItemProvider.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { CompletionItem, CompletionItemKind } from 'vscode'; -import { SuggestionProviders } from '@xml-tools/content-assist'; -import IndexManager from 'indexer/IndexManager'; -import AutoloadNamespaceIndexer from 'indexer/autoload-namespace/AutoloadNamespaceIndexer'; -import { XMLElement, XMLAttribute } from '@xml-tools/ast'; -import { XmlCompletionItemProvider } from './XmlCompletionItemProvider'; - -interface AttributeValueMatch { - element?: string; - attributeName: string; -} - -interface ElementContentMatch { - element?: string; - attributeName?: string; - attributeValue?: string; -} - -export class NamespaceCompletionItemProvider extends XmlCompletionItemProvider { - private static readonly ATTRIBUTE_VALUE_MATCHERS: AttributeValueMatch[] = [ - { - element: 'preference', - attributeName: 'for', - }, - { - element: 'preference', - attributeName: 'type', - }, - { - element: 'type', - attributeName: 'name', - }, - { - element: 'plugin', - attributeName: 'type', - }, - { - element: 'virtualType', - attributeName: 'type', - }, - { - attributeName: 'instance', - }, - { - attributeName: 'class', - }, - { - element: 'attribute', - attributeName: 'type', - }, - { - element: 'extension_attributes', - attributeName: 'for', - }, - { - element: 'consumer', - attributeName: 'handler', - }, - { - element: 'queue', - attributeName: 'handler', - }, - { - element: 'handler', - attributeName: 'type', - }, - ]; - - private static readonly ELEMENT_CONTENT_MATCHERS: ElementContentMatch[] = [ - { - attributeName: 'xsi:type', - attributeValue: 'object', - }, - { - element: 'backend_model', - }, - { - element: 'frontend_model', - }, - { - element: 'source_model', - }, - ]; - - getFilePatterns(): string[] { - return ['**/etc/**/*.xml']; - } - - getCompletionProviders(): SuggestionProviders { - return { - attributeValue: [this.getAttributeValueCompletions.bind(this)], - elementContent: [this.getElementContentCompletions.bind(this)], - }; - } - - private getAttributeValueCompletions({ - element, - attribute, - }: { - element: XMLElement; - attribute: XMLAttribute; - }): CompletionItem[] { - const match = NamespaceCompletionItemProvider.ATTRIBUTE_VALUE_MATCHERS.find(matchElement => { - if (matchElement.element && matchElement.element !== element.name) { - return false; - } - - return matchElement.attributeName === attribute.key; - }); - - if (!match) { - return []; - } - - const value = attribute?.value || ''; - return this.getCompletionItems(value); - } - - private getElementContentCompletions({ element }: { element: XMLElement }): CompletionItem[] { - const match = NamespaceCompletionItemProvider.ELEMENT_CONTENT_MATCHERS.find(matchElement => { - if (matchElement.element && matchElement.element !== element.name) { - return false; - } - - if (matchElement.attributeName && matchElement.attributeValue) { - return element.attributes.some( - attribute => - attribute.key === matchElement.attributeName && - attribute.value === matchElement.attributeValue - ); - } - - return true; - }); - - if (!match) { - return []; - } - - const elementContent = - element.textContents.length > 0 ? (element.textContents[0].text ?? '') : ''; - - return this.getCompletionItems(elementContent); - } - - private getCompletionItems(prefix: string): CompletionItem[] { - const namespaceIndexData = IndexManager.getIndexData(AutoloadNamespaceIndexer.KEY); - - if (!namespaceIndexData) { - return []; - } - - const completions = namespaceIndexData.findNamespacesByPrefix(prefix); - - return completions.map(namespace => { - return new CompletionItem(namespace.fqn, CompletionItemKind.Value); - }); - } -} diff --git a/src/completion/xml/NamespaceCompletionProvider.ts b/src/completion/xml/NamespaceCompletionProvider.ts new file mode 100644 index 0000000..d181e91 --- /dev/null +++ b/src/completion/xml/NamespaceCompletionProvider.ts @@ -0,0 +1,66 @@ +import { CompletionItem, CompletionItemKind, Range, TextDocument } from 'vscode'; +import IndexManager from 'indexer/IndexManager'; +import AutoloadNamespaceIndexer from 'indexer/autoload-namespace/AutoloadNamespaceIndexer'; +import { XMLElement, XMLAttribute } from '@xml-tools/ast'; +import { CombinedCondition, XmlSuggestionProvider } from 'common/xml/XmlSuggestionProvider'; +import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; +import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; +import { ElementAttributeMatches } from 'common/xml/suggestion/condition/ElementAttributeMatches'; + +export class NamespaceCompletionProvider extends XmlSuggestionProvider { + public getAttributeValueConditions(): CombinedCondition[] { + return [ + [new ElementNameMatches('preference'), new AttributeNameMatches('for')], + [new ElementNameMatches('preference'), new AttributeNameMatches('type')], + [new ElementNameMatches('type'), new AttributeNameMatches('name')], + [new ElementNameMatches('plugin'), new AttributeNameMatches('type')], + [new ElementNameMatches('virtualType'), new AttributeNameMatches('type')], + [new AttributeNameMatches('instance')], + [new AttributeNameMatches('class')], + [new ElementNameMatches('attribute'), new AttributeNameMatches('type')], + [new ElementNameMatches('extension_attributes'), new AttributeNameMatches('for')], + [new ElementNameMatches('consumer'), new AttributeNameMatches('handler')], + [new ElementNameMatches('queue'), new AttributeNameMatches('handler')], + [new ElementNameMatches('handler'), new AttributeNameMatches('type')], + ]; + } + + public getElementContentMatches(): CombinedCondition[] { + return [ + [new ElementAttributeMatches('xsi:type', 'object')], + [new ElementNameMatches('backend_model')], + [new ElementNameMatches('frontend_model')], + [new ElementNameMatches('source_model')], + ]; + } + + public getFilePatterns(): string[] { + return ['**/etc/**/*.xml']; + } + + public getConfigKey(): string | undefined { + return 'provideXmlCompletions'; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): CompletionItem[] { + const namespaceIndexData = IndexManager.getIndexData(AutoloadNamespaceIndexer.KEY); + + if (!namespaceIndexData) { + return []; + } + + const completions = namespaceIndexData.findNamespacesByPrefix(value); + + return completions.map(namespace => { + const item = new CompletionItem(namespace.fqn, CompletionItemKind.Value); + item.range = range; + return item; + }); + } +} diff --git a/src/completion/xml/XmlCompletionItemProvider.ts b/src/completion/xml/XmlCompletionItemProvider.ts deleted file mode 100644 index 4cc00ca..0000000 --- a/src/completion/xml/XmlCompletionItemProvider.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { minimatch } from 'minimatch'; -import { CompletionItem, Position, TextDocument, Range } from 'vscode'; -import { getSuggestions, SuggestionProviders } from '@xml-tools/content-assist'; -import { TokenData } from 'common/xml/XmlDocumentParser'; -import Config from 'common/Config'; - -export abstract class XmlCompletionItemProvider { - public abstract getFilePatterns(): string[]; - public abstract getCompletionProviders(): SuggestionProviders; - - public async getCompletions( - document: TextDocument, - position: Position, - tokenData: TokenData - ): Promise { - const offset = document.offsetAt(position); - - const completions = getSuggestions({ - ...tokenData, - offset, - providers: this.getCompletionProviders(), - }); - - return this.fixCompletionItemRanges(document, position, completions); - } - - private fixCompletionItemRanges( - document: TextDocument, - position: Position, - completions: CompletionItem[] - ): CompletionItem[] { - const range = document.getWordRangeAtPosition(position, /("[^"]+")|(>[^<]+<)/); - - if (range) { - const rangeWithoutQuotes = new Range( - range.start.with({ character: range.start.character + 1 }), - range.end.with({ character: range.end.character - 1 }) - ); - - for (const completion of completions) { - completion.range = rangeWithoutQuotes; - } - } - - return completions; - } - - public canProvideCompletion(document: TextDocument): boolean { - const provideXmlCompletions = Config.get('provideXmlCompletions'); - - if (!provideXmlCompletions) { - return false; - } - - return this.getFilePatterns().some(pattern => - minimatch(document.uri.fsPath, pattern, { matchBase: true }) - ); - } -} diff --git a/src/definition/XmlDefinitionProvider.ts b/src/definition/XmlDefinitionProvider.ts deleted file mode 100644 index 3ccf11b..0000000 --- a/src/definition/XmlDefinitionProvider.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { minimatch } from 'minimatch'; -import { - CancellationToken, - DefinitionProvider, - LocationLink, - Position, - TextDocument, -} from 'vscode'; -import { getSuggestions, SuggestionProviders } from '@xml-tools/content-assist'; -import XmlDocumentParser from 'common/xml/XmlDocumentParser'; -import Config from 'common/Config'; - -export abstract class XmlDefinitionProvider implements DefinitionProvider { - public abstract getFilePatterns(): string[]; - public abstract getDefinitionProviders(): SuggestionProviders; - - public async provideDefinition( - document: TextDocument, - position: Position, - token: CancellationToken - ): Promise { - if (!this.canProvideDefinition(document)) { - return []; - } - - const { cst, tokenVector, ast } = await XmlDocumentParser.parse(document); - - const definitions = getSuggestions({ - ast, - cst, - tokenVector, - offset: document.offsetAt(position), - providers: this.getDefinitionProviders(), - }); - - return definitions.filter(definition => definition.targetUri.fsPath !== document.uri.fsPath); - } - - private canProvideDefinition(document: TextDocument): boolean { - const provideXmlDefinitions = Config.get('provideXmlDefinitions'); - - if (!provideXmlDefinitions) { - return false; - } - - return this.getFilePatterns().some(pattern => - minimatch(document.uri.fsPath, pattern, { matchBase: true }) - ); - } -} diff --git a/src/definition/XmlDefinitionProviderProcessor.ts b/src/definition/XmlDefinitionProviderProcessor.ts new file mode 100644 index 0000000..61bdafa --- /dev/null +++ b/src/definition/XmlDefinitionProviderProcessor.ts @@ -0,0 +1,28 @@ +import { + CancellationToken, + DefinitionProvider, + LocationLink, + Position, + TextDocument, +} from 'vscode'; +import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor'; +import { AclDefinitionProvider } from './xml/AclDefinitionProvider'; +import { ModuleDefinitionProvider } from './xml/ModuleDefinitionProvider'; + +export class XmlDefinitionProviderProcessor + extends XmlSuggestionProviderProcessor + implements DefinitionProvider +{ + public constructor() { + super([new AclDefinitionProvider(), new ModuleDefinitionProvider()]); + } + + public async provideDefinition( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + const definitions = await this.provideSuggestions(document, position, token); + return definitions.filter(definition => definition.targetUri.fsPath !== document.uri.fsPath); + } +} diff --git a/src/definition/XmlModuleDefinitionProvider.ts b/src/definition/XmlModuleDefinitionProvider.ts deleted file mode 100644 index 7b706f6..0000000 --- a/src/definition/XmlModuleDefinitionProvider.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { AttributeValueCompletionOptions, SuggestionProviders } from '@xml-tools/content-assist'; -import { XmlDefinitionProvider } from './XmlDefinitionProvider'; -import { LocationLink, Uri, Range } from 'vscode'; -import ModuleIndexer from 'indexer/module/ModuleIndexer'; -import IndexManager from 'indexer/IndexManager'; - -export class XmlModuleDefinitionProvider extends XmlDefinitionProvider { - public getFilePatterns(): string[] { - return ['**/etc/module.xml', '**/etc/**/routes.xml']; - } - - public getDefinitionProviders(): SuggestionProviders { - return { - attributeValue: [this.getModuleDefinitions], - }; - } - - private getModuleDefinitions({ - element, - attribute, - }: AttributeValueCompletionOptions): LocationLink[] { - if (element.name !== 'module' || attribute.key !== 'name') { - return []; - } - - const moduleName = attribute.value; - - if (!moduleName) { - return []; - } - - const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); - - if (!moduleIndexData) { - return []; - } - - const module = moduleIndexData.getModule(moduleName); - - if (!module) { - return []; - } - - const moduleXmlUri = Uri.file(module.moduleXmlPath); - - return [ - { - targetUri: moduleXmlUri, - targetRange: new Range(0, 0, 0, 0), - originSelectionRange: new Range( - attribute.position.startLine - 1, - attribute.position.startColumn + 5, - attribute.position.endLine - 1, - attribute.position.endColumn - 1 - ), - }, - ]; - } -} diff --git a/src/definition/xml/AclDefinitionProvider.ts b/src/definition/xml/AclDefinitionProvider.ts new file mode 100644 index 0000000..95b1449 --- /dev/null +++ b/src/definition/xml/AclDefinitionProvider.ts @@ -0,0 +1,58 @@ +import { LocationLink, Uri, Range, TextDocument } from 'vscode'; +import AclIndexer from 'indexer/acl/AclIndexer'; +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 { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; + +export class AclDefinitionProvider extends XmlSuggestionProvider { + public getFilePatterns(): string[] { + return ['**/etc/acl.xml', '**/etc/webapi.xml', '**/etc/**/system.xml']; + } + + public getAttributeValueConditions(): CombinedCondition[] { + return [ + [new ElementNameMatches('resource'), new AttributeNameMatches('id')], + [new ElementNameMatches('resource'), new AttributeNameMatches('ref')], + ]; + } + + public getElementContentMatches(): CombinedCondition[] { + return [[new ElementNameMatches('resource')]]; + } + + public getConfigKey(): string | undefined { + return 'provideXmlDefinitions'; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): LocationLink[] { + const aclIndexData = IndexManager.getIndexData(AclIndexer.KEY); + + if (!aclIndexData) { + return []; + } + + const acl = aclIndexData.getAcl(value); + + if (!acl) { + return []; + } + + const aclXmlUri = Uri.file(acl.path); + + return [ + { + targetUri: aclXmlUri, + targetRange: new Range(0, 0, 0, 0), + originSelectionRange: range, + }, + ]; + } +} diff --git a/src/definition/xml/ModuleDefinitionProvider.ts b/src/definition/xml/ModuleDefinitionProvider.ts new file mode 100644 index 0000000..4322fe5 --- /dev/null +++ b/src/definition/xml/ModuleDefinitionProvider.ts @@ -0,0 +1,60 @@ +import { LocationLink, Uri, Range, TextDocument } from 'vscode'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import IndexManager from 'indexer/IndexManager'; +import { CombinedCondition, XmlSuggestionProvider } from 'common/xml/XmlSuggestionProvider'; +import { XMLElement, XMLAttribute } from '@xml-tools/ast'; +import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; +import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; + +export class ModuleDefinitionProvider extends XmlSuggestionProvider { + public getFilePatterns(): string[] { + return ['**/etc/module.xml', '**/etc/**/routes.xml']; + } + + public getAttributeValueConditions(): CombinedCondition[] { + return [[new ElementNameMatches('module'), new AttributeNameMatches('name')]]; + } + + public getConfigKey(): string | undefined { + return 'provideXmlDefinitions'; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): LocationLink[] { + if (!attribute) { + return []; + } + + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + return []; + } + + const module = moduleIndexData.getModule(value); + + if (!module) { + return []; + } + + const moduleXmlUri = Uri.file(module.moduleXmlPath); + + return [ + { + targetUri: moduleXmlUri, + targetRange: new Range(0, 0, 0, 0), + originSelectionRange: new Range( + attribute.position.startLine - 1, + attribute.position.startColumn + 5, + attribute.position.endLine - 1, + attribute.position.endColumn - 1 + ), + }, + ]; + } +} diff --git a/src/extension.ts b/src/extension.ts index 408175f..3f0ea06 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,8 +13,9 @@ import Magento from 'util/Magento'; import { WorkspaceFolder } from 'vscode'; import Logger from 'util/Logger'; import { Command } from 'command/Command'; -import { XmlModuleDefinitionProvider } from 'definition/XmlModuleDefinitionProvider'; -import { XmlCompletionProvider } from 'completion/XmlCompletionProvider'; +import { XmlDefinitionProviderProcessor } from 'definition/XmlDefinitionProviderProcessor'; +import { XmlCompletionProviderProcessor } from 'completion/XmlCompletionProviderProcessor'; +import { XmlHoverProviderProcessor } from 'hover/XmlHoverProviderProcessor'; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed @@ -93,7 +94,7 @@ export async function activate(context: vscode.ExtensionContext) { // definition providers context.subscriptions.push( vscode.languages.registerDefinitionProvider('xml', new XmlClasslikeDefinitionProvider()), - vscode.languages.registerDefinitionProvider('xml', new XmlModuleDefinitionProvider()) + vscode.languages.registerDefinitionProvider('xml', new XmlDefinitionProviderProcessor()) ); // codelens providers @@ -105,14 +106,15 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.languages.registerCompletionItemProvider( { language: 'xml', scheme: 'file' }, - new XmlCompletionProvider(), + new XmlCompletionProviderProcessor(), ...'\\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ) ); // hover providers context.subscriptions.push( - vscode.languages.registerHoverProvider('xml', new XmlClasslikeHoverProvider()) + vscode.languages.registerHoverProvider('xml', new XmlClasslikeHoverProvider()), + vscode.languages.registerHoverProvider('xml', new XmlHoverProviderProcessor()) ); await activeTextEditorChangeObserver.execute(vscode.window.activeTextEditor); diff --git a/src/hover/XmlHoverProviderProcessor.ts b/src/hover/XmlHoverProviderProcessor.ts new file mode 100644 index 0000000..38279c7 --- /dev/null +++ b/src/hover/XmlHoverProviderProcessor.ts @@ -0,0 +1,26 @@ +import { CancellationToken, Hover, Position, TextDocument } from 'vscode'; +import { XmlSuggestionProviderProcessor } from 'common/xml/XmlSuggestionProviderProcessor'; +import { AclHoverProvider } from 'hover/xml/AclHoverProvider'; +import { ModuleHoverProvider } from 'hover/xml/ModuleHoverProvider'; + +export class XmlHoverProviderProcessor extends XmlSuggestionProviderProcessor { + public constructor() { + super([new AclHoverProvider(), new ModuleHoverProvider()]); + } + + public async provideHover( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + const suggestions = await this.provideSuggestions(document, position, token); + + const suggestion = suggestions.length > 0 ? suggestions[0] : null; + + if (!suggestion) { + return null; + } + + return suggestion; + } +} diff --git a/src/hover/xml/AclHoverProvider.ts b/src/hover/xml/AclHoverProvider.ts new file mode 100644 index 0000000..2996e64 --- /dev/null +++ b/src/hover/xml/AclHoverProvider.ts @@ -0,0 +1,72 @@ +import { Hover, MarkdownString, Uri, Range, TextDocument } from 'vscode'; +import AclIndexer from 'indexer/acl/AclIndexer'; +import IndexManager from 'indexer/IndexManager'; +import { CombinedCondition, XmlSuggestionProvider } from 'common/xml/XmlSuggestionProvider'; +import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; +import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; +import { XMLElement, XMLAttribute } from '@xml-tools/ast'; + +export class AclHoverProvider extends XmlSuggestionProvider { + public getAttributeValueConditions(): CombinedCondition[] { + return [ + [new ElementNameMatches('resource'), new AttributeNameMatches('id')], + [new ElementNameMatches('resource'), new AttributeNameMatches('ref')], + ]; + } + + public getElementContentMatches(): CombinedCondition[] { + return [[new ElementNameMatches('resource')]]; + } + + public getConfigKey(): string | undefined { + return 'provideXmlHovers'; + } + + public getFilePatterns(): string[] { + return ['**/etc/acl.xml', '**/etc/webapi.xml', '**/etc/**/system.xml']; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): Hover[] { + const aclIndexData = IndexManager.getIndexData(AclIndexer.KEY); + + if (!aclIndexData) { + return []; + } + + const acl = aclIndexData.getAcl(value); + + if (!acl) { + return []; + } + + const markdown = new MarkdownString(); + markdown.appendMarkdown(`**ACL**: ${acl.title}\n\n`); + markdown.appendMarkdown(`- ID: \`${acl.id}\`\n\n`); + + if (acl.description) { + markdown.appendMarkdown(`${acl.description}\n\n`); + } + + if (acl.disabled) { + markdown.appendMarkdown(`- **Disabled**\n\n`); + } + + if (acl.sortOrder) { + markdown.appendMarkdown(`- Sort Order: ${acl.sortOrder}\n\n`); + } + + if (acl.parent) { + markdown.appendMarkdown(`- Parent ID: \`${acl.parent}\`\n\n`); + } + + markdown.appendMarkdown(`[acl.xml](${Uri.file(acl.path)})`); + + return [new Hover(markdown, range)]; + } +} diff --git a/src/hover/xml/ModuleHoverProvider.ts b/src/hover/xml/ModuleHoverProvider.ts new file mode 100644 index 0000000..093a340 --- /dev/null +++ b/src/hover/xml/ModuleHoverProvider.ts @@ -0,0 +1,80 @@ +import { Hover, MarkdownString, Uri, Range, workspace, TextDocument } from 'vscode'; +import AclIndexer from 'indexer/acl/AclIndexer'; +import IndexManager from 'indexer/IndexManager'; +import { CombinedCondition, XmlSuggestionProvider } from 'common/xml/XmlSuggestionProvider'; +import { AttributeNameMatches } from 'common/xml/suggestion/condition/AttributeNameMatches'; +import { ElementNameMatches } from 'common/xml/suggestion/condition/ElementNameMatches'; +import { XMLElement, XMLAttribute } from '@xml-tools/ast'; +import ModuleIndexer from 'indexer/module/ModuleIndexer'; +import { ParentElementNameMatches } from 'common/xml/suggestion/condition/ParentElementNameMatches'; +import path from 'path'; + +export class ModuleHoverProvider extends XmlSuggestionProvider { + public getAttributeValueConditions(): CombinedCondition[] { + return [ + [ + new ElementNameMatches('module'), + new AttributeNameMatches('name'), + new ParentElementNameMatches('sequence'), + ], + [ + new ElementNameMatches('module'), + new AttributeNameMatches('name'), + new ParentElementNameMatches('route'), + ], + ]; + } + + public getConfigKey(): string | undefined { + return 'provideXmlHovers'; + } + + public getFilePatterns(): string[] { + return ['**/etc/module.xml', '**/etc/**/routes.xml']; + } + + public getSuggestionItems( + value: string, + range: Range, + document: TextDocument, + element: XMLElement, + attribute?: XMLAttribute + ): Hover[] { + const moduleIndexData = IndexManager.getIndexData(ModuleIndexer.KEY); + + if (!moduleIndexData) { + return []; + } + + const module = moduleIndexData.getModule(value); + + if (!module) { + return []; + } + + const markdown = new MarkdownString(); + markdown.appendMarkdown(`**Module**: ${module.name}\n\n`); + + if (module.version) { + markdown.appendMarkdown(`- Version: \`${module.version}\`\n\n`); + } + + const workspaceFolder = workspace.getWorkspaceFolder(document.uri); + + if (!workspaceFolder) { + return []; + } + + const relativePath = path.relative(workspaceFolder.uri.fsPath, module.moduleXmlPath); + + markdown.appendMarkdown(`- Path: \`${relativePath}\`\n\n`); + + if (module.sequence) { + markdown.appendMarkdown(`- Sequence: \n\n - ${module.sequence.join('\n - ')}\n\n`); + } + + markdown.appendMarkdown(`[module.xml](${Uri.file(module.moduleXmlPath)})`); + + return [new Hover(markdown, range)]; + } +} diff --git a/src/indexer/IndexManager.ts b/src/indexer/IndexManager.ts index a890259..cbecf67 100644 --- a/src/indexer/IndexManager.ts +++ b/src/indexer/IndexManager.ts @@ -14,14 +14,22 @@ import { AutoloadNamespaceIndexData } from './autoload-namespace/AutoloadNamespa import { EventsIndexData } from './events/EventsIndexData'; import Logger from 'util/Logger'; import { IndexerKey } from 'types/indexer'; +import AclIndexer from './acl/AclIndexer'; +import { AclIndexData } from './acl/AclIndexData'; -type IndexerInstance = DiIndexer | ModuleIndexer | AutoloadNamespaceIndexer | EventsIndexer; +type IndexerInstance = + | DiIndexer + | ModuleIndexer + | AutoloadNamespaceIndexer + | EventsIndexer + | AclIndexer; type IndexerDataMap = { [DiIndexer.KEY]: DiIndexData; [ModuleIndexer.KEY]: ModuleIndexData; [AutoloadNamespaceIndexer.KEY]: AutoloadNamespaceIndexData; [EventsIndexer.KEY]: EventsIndexData; + [AclIndexer.KEY]: AclIndexData; }; class IndexManager { @@ -34,6 +42,7 @@ class IndexManager { new ModuleIndexer(), new AutoloadNamespaceIndexer(), new EventsIndexer(), + new AclIndexer(), ]; this.indexStorage = new IndexStorage(); } @@ -148,23 +157,25 @@ class IndexManager { return undefined; } - if (id === DiIndexer.KEY) { - return new DiIndexData(data) as IndexerDataMap[T]; - } + switch (id) { + case DiIndexer.KEY: + return new DiIndexData(data) as IndexerDataMap[T]; - if (id === ModuleIndexer.KEY) { - return new ModuleIndexData(data) as IndexerDataMap[T]; - } + case ModuleIndexer.KEY: + return new ModuleIndexData(data) as IndexerDataMap[T]; - if (id === AutoloadNamespaceIndexer.KEY) { - return new AutoloadNamespaceIndexData(data) as IndexerDataMap[T]; - } + case AutoloadNamespaceIndexer.KEY: + return new AutoloadNamespaceIndexData(data) as IndexerDataMap[T]; - if (id === EventsIndexer.KEY) { - return new EventsIndexData(data) as IndexerDataMap[T]; - } + case EventsIndexer.KEY: + return new EventsIndexData(data) as IndexerDataMap[T]; + + case AclIndexer.KEY: + return new AclIndexData(data) as IndexerDataMap[T]; - return undefined; + default: + return undefined; + } } protected async indexFileInner( diff --git a/src/indexer/acl/AclIndexData.ts b/src/indexer/acl/AclIndexData.ts new file mode 100644 index 0000000..99476cc --- /dev/null +++ b/src/indexer/acl/AclIndexData.ts @@ -0,0 +1,21 @@ +import { Memoize } from 'typescript-memoize'; +import { Acl } from './types'; +import { AbstractIndexData } from 'indexer/AbstractIndexData'; +import AclIndexer from './AclIndexer'; + +export class AclIndexData extends AbstractIndexData { + @Memoize({ + tags: [AclIndexer.KEY], + }) + public getAcls(): Acl[] { + return Array.from(this.data.values()).flat(); + } + + public getAcl(aclId: string): Acl | undefined { + return this.getAcls().find(acl => acl.id === aclId); + } + + public getAclsByPrefix(prefix: string): Acl[] { + return this.getAcls().filter(acl => acl.id.startsWith(prefix)); + } +} diff --git a/src/indexer/acl/AclIndexer.ts b/src/indexer/acl/AclIndexer.ts new file mode 100644 index 0000000..f6fc0fc --- /dev/null +++ b/src/indexer/acl/AclIndexer.ts @@ -0,0 +1,104 @@ +import { RelativePattern, Uri } from 'vscode'; +import { XMLParser } from 'fast-xml-parser'; +import { get, map } from 'lodash-es'; +import { Indexer } from 'indexer/Indexer'; +import FileSystem from 'util/FileSystem'; +import { IndexerKey } from 'types/indexer'; +import { Acl } from './types'; + +interface Element { + '@_id'?: string; + '@_title'?: string; + '@_description'?: string; + '@_sortOrder'?: string; + '@_disabled'?: string; + resource?: Element[]; +} + +export default class AclIndexer extends Indexer { + public static readonly KEY = 'acl'; + + private xmlParser: XMLParser; + + public constructor() { + super(); + + this.xmlParser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + isArray: name => { + return name === 'resource'; + }, + }); + } + + public getVersion(): number { + return 1; + } + + public getId(): IndexerKey { + return AclIndexer.KEY; + } + + public getName(): string { + return 'acl.xml'; + } + + public getPattern(uri: Uri): RelativePattern { + return new RelativePattern(uri, '**/etc/acl.xml'); + } + + public async indexFile(uri: Uri): Promise { + const xml = await FileSystem.readFile(uri); + + const parsed = this.xmlParser.parse(xml); + + const events = get(parsed, 'config.acl.resources.resource', []); + + const acls = []; + + for (const event of events) { + acls.push(...this.getResources(event, uri)); + } + + return acls; + } + + private getResources(element: Element, uri: Uri, parent?: string): Acl[] { + const resources = element.resource ?? []; + + const acls: Acl[] = []; + + if (this.isUniqueAcl(element)) { + acls.push(this.getAclData(element, uri, parent)); + } + + for (const resource of resources) { + if (Array.isArray(resource.resource)) { + acls.push(...this.getResources(resource, uri, element['@_id'])); + } + + if (this.isUniqueAcl(resource)) { + acls.push(this.getAclData(resource, uri, element['@_id'])); + } + } + + return acls; + } + + private isUniqueAcl(element: Element): boolean { + return !!element['@_id'] && !!element['@_title']; + } + + private getAclData(element: Element, uri: Uri, parent?: string): Acl { + return { + id: element['@_id']!, + title: element['@_title']!, + description: element['@_description'], + sortOrder: element['@_sortOrder'] ? parseInt(element['@_sortOrder'], 10) : undefined, + disabled: element['@_disabled'] === 'true', + parent, + path: uri.fsPath, + }; + } +} diff --git a/src/indexer/acl/types.ts b/src/indexer/acl/types.ts new file mode 100644 index 0000000..8cf0c8d --- /dev/null +++ b/src/indexer/acl/types.ts @@ -0,0 +1,9 @@ +export interface Acl { + id: string; + path: string; + title: string; + description?: string; + sortOrder?: number; + disabled?: boolean; + parent?: string; +}