xml rearranger support: ordering of attributes

This commit is contained in:
Eugene Kudelevsky
2013-02-14 14:42:54 +04:00
parent 288f5f396c
commit 57470b7151
9 changed files with 450 additions and 1 deletions
@@ -31,5 +31,7 @@ public enum ArrangementEntryType {
CONST, VAR, PROPERTY, EVENT_HANDLER, STATIC_INIT,
NAMESPACE, TRAIT
NAMESPACE, TRAIT,
XML_TAG, XML_ATTRIBUTE
}
@@ -449,6 +449,7 @@
<breadcrumbsPresentationProvider implementation="com.intellij.codeInsight.daemon.impl.tagTreeHighlighting.XmlTagTreeBreadcrumbsPresentationProvider"/>
<daemon.changeLocalityDetector implementation="com.intellij.xml.XmlChangeLocalityDetector"/>
<xmlStructureViewBuilderProvider implementation="com.intellij.lang.html.structureView.HtmlStructureViewBuilderProvider"/>
<lang.rearranger language="XML" implementationClass="com.intellij.xml.arrangement.XmlRearranger"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains">
<urlOpener implementation="com.intellij.ide.browsers.impl.DefaultUrlOpener" order="last"/>
@@ -19,6 +19,7 @@ import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsProvider;
import com.intellij.xml.arrangement.XmlArrangementPanel;
/**
* @author Rustam Vishnyakov
@@ -32,6 +33,7 @@ public class XmlCodeStyleMainPanel extends TabbedLanguageCodeStylePanel {
protected void initTabs(CodeStyleSettings settings) {
addIndentOptionsTab(settings);
addTab(new CodeStyleXmlPanel(settings));
addTab(new XmlArrangementPanel(settings));
for (CodeStyleSettingsProvider provider : Extensions.getExtensions(CodeStyleSettingsProvider.EXTENSION_POINT_NAME)) {
if (provider.getLanguage() == XMLLanguage.INSTANCE && !provider.hasSettingsPage()) {
@@ -0,0 +1,33 @@
package com.intellij.xml.arrangement;
import com.intellij.application.options.codeStyle.arrangement.ArrangementSettingsPanel;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import org.jetbrains.annotations.NotNull;
/**
* @author Eugene.Kudelevsky
*/
public class XmlArrangementPanel extends ArrangementSettingsPanel {
public XmlArrangementPanel(@NotNull CodeStyleSettings settings) {
super(settings, XMLLanguage.INSTANCE);
}
@Override
protected int getRightMargin() {
return 80;
}
@NotNull
@Override
protected FileType getFileType() {
return XmlFileType.INSTANCE;
}
@Override
protected String getPreviewText() {
return null;
}
}
@@ -0,0 +1,23 @@
package com.intellij.xml.arrangement;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* @author Eugene.Kudelevsky
*/
public class XmlArrangementParseInfo {
private final List<XmlElementArrangementEntry> myEntries = new ArrayList<XmlElementArrangementEntry>();
@NotNull
public List<XmlElementArrangementEntry> getEntries() {
return myEntries;
}
public void addEntry(@NotNull XmlElementArrangementEntry entry) {
myEntries.add(entry);
}
}
@@ -0,0 +1,103 @@
package com.intellij.xml.arrangement;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.XmlElementVisitor;
import com.intellij.psi.codeStyle.arrangement.DefaultArrangementEntry;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
/**
* @author Eugene.Kudelevsky
*/
public class XmlArrangementVisitor extends XmlElementVisitor {
private final Stack<XmlElementArrangementEntry> myStack = new Stack<XmlElementArrangementEntry>();
private final XmlArrangementParseInfo myInfo;
private final Collection<TextRange> myRanges;
public XmlArrangementVisitor(@NotNull XmlArrangementParseInfo info, @NotNull Collection<TextRange> ranges) {
myInfo = info;
myRanges = ranges;
}
@Override
public void visitXmlFile(XmlFile file) {
final XmlTag tag = file.getRootTag();
if (tag != null) {
tag.accept(this);
}
}
@Override
public void visitXmlTag(XmlTag tag) {
final XmlElementArrangementEntry entry = createNewEntry(
tag.getTextRange(), ArrangementEntryType.XML_TAG, null, true);
processEntry(entry, tag);
}
@Override
public void visitXmlAttribute(XmlAttribute attribute) {
final String name = attribute.isNamespaceDeclaration() ? "" : attribute.getName();
final XmlElementArrangementEntry entry = createNewEntry(
attribute.getTextRange(), ArrangementEntryType.XML_ATTRIBUTE, name, true);
processEntry(entry, null);
}
private void processEntry(@Nullable XmlElementArrangementEntry entry, @Nullable PsiElement nextElement) {
if (entry == null || nextElement == null) {
return;
}
myStack.push(entry);
try {
nextElement.acceptChildren(this);
}
finally {
myStack.pop();
}
}
@Nullable
private XmlElementArrangementEntry createNewEntry(@NotNull TextRange range,
@NotNull ArrangementEntryType type,
@Nullable String name,
boolean canBeMatched) {
if (!isWithinBounds(range)) {
return null;
}
final DefaultArrangementEntry current = getCurrent();
final XmlElementArrangementEntry entry = new XmlElementArrangementEntry(
current, range, type, name, canBeMatched);
if (current == null) {
myInfo.addEntry(entry);
}
else {
current.addChild(entry);
}
return entry;
}
@Nullable
private DefaultArrangementEntry getCurrent() {
return myStack.isEmpty() ? null : myStack.peek();
}
private boolean isWithinBounds(@NotNull TextRange range) {
for (TextRange textRange : myRanges) {
if (textRange.intersects(range)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,50 @@
package com.intellij.xml.arrangement;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.codeStyle.arrangement.ArrangementEntry;
import com.intellij.psi.codeStyle.arrangement.DefaultArrangementEntry;
import com.intellij.psi.codeStyle.arrangement.NameAwareArrangementEntry;
import com.intellij.psi.codeStyle.arrangement.TypeAwareArrangementEntry;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Set;
/**
* @author Eugene.Kudelevsky
*/
public class XmlElementArrangementEntry extends DefaultArrangementEntry
implements TypeAwareArrangementEntry, NameAwareArrangementEntry {
private final ArrangementEntryType myType;
private final String myName;
public XmlElementArrangementEntry(@Nullable ArrangementEntry parent,
@NotNull TextRange range,
@NotNull ArrangementEntryType type,
@Nullable String name,
boolean canBeMatched) {
super(parent, range.getStartOffset(), range.getEndOffset(), canBeMatched);
myName = name;
myType = type;
}
@Nullable
@Override
public String getName() {
return myName;
}
@NotNull
@Override
public Set<ArrangementEntryType> getTypes() {
return Collections.singleton(myType);
}
@NotNull
public ArrangementEntryType getType() {
return myType;
}
}
@@ -0,0 +1,182 @@
package com.intellij.xml.arrangement;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.arrangement.ArrangementSettings;
import com.intellij.psi.codeStyle.arrangement.Rearranger;
import com.intellij.psi.codeStyle.arrangement.StdArrangementSettings;
import com.intellij.psi.codeStyle.arrangement.group.ArrangementGroupingRule;
import com.intellij.psi.codeStyle.arrangement.group.ArrangementGroupingType;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType;
import com.intellij.psi.codeStyle.arrangement.match.ArrangementModifier;
import com.intellij.psi.codeStyle.arrangement.match.StdArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition;
import com.intellij.psi.codeStyle.arrangement.order.ArrangementEntryOrderType;
import com.intellij.psi.codeStyle.arrangement.settings.ArrangementStandardSettingsAware;
import com.intellij.psi.codeStyle.arrangement.settings.ArrangementStandardSettingsRepresentationAware;
import com.intellij.psi.codeStyle.arrangement.settings.DefaultArrangementSettingsRepresentationManager;
import gnu.trove.TObjectIntHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Eugene.Kudelevsky
*/
public class XmlRearranger
implements Rearranger<XmlElementArrangementEntry>,
ArrangementStandardSettingsAware,
ArrangementStandardSettingsRepresentationAware {
private static final Set<ArrangementEntryType> SUPPORTED_TYPES = EnumSet.of(
ArrangementEntryType.XML_ATTRIBUTE, ArrangementEntryType.XML_TAG);
private static final List<StdArrangementMatchRule> DEFAULT_MATCH_RULES = new ArrayList<StdArrangementMatchRule>();
private static final StdArrangementSettings DEFAULT_SETTINGS = new StdArrangementSettings(
Collections.<ArrangementGroupingRule>emptyList(), DEFAULT_MATCH_RULES);
@NotNull private static final TObjectIntHashMap<Object> WEIGHTS = new TObjectIntHashMap<Object>();
@NotNull
private static final Comparator<Object> COMPARATOR = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
if (WEIGHTS.containsKey(o1) && WEIGHTS.containsKey(o2)) {
return WEIGHTS.get(o1) - WEIGHTS.get(o2);
}
else if (WEIGHTS.containsKey(o1) && !WEIGHTS.containsKey(o2)) {
return -1;
}
else if (!WEIGHTS.containsKey(o1) && WEIGHTS.containsKey(o2)) {
return 1;
}
else {
return o1.hashCode() - o2.hashCode();
}
}
};
static {
final Object[] ids = {
ArrangementEntryType.XML_TAG, ArrangementEntryType.XML_ATTRIBUTE
};
for (int i = 0; i < ids.length; i++) {
WEIGHTS.put(ids[i], i);
}
}
@Nullable
@Override
public StdArrangementSettings getDefaultSettings() {
return DEFAULT_SETTINGS;
}
@Override
public boolean isNameFilterSupported() {
return true;
}
@Override
public boolean isEnabled(@NotNull ArrangementEntryType type, @Nullable ArrangementMatchCondition current) {
return SUPPORTED_TYPES.contains(type);
}
@Override
public boolean isEnabled(@NotNull ArrangementModifier modifier, @Nullable ArrangementMatchCondition current) {
return false;
}
@Override
public boolean isEnabled(@NotNull ArrangementGroupingType groupingType, @Nullable ArrangementEntryOrderType orderType) {
return false;
}
@NotNull
@Override
public Collection<Set<?>> getMutexes() {
return Collections.<Set<?>>singleton(SUPPORTED_TYPES);
}
@Nullable
@Override
public Pair<XmlElementArrangementEntry, List<XmlElementArrangementEntry>> parseWithNew(@NotNull PsiElement root,
@Nullable Document document,
@NotNull Collection<TextRange> ranges,
@NotNull PsiElement element,
@Nullable ArrangementSettings settings) {
final XmlArrangementParseInfo newEntryInfo = new XmlArrangementParseInfo();
element.accept(new XmlArrangementVisitor(newEntryInfo, Collections.singleton(element.getTextRange())));
if (newEntryInfo.getEntries().size() != 1) {
return null;
}
final XmlElementArrangementEntry entry = newEntryInfo.getEntries().get(0);
final XmlArrangementParseInfo existingEntriesInfo = new XmlArrangementParseInfo();
root.accept(new XmlArrangementVisitor(existingEntriesInfo, ranges));
return Pair.create(entry, existingEntriesInfo.getEntries());
}
@NotNull
@Override
public List<XmlElementArrangementEntry> parse(@NotNull PsiElement root,
@Nullable Document document,
@NotNull Collection<TextRange> ranges,
@Nullable ArrangementSettings settings) {
final XmlArrangementParseInfo parseInfo = new XmlArrangementParseInfo();
root.accept(new XmlArrangementVisitor(parseInfo, ranges));
return parseInfo.getEntries();
}
@Override
public int getBlankLines(@NotNull CodeStyleSettings settings,
@Nullable XmlElementArrangementEntry parent,
@Nullable XmlElementArrangementEntry previous,
@NotNull XmlElementArrangementEntry target) {
return -1;
}
@NotNull
@Override
public String getDisplayValue(@NotNull ArrangementEntryType type) {
switch (type) {
case XML_TAG:
return "tag";
case XML_ATTRIBUTE:
return "attribute";
default:
return DefaultArrangementSettingsRepresentationManager.
INSTANCE.getDisplayValue(type);
}
}
@NotNull
@Override
public String getDisplayValue(@NotNull ArrangementModifier modifier) {
return DefaultArrangementSettingsRepresentationManager.INSTANCE.getDisplayValue(modifier);
}
@NotNull
@Override
public String getDisplayValue(@NotNull ArrangementGroupingType groupingType) {
return DefaultArrangementSettingsRepresentationManager.INSTANCE.getDisplayValue(groupingType);
}
@NotNull
@Override
public String getDisplayValue(@NotNull ArrangementEntryOrderType orderType) {
return DefaultArrangementSettingsRepresentationManager.INSTANCE.getDisplayValue(orderType);
}
@NotNull
@Override
public <T> List<T> sort(@NotNull Collection<T> ids) {
final List<T> result = new ArrayList<T>(ids);
Collections.sort(result, COMPARATOR);
return result;
}
}
@@ -0,0 +1,53 @@
package com.intellij.xml.arrangement
import com.intellij.ide.highlighter.XmlFileType
import com.intellij.lang.xml.XMLLanguage
import com.intellij.psi.codeStyle.arrangement.AbstractRearrangerTest
import static com.intellij.psi.codeStyle.arrangement.order.ArrangementEntryOrderType.BY_NAME
/**
* @author Eugene.Kudelevsky
*/
class XmlRearrangerTest extends AbstractRearrangerTest {
XmlRearrangerTest() {
fileType = XmlFileType.INSTANCE
language = XMLLanguage.INSTANCE
}
void testAttributeSorting1() {
doTest(
initial: '''<root xmlns:ns="http://ns.com" attr2="value2" attr1="value1" attr3="value3"/>''',
expected: '''<root xmlns:ns="http://ns.com" attr1="value1" attr2="value2" attr3="value3"/>''',
rules: [rule(BY_NAME, ".*")]
)
}
void testAttributeSorting2() {
doTest(
initial: '''<root attr3="value3" attr2="value2" attr1="value1" xmlns:ns="http://ns.com"/>''',
expected: '''<root xmlns:ns="http://ns.com" attr1="value1" attr2="value2" attr3="value3"/>''',
rules: [rule(BY_NAME, ".*")]
)
}
void testAttributeSorting3() {
doTest(
initial: '''\
<root xmlns:ns1="http://ns1.com" xmlns:ns2="http://ns2.com" attr2="value2" attr1="value1">
<tag1 attr2="value2" attr1="value1">
<tag2 attr1="value1" attr2="value2"/>
<tag3 attr2="value2" attr1="value1" xmlns:ns3="http://ns.com"/>
</tag1>
</root>
''',
expected: '''\
<root xmlns:ns1="http://ns1.com" xmlns:ns2="http://ns2.com" attr1="value1" attr2="value2">
<tag1 attr1="value1" attr2="value2">
<tag2 attr1="value1" attr2="value2"/>
<tag3 xmlns:ns3="http://ns.com" attr1="value1" attr2="value2"/>
</tag1>
</root>
''',
rules: [rule(BY_NAME, ".*")]
)
}
}