diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index 86467690c..4837ddaf9 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -178,8 +178,11 @@
-
-
+
+
+
+
+
@@ -209,6 +212,9 @@
+
+
+
diff --git a/src/com/magento/idea/magento2plugin/completion/provider/xml/ConfigXmlTagNameProvider.java b/src/com/magento/idea/magento2plugin/completion/provider/xml/ConfigXmlTagNameProvider.java
new file mode 100644
index 000000000..5bc913b52
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/completion/provider/xml/ConfigXmlTagNameProvider.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.completion.provider.xml;
+
+import static com.magento.idea.magento2plugin.magento.files.ModuleConfigXmlFile.FILE_NAME;
+import static com.magento.idea.magento2plugin.magento.files.ModuleConfigXmlFile.ROOT_TAG_NAME;
+
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.xml.XmlTagNameProvider;
+import com.magento.idea.magento2plugin.project.Settings;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class ConfigXmlTagNameProvider implements XmlTagNameProvider {
+
+ @Override
+ public void addTagNameVariants(
+ final List elements,
+ final @NotNull XmlTag tag,
+ final String prefix
+ ) {
+ if (!Settings.isEnabled(tag.getProject()) || !validate(tag)) {
+ return;
+ }
+ final int tagDepth = getTagDepth(tag);
+
+ if (tagDepth == -1) {
+ return;
+ }
+ final ConfigTagScope tagScope = getTagScope(tag);
+
+ if (tagScope == null) {
+ return;
+ }
+ final ConfigTagTypeByScopedDepth configType = ConfigTagTypeByScopedDepth
+ .getTagTypeByScopeAndDepth(tagDepth, tagScope);
+
+ if (configType == null) {
+ return;
+ }
+ final String indexKey = configType.getConfigPath(tag);
+ final String phrase = indexKey.isEmpty() ? prefix : indexKey + "." + prefix;
+
+ elements.addAll(configType.makeCompletion(phrase, tag.getProject()));
+ }
+
+ private boolean validate(final @NotNull XmlTag tag) {
+ if (!(tag.getContainingFile() instanceof XmlFile)) {
+ return false;
+ }
+ final XmlFile file = (XmlFile) tag.getContainingFile();
+ final XmlTag rootTag = file.getRootTag();
+
+ if (!FILE_NAME.equals(file.getName())
+ || rootTag == null
+ || !ROOT_TAG_NAME.equals(rootTag.getName())) {
+ return false;
+ }
+ final int tagDepth = getTagDepth(tag);
+
+ return tagDepth >= 2 && tagDepth <= 5;
+ }
+
+ private int getTagDepth(final @NotNull XmlTag tag) {
+ if (!(tag.getContainingFile() instanceof XmlFile)) {
+ return -1;
+ }
+ final XmlFile file = (XmlFile) tag.getContainingFile();
+ final XmlTag rootTag = file.getRootTag();
+
+ if (rootTag == null) {
+ return -1;
+ }
+ int depth = 0;
+ XmlTag iterable = tag;
+
+ while (!iterable.equals(rootTag)) {
+ depth++;
+ iterable = iterable.getParentTag();
+
+ if (iterable == null) {
+ break;
+ }
+ }
+
+ return depth;
+ }
+
+ private @Nullable ConfigTagScope getTagScope(final @NotNull XmlTag tag) {
+ if (!(tag.getContainingFile() instanceof XmlFile)) {
+ return null;
+ }
+ final XmlFile file = (XmlFile) tag.getContainingFile();
+ final XmlTag rootTag = file.getRootTag();
+
+ if (rootTag == null) {
+ return null;
+ }
+ XmlTag scopeTag = null;
+ XmlTag iterable = tag;
+
+ while (!iterable.equals(rootTag)) {
+ scopeTag = iterable;
+ iterable = iterable.getParentTag();
+
+ if (iterable == null) {
+ break;
+ }
+ }
+
+ if (scopeTag == null) {
+ return null;
+ }
+
+ return ConfigTagScope.getScopeByTagName(scopeTag.getName());
+ }
+
+ private enum ConfigTagTypeByScopedDepth {
+
+ SECTION(2, 3, 3, 0),
+ GROUP(3, 4, 4, 1),
+ FIELD(4, 5, 5, 2);
+
+ private final int defaultScope;
+ private final int websitesScope;
+ private final int storesScope;
+ private final int fallbackDepth;
+
+ ConfigTagTypeByScopedDepth(
+ final int defaultScope,
+ final int websitesScope,
+ final int storesScope,
+ final int fallbackDepth
+ ) {
+ this.defaultScope = defaultScope;
+ this.websitesScope = websitesScope;
+ this.storesScope = storesScope;
+ this.fallbackDepth = fallbackDepth;
+ }
+
+ public static ConfigTagTypeByScopedDepth getTagTypeByScopeAndDepth(
+ final int depth,
+ final ConfigTagScope scope
+ ) {
+ for (final ConfigTagTypeByScopedDepth tagType : ConfigTagTypeByScopedDepth.values()) {
+ int depthForScope = -1;
+
+ if (ConfigTagScope.DEFAULT.equals(scope)) {
+ depthForScope = tagType.defaultScope;
+ } else if (ConfigTagScope.WEBSITES.equals(scope)) {
+ depthForScope = tagType.websitesScope;
+ } else if (ConfigTagScope.STORES.equals(scope)) {
+ depthForScope = tagType.storesScope;
+ }
+
+ if (depth == depthForScope) {
+ return tagType;
+ }
+ }
+
+ return null;
+ }
+
+ public @NotNull String getConfigPath(final @NotNull XmlTag tag) {
+ final List parts = new ArrayList<>();
+ XmlTag iterable = tag.getParentTag();
+
+ for (int i = 0; i < fallbackDepth; i++) {
+ if (iterable != null) {
+ parts.add(iterable.getName());
+ iterable = iterable.getParentTag();
+ }
+ }
+
+ if (parts.isEmpty()) {
+ return "";
+ }
+ Collections.reverse(parts);
+
+ return String.join(".", parts);
+ }
+
+ public List makeCompletion(
+ final @NotNull String phrase,
+ final @NotNull Project project
+ ) {
+ if (this.equals(ConfigTagTypeByScopedDepth.SECTION)) {
+ return SectionNameCompletionProvider.makeCompletion(phrase, project);
+ } else if (this.equals(ConfigTagTypeByScopedDepth.GROUP)) {
+ return GroupNameCompletionProvider.makeCompletion(phrase, project);
+ } else if (this.equals(ConfigTagTypeByScopedDepth.FIELD)) {
+ return FieldNameCompletionProvider.makeCompletion(phrase, project);
+ }
+
+ return new ArrayList<>();
+ }
+ }
+
+ private enum ConfigTagScope {
+
+ DEFAULT("default"),
+ WEBSITES("websites"),
+ STORES("stores");
+
+ private final String scopeTagName;
+
+ ConfigTagScope(final String scopeTagName) {
+ this.scopeTagName = scopeTagName;
+ }
+
+ public static ConfigTagScope getScopeByTagName(final @NotNull String tagName) {
+ for (final ConfigTagScope scope : ConfigTagScope.values()) {
+ if (tagName.equals(scope.scopeTagName)) {
+ return scope;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/completion/provider/xml/FieldNameCompletionProvider.java b/src/com/magento/idea/magento2plugin/completion/provider/xml/FieldNameCompletionProvider.java
new file mode 100644
index 000000000..a8c101901
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/completion/provider/xml/FieldNameCompletionProvider.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.completion.provider.xml;
+
+import com.intellij.codeInsight.completion.CompletionParameters;
+import com.intellij.codeInsight.completion.CompletionProvider;
+import com.intellij.codeInsight.completion.CompletionResultSet;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.util.ProcessingContext;
+import com.intellij.util.indexing.FileBasedIndex;
+import com.magento.idea.magento2plugin.MagentoIcons;
+import com.magento.idea.magento2plugin.project.Settings;
+import com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlFieldIndex;
+import com.magento.idea.magento2plugin.util.magento.xml.SystemConfigurationParserUtil;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.jetbrains.annotations.NotNull;
+
+public class FieldNameCompletionProvider extends CompletionProvider {
+
+ @Override
+ protected void addCompletions(
+ final @NotNull CompletionParameters parameters,
+ final @NotNull ProcessingContext context,
+ final @NotNull CompletionResultSet result
+ ) {
+ final PsiElement position = parameters.getPosition().getOriginalElement();
+
+ if (position == null || !Settings.isEnabled(position.getProject())) {
+ return;
+ }
+ final XmlTag startingTag = PsiTreeUtil.getParentOfType(
+ position,
+ XmlTag.class
+ );
+
+ if (startingTag == null) {
+ return;
+ }
+ final String currentPath = SystemConfigurationParserUtil
+ .parseOuterConfigPath(
+ startingTag,
+ SystemConfigurationParserUtil.ParsingDepth.GROUP_ID
+ );
+
+ if (currentPath == null) {
+ return;
+ }
+ final String prefix = result.getPrefixMatcher().getPrefix().trim();
+ final String capture = currentPath + "." + prefix;
+
+ for (final LookupElement element : makeCompletion(capture, position.getProject())) {
+ result.addElement(element);
+ }
+ }
+
+ /**
+ * Make completion for fields.
+ *
+ * @param phrase String
+ * @param project Project
+ *
+ * @return List[LookupElement]
+ */
+ public static List makeCompletion(
+ final @NotNull String phrase,
+ final @NotNull Project project
+ ) {
+ final List result = new ArrayList<>();
+ final FileBasedIndex index = FileBasedIndex.getInstance();
+ final Collection allKeys = index.getAllKeys(SystemXmlFieldIndex.KEY, project);
+ final AtomicInteger resultsCounter = new AtomicInteger(0);
+
+ allKeys.stream()
+ .filter(input -> input.startsWith(phrase) && !input.equals(phrase))
+ .takeWhile(input -> resultsCounter.get() < 5)
+ .forEach(
+ input -> {
+ if (index.getValues(
+ SystemXmlFieldIndex.KEY,
+ input,
+ GlobalSearchScope.allScope(project)).isEmpty()
+ ) {
+ return;
+ }
+ final String[] pathParts = input.split("\\.");
+
+ if (pathParts.length != 3) { //NOPMD
+ return;
+ }
+ final String fieldId = pathParts[2];
+ result.add(
+ LookupElementBuilder.create(fieldId)
+ .withIcon(MagentoIcons.MODULE)
+ );
+ resultsCounter.incrementAndGet();
+ }
+ );
+
+ return result;
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/completion/provider/xml/GroupNameCompletionProvider.java b/src/com/magento/idea/magento2plugin/completion/provider/xml/GroupNameCompletionProvider.java
new file mode 100644
index 000000000..dd5ac371a
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/completion/provider/xml/GroupNameCompletionProvider.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.completion.provider.xml;
+
+import com.intellij.codeInsight.completion.CompletionParameters;
+import com.intellij.codeInsight.completion.CompletionProvider;
+import com.intellij.codeInsight.completion.CompletionResultSet;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.util.ProcessingContext;
+import com.intellij.util.indexing.FileBasedIndex;
+import com.magento.idea.magento2plugin.MagentoIcons;
+import com.magento.idea.magento2plugin.project.Settings;
+import com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlGroupIndex;
+import com.magento.idea.magento2plugin.util.magento.xml.SystemConfigurationParserUtil;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.jetbrains.annotations.NotNull;
+
+public class GroupNameCompletionProvider extends CompletionProvider {
+
+ @Override
+ protected void addCompletions(
+ final @NotNull CompletionParameters parameters,
+ final @NotNull ProcessingContext context,
+ final @NotNull CompletionResultSet result
+ ) {
+ final PsiElement position = parameters.getPosition().getOriginalElement();
+
+ if (position == null || !Settings.isEnabled(position.getProject())) {
+ return;
+ }
+ final XmlTag startingTag = PsiTreeUtil.getParentOfType(
+ position,
+ XmlTag.class
+ );
+ if (startingTag == null) {
+ return;
+ }
+ final String currentPath = SystemConfigurationParserUtil
+ .parseOuterConfigPath(
+ startingTag,
+ SystemConfigurationParserUtil.ParsingDepth.SECTION_ID
+ );
+
+ if (currentPath == null) {
+ return;
+ }
+ final String prefix = result.getPrefixMatcher().getPrefix().trim();
+ final String capture = currentPath + "." + prefix;
+
+ for (final LookupElement element : makeCompletion(capture, position.getProject())) {
+ result.addElement(element);
+ }
+ }
+
+ /**
+ * Make completion for groups.
+ *
+ * @param phrase String
+ * @param project Project
+ *
+ * @return List[LookupElement]
+ */
+ public static List makeCompletion(
+ final @NotNull String phrase,
+ final @NotNull Project project
+ ) {
+ final List result = new ArrayList<>();
+ final Collection allKeys = FileBasedIndex.getInstance().getAllKeys(
+ SystemXmlGroupIndex.KEY,
+ project
+ );
+ final AtomicInteger resultsCounter = new AtomicInteger(0);
+
+ allKeys.stream()
+ .filter(input -> input.startsWith(phrase) && !input.equals(phrase))
+ .takeWhile(input -> resultsCounter.get() < 5)
+ .forEach(
+ input -> {
+ if (FileBasedIndex.getInstance().getValues(
+ SystemXmlGroupIndex.KEY,
+ input,
+ GlobalSearchScope.allScope(project)).isEmpty()
+ ) {
+ return;
+ }
+ final String[] groupNameParts = input.split("\\.");
+
+ if (groupNameParts.length != 2) { //NOPMD
+ return;
+ }
+ final String groupId = groupNameParts[1];
+ result.add(
+ LookupElementBuilder.create(groupId)
+ .withIcon(MagentoIcons.MODULE)
+ );
+ resultsCounter.incrementAndGet();
+ }
+ );
+
+ return result;
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/completion/provider/xml/SectionNameCompletionProvider.java b/src/com/magento/idea/magento2plugin/completion/provider/xml/SectionNameCompletionProvider.java
new file mode 100644
index 000000000..2858b298c
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/completion/provider/xml/SectionNameCompletionProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.completion.provider.xml;
+
+import com.intellij.codeInsight.completion.CompletionParameters;
+import com.intellij.codeInsight.completion.CompletionProvider;
+import com.intellij.codeInsight.completion.CompletionResultSet;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.ProcessingContext;
+import com.intellij.util.indexing.FileBasedIndex;
+import com.magento.idea.magento2plugin.MagentoIcons;
+import com.magento.idea.magento2plugin.project.Settings;
+import com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlSectionIndex;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.jetbrains.annotations.NotNull;
+
+public class SectionNameCompletionProvider extends CompletionProvider {
+
+ @Override
+ protected void addCompletions(
+ final @NotNull CompletionParameters parameters,
+ final @NotNull ProcessingContext context,
+ final @NotNull CompletionResultSet result
+ ) {
+ final PsiElement position = parameters.getPosition().getOriginalElement();
+
+ if (position == null || !Settings.isEnabled(position.getProject())) {
+ return;
+ }
+ final String prefix = result.getPrefixMatcher().getPrefix().trim();
+
+ for (final LookupElement element : makeCompletion(prefix, position.getProject())) {
+ result.addElement(element);
+ }
+ }
+
+ /**
+ * Make completion for sections.
+ *
+ * @param phrase String
+ * @param project Project
+ *
+ * @return List[LookupElement]
+ */
+ public static List makeCompletion(
+ final @NotNull String phrase,
+ final @NotNull Project project
+ ) {
+ final List result = new ArrayList<>();
+ final Collection allKeys = FileBasedIndex.getInstance().getAllKeys(
+ SystemXmlSectionIndex.KEY,
+ project
+ );
+ final AtomicInteger resultsCounter = new AtomicInteger(0);
+
+ allKeys.stream()
+ .filter(input -> input.startsWith(phrase) && !input.equals(phrase))
+ .takeWhile(input -> resultsCounter.get() < 5)
+ .forEach(
+ input -> {
+ if (FileBasedIndex.getInstance().getValues(
+ SystemXmlSectionIndex.KEY,
+ input,
+ GlobalSearchScope.allScope(project)).isEmpty()
+ ) {
+ return;
+ }
+ result.add(LookupElementBuilder
+ .create(input).withIcon(MagentoIcons.MODULE));
+ resultsCounter.incrementAndGet();
+ }
+ );
+
+ return result;
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/completion/xml/SystemConfigurationContributor.java b/src/com/magento/idea/magento2plugin/completion/xml/SystemConfigurationContributor.java
new file mode 100644
index 000000000..532ef8862
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/completion/xml/SystemConfigurationContributor.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.completion.xml;
+
+import com.intellij.codeInsight.completion.CompletionContributor;
+import com.intellij.codeInsight.completion.CompletionType;
+import com.intellij.patterns.PlatformPatterns;
+import com.intellij.patterns.PsiElementPattern;
+import com.intellij.patterns.StandardPatterns;
+import com.intellij.patterns.XmlFilePattern;
+import com.intellij.patterns.XmlNamedElementPattern;
+import com.intellij.patterns.XmlPatterns;
+import com.intellij.patterns.XmlTagPattern;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.xml.XmlTokenType;
+import com.magento.idea.magento2plugin.completion.provider.xml.FieldNameCompletionProvider;
+import com.magento.idea.magento2plugin.completion.provider.xml.GroupNameCompletionProvider;
+import com.magento.idea.magento2plugin.completion.provider.xml.SectionNameCompletionProvider;
+import com.magento.idea.magento2plugin.magento.files.ModuleConfigXmlFile;
+import com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile;
+
+public class SystemConfigurationContributor extends CompletionContributor {
+
+ /**
+ * Contributes completions to the system.xml and config.xml files.
+ */
+ @SuppressWarnings("PMD.ExcessiveMethodLength")
+ public SystemConfigurationContributor() {
+ super();
+ final XmlFilePattern.Capture systemXmlFileCapture = XmlPatterns
+ .xmlFile()
+ .withName(StandardPatterns.string().endsWith(ModuleSystemXmlFile.FILE_NAME));
+ final XmlFilePattern.Capture configXmlFileCapture = XmlPatterns
+ .xmlFile()
+ .withName(StandardPatterns.string().endsWith(ModuleConfigXmlFile.FILE_NAME));
+ final XmlNamedElementPattern.XmlAttributePattern idAttributeCapture = XmlPatterns
+ .xmlAttribute()
+ .withName("id");
+ final XmlTagPattern.Capture sectionTagCapture = XmlPatterns
+ .xmlTag()
+ .withName(ModuleSystemXmlFile.SECTION_TAG_NAME);
+ final XmlTagPattern.Capture groupTagCapture = XmlPatterns
+ .xmlTag()
+ .withName(ModuleSystemXmlFile.GROUP_TAG_NAME);
+ final XmlTagPattern.Capture fieldTagCapture = XmlPatterns
+ .xmlTag()
+ .withName(ModuleSystemXmlFile.FIELD_TAG_NAME);
+
+ // in the system.xml file
+ extend(
+ CompletionType.BASIC,
+ PlatformPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)
+ .inside(idAttributeCapture)
+ .inside(sectionTagCapture)
+ .inFile(systemXmlFileCapture)
+ .withSuperParent(3, sectionTagCapture),
+ new SectionNameCompletionProvider()
+ );
+
+ // in the system.xml file
+ extend(
+ CompletionType.BASIC,
+ PlatformPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)
+ .inside(idAttributeCapture)
+ .inside(groupTagCapture)
+ .inside(sectionTagCapture)
+ .inFile(systemXmlFileCapture)
+ .withSuperParent(3, groupTagCapture),
+ new GroupNameCompletionProvider()
+ );
+
+ // in the system.xml file
+ extend(
+ CompletionType.BASIC,
+ PlatformPatterns.psiElement(XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN)
+ .inside(idAttributeCapture)
+ .inside(fieldTagCapture)
+ .inside(groupTagCapture)
+ .inside(sectionTagCapture)
+ .inFile(systemXmlFileCapture)
+ .withSuperParent(3, fieldTagCapture),
+ new FieldNameCompletionProvider()
+ );
+
+ final PsiElementPattern.Capture tagTextInConfigFile = PlatformPatterns
+ .psiElement(XmlTokenType.XML_DATA_CHARACTERS)
+ .inside(XmlPatterns.xmlTag().withName(ModuleConfigXmlFile.ROOT_TAG_NAME))
+ .inFile(configXmlFileCapture);
+ final XmlTagPattern.Capture defaultScopeTag = XmlPatterns.xmlTag()
+ .withName(ModuleConfigXmlFile.DEFAULT_SCOPE);
+ final XmlTagPattern.Capture websiteScopeTag = XmlPatterns.xmlTag()
+ .withName(ModuleConfigXmlFile.WEBSITE_SCOPE);
+ final XmlTagPattern.Capture storeScopeTag = XmlPatterns.xmlTag()
+ .withName(ModuleConfigXmlFile.STORE_SCOPE);
+
+ // for sections in the config.xml file
+ extend(
+ CompletionType.BASIC,
+ PlatformPatterns.or(
+ tagTextInConfigFile.withSuperParent(2, defaultScopeTag),
+ tagTextInConfigFile.withSuperParent(3, websiteScopeTag),
+ tagTextInConfigFile.withSuperParent(3, storeScopeTag)
+ ),
+ new SectionNameCompletionProvider()
+ );
+
+ // for groups in the config.xml file
+ extend(
+ CompletionType.BASIC,
+ PlatformPatterns.or(
+ tagTextInConfigFile.withSuperParent(3, defaultScopeTag),
+ tagTextInConfigFile.withSuperParent(4, websiteScopeTag),
+ tagTextInConfigFile.withSuperParent(4, storeScopeTag)
+ ),
+ new GroupNameCompletionProvider()
+ );
+
+ // for fields in the config.xml file
+ extend(
+ CompletionType.BASIC,
+ PlatformPatterns.or(
+ tagTextInConfigFile.withSuperParent(4, defaultScopeTag),
+ tagTextInConfigFile.withSuperParent(5, websiteScopeTag),
+ tagTextInConfigFile.withSuperParent(5, storeScopeTag)
+ ),
+ new FieldNameCompletionProvider()
+ );
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/indexes/IndexManager.java b/src/com/magento/idea/magento2plugin/indexes/IndexManager.java
index 61c05fca5..0d48528e1 100644
--- a/src/com/magento/idea/magento2plugin/indexes/IndexManager.java
+++ b/src/com/magento/idea/magento2plugin/indexes/IndexManager.java
@@ -30,6 +30,9 @@
import com.magento.idea.magento2plugin.stubs.indexes.xml.MenuIndex;
import com.magento.idea.magento2plugin.stubs.indexes.xml.PhpClassNameIndex;
import com.magento.idea.magento2plugin.stubs.indexes.xml.ProductTypeIndex;
+import com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlFieldIndex;
+import com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlGroupIndex;
+import com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlSectionIndex;
import com.magento.idea.magento2plugin.stubs.indexes.xml.UIComponentIndex;
@SuppressWarnings({"PMD.ClassNamingConventions", "PMD.UseUtilityClass"})
@@ -39,45 +42,48 @@ public class IndexManager {
* Refresh Magento 2 indexes.
*/
public static void manualReindex() {
- final ID, ?>[] indexIds = new ID, ?>[] {//NOPMD
- // php
- ModulePackageIndex.KEY,
- // xml|di configuration
- PluginIndex.KEY,
- VirtualTypeIndex.KEY,
- DeclarativeSchemaElementsIndex.KEY,
- // layouts
- BlockNameIndex.KEY,
- ContainerNameIndex.KEY,
- UIComponentIndex.KEY,
- // events
- EventNameIndex.KEY,
- EventObserverIndex.KEY,
- // webapi
- WebApiTypeIndex.KEY,
- ModuleNameIndex.KEY,
- PhpClassNameIndex.KEY,
- //acl
- AclResourceIndex.KEY,
- //menu
- MenuIndex.KEY,
- //require_js
- RequireJsIndex.KEY,
- MagentoLibJsIndex.KEY,
- // mftf
- ActionGroupIndex.KEY,
- DataIndex.KEY,
- PageIndex.KEY,
- SectionIndex.KEY,
- TestNameIndex.KEY,
- TestExtendsIndex.KEY,
- //graphql
- GraphQlResolverIndex.KEY,
- //product types
- ProductTypeIndex.KEY
+ final ID, ?>[] indexIds = new ID, ?>[]{//NOPMD
+ // php
+ ModulePackageIndex.KEY,
+ // xml|di configuration
+ PluginIndex.KEY,
+ VirtualTypeIndex.KEY,
+ DeclarativeSchemaElementsIndex.KEY,
+ SystemXmlSectionIndex.KEY,
+ SystemXmlGroupIndex.KEY,
+ SystemXmlFieldIndex.KEY,
+ // layouts
+ BlockNameIndex.KEY,
+ ContainerNameIndex.KEY,
+ UIComponentIndex.KEY,
+ // events
+ EventNameIndex.KEY,
+ EventObserverIndex.KEY,
+ // webapi
+ WebApiTypeIndex.KEY,
+ ModuleNameIndex.KEY,
+ PhpClassNameIndex.KEY,
+ //acl
+ AclResourceIndex.KEY,
+ //menu
+ MenuIndex.KEY,
+ //require_js
+ RequireJsIndex.KEY,
+ MagentoLibJsIndex.KEY,
+ // mftf
+ ActionGroupIndex.KEY,
+ DataIndex.KEY,
+ PageIndex.KEY,
+ SectionIndex.KEY,
+ TestNameIndex.KEY,
+ TestExtendsIndex.KEY,
+ //graphql
+ GraphQlResolverIndex.KEY,
+ //product types
+ ProductTypeIndex.KEY
};
- for (final ID, ?> id: indexIds) {
+ for (final ID, ?> id : indexIds) {
try {
FileBasedIndexImpl.getInstance().requestRebuild(id);
FileBasedIndexImpl.getInstance().scheduleRebuild(id, new Throwable());//NOPMD
diff --git a/src/com/magento/idea/magento2plugin/magento/files/ModuleConfigXmlFile.java b/src/com/magento/idea/magento2plugin/magento/files/ModuleConfigXmlFile.java
index 2726ead9c..22768c73d 100644
--- a/src/com/magento/idea/magento2plugin/magento/files/ModuleConfigXmlFile.java
+++ b/src/com/magento/idea/magento2plugin/magento/files/ModuleConfigXmlFile.java
@@ -13,6 +13,11 @@ public final class ModuleConfigXmlFile implements ModuleFileInterface {
public static final String FILE_NAME = "config.xml";
public static final String TEMPLATE = "Magento Config XML";
+ public static final String ROOT_TAG_NAME = "config";
+ public static final String DEFAULT_SCOPE = "default";
+ public static final String STORE_SCOPE = "stores";
+ public static final String WEBSITE_SCOPE = "websites";
+
@Override
public String getFileName() {
return FILE_NAME;
diff --git a/src/com/magento/idea/magento2plugin/magento/files/ModuleSystemXmlFile.java b/src/com/magento/idea/magento2plugin/magento/files/ModuleSystemXmlFile.java
index 11017dd18..db7c6b807 100644
--- a/src/com/magento/idea/magento2plugin/magento/files/ModuleSystemXmlFile.java
+++ b/src/com/magento/idea/magento2plugin/magento/files/ModuleSystemXmlFile.java
@@ -18,6 +18,12 @@ public final class ModuleSystemXmlFile implements ModuleFileInterface {
public static final String XML_TAG_FRONTEND_MODEL = "frontend_model";
public static final String XML_TAG_BACKEND_MODEL = "backend_model";
+ public static final String ROOT_TAG_NAME = "config";
+ public static final String SYSTEM_TAG_NAME = "system";
+ public static final String SECTION_TAG_NAME = "section";
+ public static final String GROUP_TAG_NAME = "group";
+ public static final String FIELD_TAG_NAME = "field";
+
@Override
public String getFileName() {
return FILE_NAME;
diff --git a/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlFieldIndex.java b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlFieldIndex.java
new file mode 100644
index 000000000..85eb9450b
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlFieldIndex.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.stubs.indexes.xml;
+
+import com.intellij.ide.highlighter.XmlFileType;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlDocument;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.util.indexing.DataIndexer;
+import com.intellij.util.indexing.FileBasedIndex;
+import com.intellij.util.indexing.FileBasedIndexExtension;
+import com.intellij.util.indexing.FileContent;
+import com.intellij.util.indexing.ID;
+import com.intellij.util.io.DataExternalizer;
+import com.intellij.util.io.EnumeratorStringDescriptor;
+import com.intellij.util.io.KeyDescriptor;
+import com.intellij.util.io.VoidDataExternalizer;
+import com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile;
+import com.magento.idea.magento2plugin.project.Settings;
+import com.magento.idea.magento2plugin.util.magento.xml.SystemConfigurationParserUtil;
+import java.util.HashMap;
+import java.util.Map;
+import org.jetbrains.annotations.NotNull;
+
+public class SystemXmlFieldIndex extends FileBasedIndexExtension {
+
+ public static final ID KEY = ID.create(
+ "com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlFieldIndex"
+ );
+ private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor();
+ private final DataExternalizer myDataExternalizer = new VoidDataExternalizer();
+
+ @Override
+ public @NotNull
+ DataIndexer getIndexer() {
+ return inputData -> {
+ final Map map = new HashMap<>();
+ final PsiFile psiFile = inputData.getPsiFile();
+
+ if (!Settings.isEnabled(psiFile.getProject())
+ || !(psiFile instanceof XmlFile)
+ || !ModuleSystemXmlFile.FILE_NAME.equals(psiFile.getName())) {
+ return map;
+ }
+ final XmlFile xmlFile = (XmlFile) psiFile;
+ final XmlDocument document = xmlFile.getDocument();
+
+ if (document == null) {
+ return map;
+ }
+ final XmlTag rootTag = xmlFile.getRootTag();
+
+ if (rootTag == null) {
+ return map;
+ }
+
+ SystemConfigurationParserUtil.parseConfigurationTags(
+ rootTag,
+ SystemConfigurationParserUtil.ParsingDepth.FIELD_ID,
+ (sectionId, groupId, fieldId) -> {
+ if (sectionId == null || groupId == null || fieldId == null) {
+ return;
+ }
+ map.put(sectionId + "." + groupId + "." + fieldId, null);
+ }
+ );
+
+ return map;
+ };
+ }
+
+ @Override
+ public @NotNull FileBasedIndex.InputFilter getInputFilter() {
+ return virtualFile -> virtualFile.getFileType() == XmlFileType.INSTANCE
+ && virtualFile.getNameWithoutExtension().equals("system");
+ }
+
+ @Override
+ public @NotNull ID getName() {
+ return KEY;
+ }
+
+ @Override
+ public @NotNull KeyDescriptor getKeyDescriptor() {
+ return myKeyDescriptor;
+ }
+
+ @Override
+ public @NotNull DataExternalizer getValueExternalizer() {
+ return myDataExternalizer;
+ }
+
+ @Override
+ public boolean dependsOnFileContent() {
+ return true;
+ }
+
+ @Override
+ public int getVersion() {
+ return 1;
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlGroupIndex.java b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlGroupIndex.java
new file mode 100644
index 000000000..668693d04
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlGroupIndex.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.stubs.indexes.xml;
+
+import com.intellij.ide.highlighter.XmlFileType;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.util.indexing.DataIndexer;
+import com.intellij.util.indexing.FileBasedIndex;
+import com.intellij.util.indexing.FileBasedIndexExtension;
+import com.intellij.util.indexing.FileContent;
+import com.intellij.util.indexing.ID;
+import com.intellij.util.io.DataExternalizer;
+import com.intellij.util.io.EnumeratorStringDescriptor;
+import com.intellij.util.io.KeyDescriptor;
+import com.intellij.util.io.VoidDataExternalizer;
+import com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile;
+import com.magento.idea.magento2plugin.project.Settings;
+import com.magento.idea.magento2plugin.util.magento.xml.SystemConfigurationParserUtil;
+import java.util.HashMap;
+import java.util.Map;
+import org.jetbrains.annotations.NotNull;
+
+public class SystemXmlGroupIndex extends FileBasedIndexExtension {
+
+ public static final ID KEY = ID.create(
+ "com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlGroupIndex"
+ );
+ private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor();
+ private final DataExternalizer myDataExternalizer = new VoidDataExternalizer();
+
+ @Override
+ public @NotNull DataIndexer getIndexer() {
+ return inputData -> {
+ final Map map = new HashMap<>();
+ final PsiFile psiFile = inputData.getPsiFile();
+
+ if (!Settings.isEnabled(psiFile.getProject())
+ || !(psiFile instanceof XmlFile)
+ || !ModuleSystemXmlFile.FILE_NAME.equals(psiFile.getName())) {
+ return map;
+ }
+ final XmlFile xmlFile = (XmlFile) psiFile;
+ final XmlTag rootTag = xmlFile.getRootTag();
+
+ if (rootTag == null) {
+ return map;
+ }
+
+ SystemConfigurationParserUtil.parseConfigurationTags(
+ rootTag,
+ SystemConfigurationParserUtil.ParsingDepth.GROUP_ID,
+ (sectionId, groupId, fieldId) -> {
+ if (sectionId == null || groupId == null) {
+ return;
+ }
+ map.put(sectionId + "." + groupId, null);
+ }
+ );
+
+ return map;
+ };
+ }
+
+ @Override
+ public @NotNull FileBasedIndex.InputFilter getInputFilter() {
+ return virtualFile -> virtualFile.getFileType() == XmlFileType.INSTANCE
+ && virtualFile.getNameWithoutExtension().equals("system");
+ }
+
+ @Override
+ public @NotNull ID getName() {
+ return KEY;
+ }
+
+ @Override
+ public @NotNull KeyDescriptor getKeyDescriptor() {
+ return myKeyDescriptor;
+ }
+
+ @Override
+ public @NotNull DataExternalizer getValueExternalizer() {
+ return myDataExternalizer;
+ }
+
+ @Override
+ public boolean dependsOnFileContent() {
+ return true;
+ }
+
+ @Override
+ public int getVersion() {
+ return 1;
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlSectionIndex.java b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlSectionIndex.java
new file mode 100644
index 000000000..f11d2f6c2
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/SystemXmlSectionIndex.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.stubs.indexes.xml;
+
+import com.intellij.ide.highlighter.XmlFileType;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.xml.XmlFile;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.util.indexing.DataIndexer;
+import com.intellij.util.indexing.FileBasedIndex;
+import com.intellij.util.indexing.FileBasedIndexExtension;
+import com.intellij.util.indexing.FileContent;
+import com.intellij.util.indexing.ID;
+import com.intellij.util.io.DataExternalizer;
+import com.intellij.util.io.EnumeratorStringDescriptor;
+import com.intellij.util.io.KeyDescriptor;
+import com.intellij.util.io.VoidDataExternalizer;
+import com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile;
+import com.magento.idea.magento2plugin.project.Settings;
+import com.magento.idea.magento2plugin.util.magento.xml.SystemConfigurationParserUtil;
+import java.util.HashMap;
+import java.util.Map;
+import org.jetbrains.annotations.NotNull;
+
+public class SystemXmlSectionIndex extends FileBasedIndexExtension {
+
+ public static final ID KEY = ID.create(
+ "com.magento.idea.magento2plugin.stubs.indexes.xml.SystemXmlSectionIndex"
+ );
+ private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor();
+ private final DataExternalizer myDataExternalizer = new VoidDataExternalizer();
+
+ @Override
+ public @NotNull DataIndexer getIndexer() {
+ return inputData -> {
+ final Map map = new HashMap<>();
+ final PsiFile psiFile = inputData.getPsiFile();
+
+ if (!Settings.isEnabled(psiFile.getProject())
+ || !(psiFile instanceof XmlFile)
+ || !ModuleSystemXmlFile.FILE_NAME.equals(psiFile.getName())) {
+ return map;
+ }
+ final XmlFile xmlFile = (XmlFile) psiFile;
+ final XmlTag rootTag = xmlFile.getRootTag();
+
+ if (rootTag == null) {
+ return map;
+ }
+
+ SystemConfigurationParserUtil.parseConfigurationTags(
+ rootTag,
+ SystemConfigurationParserUtil.ParsingDepth.SECTION_ID,
+ (sectionId, groupId, fieldId) -> {
+ if (sectionId == null) {
+ return;
+ }
+ map.put(sectionId, null);
+ }
+ );
+
+ return map;
+ };
+ }
+
+ @Override
+ public @NotNull FileBasedIndex.InputFilter getInputFilter() {
+ return virtualFile -> virtualFile.getFileType() == XmlFileType.INSTANCE
+ && virtualFile.getNameWithoutExtension().equals("system");
+ }
+
+ @Override
+ public @NotNull ID getName() {
+ return KEY;
+ }
+
+ @Override
+ public @NotNull KeyDescriptor getKeyDescriptor() {
+ return myKeyDescriptor;
+ }
+
+ @Override
+ public @NotNull DataExternalizer getValueExternalizer() {
+ return myDataExternalizer;
+ }
+
+ @Override
+ public boolean dependsOnFileContent() {
+ return true;
+ }
+
+ @Override
+ public int getVersion() {
+ return 1;
+ }
+}
diff --git a/src/com/magento/idea/magento2plugin/util/magento/xml/SystemConfigurationParserUtil.java b/src/com/magento/idea/magento2plugin/util/magento/xml/SystemConfigurationParserUtil.java
new file mode 100644
index 000000000..495abda2e
--- /dev/null
+++ b/src/com/magento/idea/magento2plugin/util/magento/xml/SystemConfigurationParserUtil.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.util.magento.xml;
+
+import static com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile.FIELD_TAG_NAME;
+import static com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile.GROUP_TAG_NAME;
+import static com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile.SECTION_TAG_NAME;
+import static com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile.SYSTEM_TAG_NAME;
+import static com.magento.idea.magento2plugin.util.xml.XmlPsiTreeUtil.findSubTagsOfParent;
+
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlTag;
+import com.magento.idea.magento2plugin.magento.files.ModuleConfigXmlFile;
+import com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@SuppressWarnings("PMD.TooManyStaticImports")
+public final class SystemConfigurationParserUtil {
+
+ private SystemConfigurationParserUtil() {}
+
+ /**
+ * Parse system configuration tags (section, group, field) with the specified depth.
+ *
+ * @param rootTag XmlTag
+ * @param parsingDepth ParsingDepth
+ * @param action RunnableOnPathElement
+ */
+ public static void parseConfigurationTags(
+ final @NotNull XmlTag rootTag,
+ final ParsingDepth parsingDepth,
+ final RunnableOnPathElement action
+ ) {
+ final XmlTag systemTag = getSystemTag(rootTag);
+
+ if (systemTag == null) {
+ return;
+ }
+
+ for (final XmlTag sectionTag : findSubTagsOfParent(systemTag, SECTION_TAG_NAME)) {
+ if (ParsingDepth.SECTION_ID.equals(parsingDepth)) {
+ action.run(sectionTag.getAttributeValue("id"), null, null);
+ continue;
+ }
+ for (final XmlTag groupTag : findSubTagsOfParent(sectionTag, GROUP_TAG_NAME)) {
+ if (ParsingDepth.GROUP_ID.equals(parsingDepth)) {
+ action.run(
+ sectionTag.getAttributeValue("id"),
+ groupTag.getAttributeValue("id"),
+ null
+ );
+ continue;
+ }
+ for (final XmlTag fieldTag : findSubTagsOfParent(groupTag, FIELD_TAG_NAME)) {
+ action.run(
+ sectionTag.getAttributeValue("id"),
+ groupTag.getAttributeValue("id"),
+ fieldTag.getAttributeValue("id")
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Parse system configuration path from the specified xml tag.
+ *
+ * @param tag XmlTag
+ * @param startFrom ParsingDepth
+ *
+ * @return String
+ */
+ public static @Nullable String parseOuterConfigPath(
+ final @NotNull XmlTag tag,
+ final @NotNull ParsingDepth startFrom
+ ) {
+ final String filename = tag.getContainingFile().getName();
+ ParsingDepth currentDepth = startFrom;
+ XmlTag currentTag = tag;
+ final List parts = new ArrayList<>();
+
+ if (ModuleSystemXmlFile.FILE_NAME.equals(filename)) {
+ currentTag = PsiTreeUtil.getParentOfType(
+ currentTag,
+ XmlTag.class
+ );
+ }
+
+ while (currentDepth != null) {
+ if (currentTag == null) {
+ break;
+ }
+ if (ModuleSystemXmlFile.FILE_NAME.equals(filename)) {
+ final String identity = currentTag.getAttributeValue("id");
+
+ if (identity != null) {
+ parts.add(identity);
+ }
+ } else if (ModuleConfigXmlFile.FILE_NAME.equals(filename)) {
+ parts.add(currentTag.getName());
+ }
+ currentDepth = ParsingDepth.getPrevious(currentDepth);
+ currentTag = PsiTreeUtil.getParentOfType(
+ currentTag,
+ XmlTag.class
+ );
+ }
+
+ if (parts.isEmpty()) {
+ return null;
+ }
+ Collections.reverse(parts);
+
+ return String.join(".", parts);
+ }
+
+ public enum ParsingDepth {
+
+ SECTION_ID(0),
+ GROUP_ID(1),
+ FIELD_ID(2);
+
+ private final int depth;
+
+ /**
+ * Parsing depth ENUM constructor.
+ *
+ * @param depth int
+ */
+ ParsingDepth(final int depth) {
+ this.depth = depth;
+ }
+
+ /**
+ * Get ENUM value by its depth number.
+ *
+ * @param depth int
+ *
+ * @return ParsingDepth
+ */
+ public static @Nullable ParsingDepth getByDepth(final int depth) {
+ for (final ParsingDepth parsingDepth : ParsingDepth.values()) {
+ if (parsingDepth.depth == depth) {
+ return parsingDepth;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get previous ENUM value by its depth number.
+ *
+ * @param current ParsingDepth
+ *
+ * @return ParsingDepth
+ */
+ public static @Nullable ParsingDepth getPrevious(final @NotNull ParsingDepth current) {
+ return getByDepth(current.depth - 1);
+ }
+ }
+
+ public interface RunnableOnPathElement {
+ void run(
+ final @Nullable String sectionId,
+ final @Nullable String groupId,
+ final @Nullable String fieldId
+ );
+ }
+
+ private static @Nullable XmlTag getSystemTag(final @NotNull XmlTag rootTag) {
+ if (!ModuleSystemXmlFile.FILE_NAME.equals(rootTag.getContainingFile().getName())) {
+ return null;
+ }
+
+ return rootTag.findFirstSubTag(SYSTEM_TAG_NAME);
+ }
+}
diff --git a/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlFieldMustProvideCompletion/config.xml b/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlFieldMustProvideCompletion/config.xml
new file mode 100644
index 000000000..b0764cc0b
--- /dev/null
+++ b/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlFieldMustProvideCompletion/config.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlGroupMustProvideCompletion/config.xml b/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlGroupMustProvideCompletion/config.xml
new file mode 100644
index 000000000..f16461cbd
--- /dev/null
+++ b/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlGroupMustProvideCompletion/config.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ f
+
+
+
diff --git a/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlSectionMustProvideCompletion/config.xml b/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlSectionMustProvideCompletion/config.xml
new file mode 100644
index 000000000..aca08fa78
--- /dev/null
+++ b/testData/completion/xml/SystemConfigurationPathsCompletion/configXmlSectionMustProvideCompletion/config.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlFieldMustProvideCompletion/system.xml b/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlFieldMustProvideCompletion/system.xml
new file mode 100644
index 000000000..796bed441
--- /dev/null
+++ b/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlFieldMustProvideCompletion/system.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlGroupMustProvideCompletion/system.xml b/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlGroupMustProvideCompletion/system.xml
new file mode 100644
index 000000000..a70e629b8
--- /dev/null
+++ b/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlGroupMustProvideCompletion/system.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlSectionMustProvideCompletion/system.xml b/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlSectionMustProvideCompletion/system.xml
new file mode 100644
index 000000000..9dce5aead
--- /dev/null
+++ b/testData/completion/xml/SystemConfigurationPathsCompletion/systemXmlSectionMustProvideCompletion/system.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/testData/project/magento2/vendor/magento/module-catalog/etc/adminhtml/system.xml b/testData/project/magento2/vendor/magento/module-catalog/etc/adminhtml/system.xml
new file mode 100644
index 000000000..a1b220230
--- /dev/null
+++ b/testData/project/magento2/vendor/magento/module-catalog/etc/adminhtml/system.xml
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
+
+
+
+ separator-top
+
+ catalog
+ Magento_Catalog::config_catalog
+
+
+
+
+ Use {{name}} as Product Name placeholder
+
+
+
+ Use {{name}} as Product Name placeholder
+
+
+
+ Use {{name}} as Product Name or {{sku}} as Product SKU placeholders
+
+
+
+ Use {{name}} and {{description}} as Product Name and Product Description placeholders
+
+
+
+
+
+
+ validate-number validate-zero-or-greater
+
+
+
+ validate-number validate-zero-or-greater
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+
+
+
+ Magento\Catalog\Model\Config\Source\ListMode
+
+
+
+ Comma-separated.
+ validate-per-page-value-list required-entry
+
+
+
+ Must be in the allowed values list.
+ validate-per-page-value
+
+
+
+ Comma-separated.
+ validate-per-page-value-list required-entry
+
+
+
+ Must be in the allowed values list.
+ validate-per-page-value
+
+
+
+ Magento\Catalog\Model\Indexer\Category\Flat\System\Config\Mode
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Magento\Catalog\Model\Indexer\Product\Flat\System\Config\Mode
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Applies to category pages.
+ Magento\Catalog\Model\Config\Source\ListSort
+
+
+
+ Whether to show "All" option in the "Show X Per Page" dropdown.
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+
+
+ 1
+ Magento\Catalog\Model\Config\CatalogClone\Media\Image
+
+ Magento\Config\Model\Config\Backend\Image
+ catalog/product/placeholder
+ catalog/product/placeholder
+
+
+
+
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+
+
+
+ "Currency Options" > "Base Currency").]]>
+ Magento\Catalog\Model\Indexer\Product\Price\System\Config\PriceScope
+ Magento\Catalog\Model\Config\Source\Price\Scope
+ 1
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+
+
+
+ validate-digits validate-zero-or-greater
+
+
+
+
+
+
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Magento\Catalog\Block\Adminhtml\Form\Renderer\Config\DateFieldsOrder
+
+
+
+ Magento\Catalog\Model\Config\Source\TimeFormat
+
+
+
+ validate-digits validate-zero-or-greater validate-number-range number-range-1000-9999
+ Please use a four-digit year format.
+ Magento\Catalog\Block\Adminhtml\Form\Renderer\Config\YearRange
+
+
+
+
+
+
+
+ Media content will be inserted into the editor as a static URL. Media content is not updated if the system configuration base URL changes.
+ Magento\Config\Model\Config\Source\Yesno
+
+ disabled
+
+
+
+
+
+
+
+
+
+
+ Magento\Catalog\Model\Config\Source\LayoutList
+
+
+
+ Magento\Catalog\Model\Config\Source\LayoutList
+
+
+
+
+
+ Magento\Catalog\Model\Config\Source\Web\CatalogMediaUrlFormat
+ Learn more about catalog URL formats.
Warning! If you switch back to legacy mode, you must use the CLI to regenerate images.]]>
+
+
+
+
+ separator-top
+
+ advanced
+ Magento_Config::config_system
+
+
+
+
+ validate-digits validate-digits-range digits-range-1-100 required-entry
+ Jpeg quality for resized images 1-100%.
+
+
+
+
+
diff --git a/tests/com/magento/idea/magento2plugin/completion/xml/SystemConfigurationPathsCompletionTest.java b/tests/com/magento/idea/magento2plugin/completion/xml/SystemConfigurationPathsCompletionTest.java
new file mode 100644
index 000000000..935a4179b
--- /dev/null
+++ b/tests/com/magento/idea/magento2plugin/completion/xml/SystemConfigurationPathsCompletionTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+package com.magento.idea.magento2plugin.completion.xml;
+
+import com.magento.idea.magento2plugin.magento.files.ModuleConfigXmlFile;
+import com.magento.idea.magento2plugin.magento.files.ModuleSystemXmlFile;
+
+public class SystemConfigurationPathsCompletionTest extends CompletionXmlFixtureTestCase {
+
+ /**
+ * Test system xml section element completion.
+ */
+ public void testSystemXmlSectionMustProvideCompletion() {
+ final String filePath = this.getFixturePath(ModuleSystemXmlFile.FILE_NAME);
+ myFixture.copyFileToProject(filePath);
+ assertCompletionContains(filePath, "catalog");
+ }
+
+ /**
+ * Test system xml group element completion.
+ */
+ public void testSystemXmlGroupMustProvideCompletion() {
+ final String filePath = this.getFixturePath(ModuleSystemXmlFile.FILE_NAME);
+ myFixture.copyFileToProject(filePath);
+ assertCompletionContains(filePath, "frontend");
+ }
+
+ /**
+ * Test system xml field element completion.
+ */
+ public void testSystemXmlFieldMustProvideCompletion() {
+ final String filePath = this.getFixturePath(ModuleSystemXmlFile.FILE_NAME);
+ myFixture.copyFileToProject(filePath);
+ assertCompletionContains(filePath, "list_allow_all");
+ }
+
+ /**
+ * Test config xml section element completion.
+ */
+ public void testConfigXmlSectionMustProvideCompletion() {
+ final String filePath = this.getFixturePath(ModuleConfigXmlFile.FILE_NAME);
+ myFixture.copyFileToProject(filePath);
+ assertCompletionContains(filePath, "catalog");
+ }
+
+ /**
+ * Test config xml group element completion.
+ */
+ public void testConfigXmlGroupMustProvideCompletion() {
+ final String filePath = this.getFixturePath(ModuleConfigXmlFile.FILE_NAME);
+ myFixture.copyFileToProject(filePath);
+ assertCompletionContains(filePath, "frontend");
+ }
+
+ /**
+ * Test config xml field element completion.
+ */
+ public void testConfigXmlFieldMustProvideCompletion() {
+ final String filePath = this.getFixturePath(ModuleConfigXmlFile.FILE_NAME);
+ myFixture.copyFileToProject(filePath);
+ assertCompletionContains(filePath, "list_allow_all");
+ }
+}