From 7a9eddae63b7029c837a20815a0505100d7d1f7d Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Mon, 23 Jul 2012 18:46:39 +0400 Subject: [PATCH] IDEA-19061 Integrate the Rearranger-plugin into core-IDEA 1. Arrangement core engine is provided; 2. Arrangement tests infrastructure is provided; 3. Added java-specific support for arrangement by type, name and modifier; 4. Corresponding tests are added; --- .../arrangement/JavaArrangementVisitor.java | 195 +++++++++++++++ .../JavaElementArrangementEntry.java | 93 +++++++ .../codeStyle/arrangement/JavaRearranger.java | 44 ++++ .../arrangement/AbstractRearrangerTest.groovy | 84 +++++++ .../JavaRearrangerByTypeTest.groovy | 178 ++++++++++++++ .../codeStyle/CommonCodeStyleSettings.java | 21 +- .../arrangement/ArrangementEntry.java | 12 + ...mentRuleUtil.java => ArrangementUtil.java} | 81 +++++- .../arrangement/DefaultArrangementEntry.java | 76 ++++++ .../psi/codeStyle/arrangement/Rearranger.java | 12 +- .../match/ByNameArrangementEntryMatcher.java | 4 +- .../match/ModifierAwareArrangementEntry.java | 4 +- .../DefaultArrangementRuleSerializerTest.java | 8 +- .../arrangement/engine/ArrangementEngine.java | 232 ++++++++++++++++++ .../src/META-INF/LangExtensionPoints.xml | 1 + .../src/META-INF/LangExtensions.xml | 2 + .../openapi/util/text/StringUtil.java | 11 + resources/src/META-INF/IdeaPlugin.xml | 3 +- 18 files changed, 1038 insertions(+), 23 deletions(-) create mode 100644 java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaArrangementVisitor.java create mode 100644 java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaElementArrangementEntry.java create mode 100644 java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaRearranger.java create mode 100644 java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy create mode 100644 java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerByTypeTest.groovy rename platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/{ArrangementRuleUtil.java => ArrangementUtil.java} (64%) create mode 100644 platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/DefaultArrangementEntry.java create mode 100644 platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementEngine.java diff --git a/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaArrangementVisitor.java b/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaArrangementVisitor.java new file mode 100644 index 000000000000..2ba78bf98153 --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaArrangementVisitor.java @@ -0,0 +1,195 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.codeStyle.arrangement; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType; +import com.intellij.psi.codeStyle.arrangement.match.ArrangementModifier; +import com.intellij.util.containers.Stack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JavaArrangementVisitor extends JavaElementVisitor { + + private static final Map MODIFIERS = new HashMap(); + static { + MODIFIERS.put(PsiModifier.PUBLIC, ArrangementModifier.PUBLIC); + MODIFIERS.put(PsiModifier.PROTECTED, ArrangementModifier.PROTECTED); + MODIFIERS.put(PsiModifier.PRIVATE, ArrangementModifier.PRIVATE); + MODIFIERS.put(PsiModifier.PACKAGE_LOCAL, ArrangementModifier.PACKAGE_PRIVATE); + MODIFIERS.put(PsiModifier.STATIC, ArrangementModifier.STATIC); + MODIFIERS.put(PsiModifier.FINAL, ArrangementModifier.FINAL); + } + + private final Stack myStack = new Stack(); + + @NotNull private final List myRootEntries; + @NotNull private Document myDocument; + @NotNull private Collection myRanges; + + public JavaArrangementVisitor(@NotNull List entries, + @NotNull Document document, + @NotNull Collection ranges) + { + myRootEntries = entries; + myDocument = document; + myRanges = ranges; + } + + @Override + public void visitClass(PsiClass aClass) { + JavaElementArrangementEntry entry = createNewEntry(aClass.getTextRange(), ArrangementEntryType.CLASS, aClass.getName(), true); + processEntry(entry, aClass, aClass); + } + + @Override + public void visitAnonymousClass(PsiAnonymousClass aClass) { + JavaElementArrangementEntry entry = createNewEntry(aClass.getTextRange(), ArrangementEntryType.CLASS, aClass.getName(), false); + processEntry(entry, null, aClass); + } + + @Override + public void visitJavaFile(PsiJavaFile file) { + for (PsiClass psiClass : file.getClasses()) { + visitClass(psiClass); + } + } + + @Override + public void visitField(PsiField field) { + JavaElementArrangementEntry entry = createNewEntry(field.getTextRange(), ArrangementEntryType.FIELD, field.getName(), true); + processEntry(entry, field, field.getInitializer()); + } + + @Override + public void visitMethod(PsiMethod method) { + JavaElementArrangementEntry entry = createNewEntry(method.getTextRange(), ArrangementEntryType.METHOD, method.getName(), true); + processEntry(entry, method, method.getBody()); + } + + @Override + public void visitExpressionStatement(PsiExpressionStatement statement) { + statement.getExpression().acceptChildren(this); + } + + @Override + public void visitNewExpression(PsiNewExpression expression) { + PsiAnonymousClass anonymousClass = expression.getAnonymousClass(); + if (anonymousClass == null) { + return; + } + JavaElementArrangementEntry entry = + createNewEntry(anonymousClass.getTextRange(), ArrangementEntryType.CLASS, anonymousClass.getName(), false); + processEntry(entry, null, anonymousClass); + } + + @Override + public void visitExpressionList(PsiExpressionList list) { + for (PsiExpression expression : list.getExpressions()) { + expression.acceptChildren(this); + } + } + + @Override + public void visitDeclarationStatement(PsiDeclarationStatement statement) { + for (PsiElement element : statement.getDeclaredElements()) { + element.acceptChildren(this); + } + } + + private void processEntry(@Nullable JavaElementArrangementEntry entry, + @Nullable PsiModifierListOwner modifier, + @Nullable PsiElement nextPsiRoot) + { + if (entry == null) { + return; + } + if (modifier != null) { + parseModifiers(modifier.getModifierList(), entry); + } + if (nextPsiRoot == null) { + return; + } + myStack.push(entry); + try { + nextPsiRoot.acceptChildren(this); + } + finally { + myStack.pop(); + } + } + + @Nullable + private JavaElementArrangementEntry createNewEntry(@NotNull TextRange range, + @NotNull ArrangementEntryType type, + @Nullable String name, + boolean canArrange) + { + if (!isWithinBounds(range)) { + return null; + } + DefaultArrangementEntry current = getCurrent(); + JavaElementArrangementEntry entry; + if (canArrange) { + TextRange expandedRange = ArrangementUtil.expandToLine(range, myDocument.getCharsSequence()); + TextRange rangeToUse = expandedRange == null ? range : expandedRange; + entry = new JavaElementArrangementEntry(current, rangeToUse, type, name, expandedRange != null); + } + else { + entry = new JavaElementArrangementEntry(current, range, type, name, false); + } + if (current == null) { + myRootEntries.add(entry); + } + else { + current.addChild(entry); + } + + return entry; + } + + private boolean isWithinBounds(@NotNull TextRange range) { + for (TextRange textRange : myRanges) { + if (textRange.intersects(range)) { + return true; + } + } + return false; + } + + @Nullable + private DefaultArrangementEntry getCurrent() { + return myStack.isEmpty() ? null : myStack.peek(); + } + + private static void parseModifiers(@Nullable PsiModifierList modifierList, @NotNull JavaElementArrangementEntry entry) { + if (modifierList == null) { + return; + } + for (String modifier : PsiModifier.MODIFIERS) { + if (modifierList.hasModifierProperty(modifier)) { + entry.addModifier(MODIFIERS.get(modifier)); + } + } + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaElementArrangementEntry.java b/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaElementArrangementEntry.java new file mode 100644 index 000000000000..75e243feeb92 --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaElementArrangementEntry.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.codeStyle.arrangement; + +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType; +import com.intellij.psi.codeStyle.arrangement.match.ArrangementModifier; +import com.intellij.psi.codeStyle.arrangement.match.ModifierAwareArrangementEntry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumSet; +import java.util.Set; + +/** + * @author Denis Zhdanov + * @since 7/20/12 4:50 PM + */ +public class JavaElementArrangementEntry extends DefaultArrangementEntry + implements TypeAwareArrangementEntry, NameAwareArrangementEntry,ModifierAwareArrangementEntry +{ + + private final Set myModifiers = EnumSet.noneOf(ArrangementModifier.class); + + @NotNull private final ArrangementEntryType myType; + @Nullable private final String myName; + + public JavaElementArrangementEntry(@Nullable ArrangementEntry parent, + @NotNull TextRange range, + @NotNull ArrangementEntryType type, + @Nullable String name, + boolean canBeMatched) + { + this(parent, range.getStartOffset(), range.getEndOffset(), type, name, canBeMatched); + } + + public JavaElementArrangementEntry(@Nullable ArrangementEntry parent, + int startOffset, + int endOffset, + @NotNull ArrangementEntryType type, + @Nullable String name, + boolean canBeArranged) + { + super(parent, startOffset, endOffset, canBeArranged); + myType = type; + myName = name; + } + + @NotNull + @Override + public Set getModifiers() { + return myModifiers; + } + + public void addModifier(@NotNull ArrangementModifier modifier) { + myModifiers.add(modifier); + } + + @Nullable + @Override + public String getName() { + return myName; + } + + @NotNull + @Override + public ArrangementEntryType getType() { + return myType; + } + + @Override + public String toString() { + return String.format( + "[%d; %d): %s %s %s", + getStartOffset(), getEndOffset(), StringUtil.join(myModifiers, " ").toLowerCase(), myType.toString().toLowerCase(), + myName == null ? "" : myName + ); + } +} diff --git a/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaRearranger.java b/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaRearranger.java new file mode 100644 index 000000000000..a4124104df51 --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/codeStyle/arrangement/JavaRearranger.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.codeStyle.arrangement; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Denis Zhdanov + * @since 7/20/12 2:31 PM + */ +public class JavaRearranger implements Rearranger { + + @NotNull + @Override + public Collection parse(@NotNull PsiElement root, + @NotNull Document document, + @NotNull Collection ranges) + { + // Following entries are subject to arrangement: class, interface, field, method. + List result = new ArrayList(); + root.accept(new JavaArrangementVisitor(result, document, ranges)); + return result; + } +} diff --git a/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy b/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy new file mode 100644 index 000000000000..b7147b116d7e --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.codeStyle.arrangement + +import com.intellij.lang.java.JavaLanguage +import com.intellij.openapi.components.ServiceManager +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.util.TextRange +import com.intellij.psi.codeStyle.CodeStyleSettings +import com.intellij.psi.codeStyle.CodeStyleSettingsManager +import com.intellij.psi.codeStyle.arrangement.engine.ArrangementEngine +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase +import org.jetbrains.annotations.NotNull +/** + * @author Denis Zhdanov + * @since 7/20/12 2:54 PM + */ +abstract class AbstractRearrangerTest extends LightCodeInsightFixtureTestCase { + + def FileType fileType + + @Override + protected void setUp() { + super.setUp() + CodeStyleSettingsManager.getInstance(myFixture.project).temporarySettings = new CodeStyleSettings() + } + + @Override + protected void tearDown() { + CodeStyleSettingsManager.getInstance(myFixture.project).dropTemporarySettings() + super.tearDown() + } + + protected void doTest(@NotNull String initial, + @NotNull String expected, + @NotNull List rules, + @NotNull Collection ranges = null) + { + def (String textToUse, List rangesToUse) = parseRanges(initial) + if (rangesToUse && ranges) { + fail("Duplicate ranges info detected: explicitly given: $ranges, derived from markup: $rangesToUse. Text:\n$initial") + } + if (!rangesToUse) { + rangesToUse = ranges ?: [TextRange.from(0, initial.length())] + } + + myFixture.configureByText(fileType, textToUse) + def settings = CodeStyleSettingsManager.getInstance(myFixture.project).currentSettings.getCommonSettings(JavaLanguage.INSTANCE) + settings.arrangementRules = rules + ArrangementEngine engine = ServiceManager.getService(myFixture.project, ArrangementEngine) + engine.arrange(myFixture.file, rangesToUse); + assertEquals(expected, myFixture.editor.document.text); + } + + @NotNull + private static def parseRanges(@NotNull String text) { + def clearText = new StringBuilder(text) + def ranges = [] + int shift = 0 + int shiftIncrease = ''.length() * 2 + 1 + def match = text =~ '(?is).*?' + match.each { + ranges << TextRange.create(match.start() - shift, match.end() - shift - shiftIncrease) + clearText.delete(match.end() - ''.length() - shift, match.end() - shift) + clearText.delete(match.start() - shift, match.start() + ''.length() - shift) + shift += shiftIncrease + } + + [clearText.toString(), ranges] + } +} diff --git a/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerByTypeTest.groovy b/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerByTypeTest.groovy new file mode 100644 index 000000000000..4642e9825c45 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerByTypeTest.groovy @@ -0,0 +1,178 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.codeStyle.arrangement + +import com.intellij.ide.highlighter.JavaFileType +import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryType +import com.intellij.psi.codeStyle.arrangement.match.ByTypeArrangementEntryMatcher +/** + * @author Denis Zhdanov + * @since 7/20/12 2:45 PM + */ +class JavaRearrangerByTypeTest extends AbstractRearrangerTest { + + JavaRearrangerByTypeTest() { + fileType = JavaFileType.INSTANCE + } + + void testFieldsBeforeMethods() { + doTest( + '''\ +class Test { + public void test() {} + private int i; +} +class Test2 { +public void test() { +} + private int i; + private int j; +}''', + '''\ +class Test { + private int i; + public void test() {} +} +class Test2 { + private int i; + private int j; +public void test() { +} +}''', + [new ArrangementRule(new ByTypeArrangementEntryMatcher(ArrangementEntryType.FIELD))] + ) + } + + void testAnonymousClassAtFieldInitializer() { + doTest( + '''\ +class Test { + private Object first = new Object() { + int inner1; + public String toString() { return "test"; } + int inner2; + }; + public Object test(Object ... args) { + return null; + } + private Object second = test(test(new Object() { + public String toString() { + return "test"; + } + private Object inner = new Object() { + public String toString() { return "innerTest"; } + }; + })); +}''', + '''\ +class Test { + private Object first = new Object() { + int inner1; + int inner2; + public String toString() { return "test"; } + }; + private Object second = test(test(new Object() { + private Object inner = new Object() { + public String toString() { return "innerTest"; } + }; + public String toString() { + return "test"; + } + })); + public Object test(Object ... args) { + return null; + } +}''', + [new ArrangementRule(new ByTypeArrangementEntryMatcher(ArrangementEntryType.FIELD))] + ) + } + + void testAnonymousClassAtMethod() { + doTest( + '''\ +class Test { + void declaration() { + Object o = new Object() { + private int test() { return 1; } + String s; + } + } + double d; + void call() { + test(test(1, new Object() { + public void test() {} + int i; + }); + } +}''', + '''\ +class Test { + double d; + void declaration() { + Object o = new Object() { + String s; + private int test() { return 1; } + } + } + void call() { + test(test(1, new Object() { + int i; + public void test() {} + }); + } +}''', + [new ArrangementRule(new ByTypeArrangementEntryMatcher(ArrangementEntryType.FIELD))] + ) + } + + void testRanges() { + doTest( + '''\ +class Test { + void outer1() {} + String outer2() {} + int i; + void test() { + method(new Object() { + void inner1() {} + Object field = new Object() { + void inner2() {} + String s; + Integer i; + } + }); + } +}''', + '''\ +class Test { + void outer1() {} + int i; + String outer2() {} + void test() { + method(new Object() { + void inner1() {} + Object field = new Object() { + String s; + void inner2() {} + Integer i; + } + }); + } +}''', + [new ArrangementRule(new ByTypeArrangementEntryMatcher(ArrangementEntryType.FIELD))] + ) + } +} diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/CommonCodeStyleSettings.java b/platform/lang-api/src/com/intellij/psi/codeStyle/CommonCodeStyleSettings.java index 15bf90ccec1f..08da83c4b290 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/CommonCodeStyleSettings.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/CommonCodeStyleSettings.java @@ -20,7 +20,7 @@ import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.util.*; import com.intellij.psi.codeStyle.arrangement.ArrangementRule; -import com.intellij.psi.codeStyle.arrangement.ArrangementRuleUtil; +import com.intellij.psi.codeStyle.arrangement.ArrangementUtil; import com.intellij.util.xmlb.SkipDefaultValuesSerializationFilters; import com.intellij.util.xmlb.XmlSerializer; import org.intellij.lang.annotations.MagicConstant; @@ -120,6 +120,16 @@ public class CommonCodeStyleSettings { return myIndentOptions; } + @NotNull + public List getArrangementRules() { + return myArrangementRules; + } + + public void setArrangementRules(@NotNull List rules) { + myArrangementRules.clear(); + myArrangementRules.addAll(rules); + } + public CommonCodeStyleSettings clone(CodeStyleSettings rootSettings) { assert rootSettings != null; CommonCodeStyleSettings commonSettings = new CommonCodeStyleSettings(myLanguage, getFileType()); @@ -168,10 +178,7 @@ public class CommonCodeStyleSettings { private static void copyFieldValue(final Object from, Object to, final Field field) throws IllegalAccessException { Class fieldType = field.getType(); - if (fieldType.isPrimitive()) { - field.set(to, field.get(from)); - } - else if (fieldType.equals(String.class)) { + if (fieldType.isPrimitive() || fieldType.equals(String.class)) { field.set(to, field.get(from)); } else { @@ -202,7 +209,7 @@ public class CommonCodeStyleSettings { } Element arrangementRulesContainer = element.getChild(ARRANGEMENT_ELEMENT_NAME); if (arrangementRulesContainer != null) { - myArrangementRules.addAll(ArrangementRuleUtil.readExternal(arrangementRulesContainer, myLanguage)); + myArrangementRules.addAll(ArrangementUtil.readExternal(arrangementRulesContainer, myLanguage)); } } @@ -224,7 +231,7 @@ public class CommonCodeStyleSettings { if (!myArrangementRules.isEmpty()) { Element container = new Element(ARRANGEMENT_ELEMENT_NAME); - ArrangementRuleUtil.writeExternal(container, myArrangementRules, myLanguage); + ArrangementUtil.writeExternal(container, myArrangementRules, myLanguage); if (!container.getChildren().isEmpty()) { element.addContent(container); } diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementEntry.java b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementEntry.java index f29ed7d103a2..d3ad9f9eefee 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementEntry.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementEntry.java @@ -15,6 +15,7 @@ */ package com.intellij.psi.codeStyle.arrangement; +import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryMatcher; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -66,4 +67,15 @@ public interface ArrangementEntry { * to move rearranged entries */ int getEndOffset(); + + /** + * Sometimes we want particular entry to serve just as another entries holder. For example, we might want to arrange + * anonymous class entries but don't want the class itself, say, to be arranged with normal inner classes. + *

+ * That is achieved for entries which return 'false' from this method call. + * + * @return true if current entry can be {@link ArrangementEntryMatcher#isMatched(ArrangementEntry) matched}; + * false otherwise + */ + boolean canBeMatched(); } diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementRuleUtil.java b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementUtil.java similarity index 64% rename from platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementRuleUtil.java rename to platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementUtil.java index 9d9c21da3533..520407ba89ba 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementRuleUtil.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/ArrangementUtil.java @@ -16,6 +16,8 @@ package com.intellij.psi.codeStyle.arrangement; import com.intellij.lang.Language; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.codeStyle.arrangement.match.ArrangementEntryMatcher; import com.intellij.psi.codeStyle.arrangement.match.CompositeArrangementEntryMatcher; import org.jdom.Element; @@ -30,9 +32,9 @@ import java.util.List; * @author Denis Zhdanov * @since 7/17/12 11:24 AM */ -public class ArrangementRuleUtil { +public class ArrangementUtil { - private ArrangementRuleUtil() { + private ArrangementUtil() { } //region Serialization @@ -141,4 +143,79 @@ public class ArrangementRuleUtil { return composite; } //endregion + + //region ArrangementEntry + + /** + * Tries to build a text range on the given arguments basis. It should conform to the criteria below: + *

+   * 
    + *
  • it's start offset is located at the start of the same line where given range starts;
  • + *
  • it's end offset is located at the end of the same line where given range ends;
  • + *
  • all symbols between the resulting range start offset and given range's start offset are white spaces or tabulations;
  • + *
  • all symbols between the given range's end offset and resulting range's end offset are white spaces or tabulations;
  • + *
+ *
+ * This method is expected to be used in a situation when we want to arrange complete rows. + * Example: + *
+   *   class Test {
+   *        void test() {
+   *        }
+   *      int i;
+   *   }
+   * 
+ * Suppose, we want to locate fields before methods. We can move the exact field and method range then but indent will be broken, + * i.e. we'll get the result below: + *
+   *   class Test {
+   *        int i;
+   *      void test() {
+   *        }
+   *   }
+   * 
+ * We can expand field and method range to the whole lines and that would allow to achieve the desired result: + *
+   *   class Test {
+   *      int i;
+   *        void test() {
+   *        }
+   *   }
+   * 
+ * + * @param initialRange anchor range + * @param text target text against which the ranges are built + * @return expanded range if possible; null otherwise + */ + @Nullable + public static TextRange expandToLine(@NotNull TextRange initialRange, @NotNull CharSequence text) { + int startOffsetToUse = initialRange.getStartOffset(); + for (int i = startOffsetToUse - 1; i >= 0; i--) { + char c = text.charAt(i); + if (!StringUtil.isWhiteSpace(c)) { + return null; + } + else if (c == '\n') { + break; + } + else { + startOffsetToUse = i; + } + } + + int endOffsetToUse = initialRange.getEndOffset(); + for (int i = endOffsetToUse; i < text.length(); i++) { + char c = text.charAt(i); + if (!StringUtil.isWhiteSpace(c)) { + return null; + } + else if (c == '\n') { + endOffsetToUse = i; + break; + } + } + + return TextRange.create(startOffsetToUse, endOffsetToUse); + } + //endregion } diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/DefaultArrangementEntry.java b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/DefaultArrangementEntry.java new file mode 100644 index 000000000000..b580ee928f37 --- /dev/null +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/DefaultArrangementEntry.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.codeStyle.arrangement; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Denis Zhdanov + * @since 7/20/12 4:53 PM + */ +public class DefaultArrangementEntry implements ArrangementEntry { + + private final List myChildren = new ArrayList(); + + @Nullable ArrangementEntry myParent; + private final int myStartOffset; + private final int myEndOffset; + private final boolean myCanBeMatched; + + public DefaultArrangementEntry(@Nullable ArrangementEntry parent, int startOffset, int endOffset, boolean canBeMatched) { + myCanBeMatched = canBeMatched; + assert startOffset < endOffset; + myParent = parent; + myStartOffset = startOffset; + myEndOffset = endOffset; + } + + @Nullable + @Override + public ArrangementEntry getParent() { + return myParent; + } + + @NotNull + @Override + public Collection getChildren() { + return myChildren; + } + + public void addChild(@NotNull ArrangementEntry entry) { + myChildren.add(entry); + } + + @Override + public int getStartOffset() { + return myStartOffset; + } + + @Override + public int getEndOffset() { + return myEndOffset; + } + + @Override + public boolean canBeMatched() { + return myCanBeMatched; + } +} diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/Rearranger.java b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/Rearranger.java index b8eb681b3704..617491fcf2bf 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/Rearranger.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/Rearranger.java @@ -16,6 +16,7 @@ package com.intellij.psi.codeStyle.arrangement; import com.intellij.lang.LanguageExtension; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import org.jetbrains.annotations.NotNull; @@ -33,15 +34,16 @@ import java.util.Collection; */ public interface Rearranger { - LanguageExtension> EXTENSION = new LanguageExtension>("com.intellij.rearranger.facade"); + LanguageExtension> EXTENSION = new LanguageExtension>("com.intellij.lang.rearranger"); /** * Allows to build rearranger-interested data for the given element. * - * @param root root element which children should be parsed for the rearrangement - * @param range target offsets range to use for filtering given root's children - * @return given root's children that are subject for further rearrangement + * @param root root element which children should be parsed for the rearrangement + * @param document document which corresponds to the target PSI tree + * @param ranges target offsets ranges to use for filtering given root's children + * @return given root's children which are subject for further rearrangement */ @NotNull - Collection parse(@NotNull PsiElement root, @NotNull TextRange range); + Collection parse(@NotNull PsiElement root, @NotNull Document document, @NotNull Collection ranges); } diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ByNameArrangementEntryMatcher.java b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ByNameArrangementEntryMatcher.java index 251c3c91f8dc..458c8763bb21 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ByNameArrangementEntryMatcher.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ByNameArrangementEntryMatcher.java @@ -35,9 +35,7 @@ public class ByNameArrangementEntryMatcher implements ArrangementEntryMatcher { public boolean isMatched(@NotNull ArrangementEntry entry) { if (entry instanceof NameAwareArrangementEntry) { String name = ((NameAwareArrangementEntry)entry).getName(); - if (name != null) { - return name.matches(myPattern); - } + return name != null && name.matches(myPattern); } return false; } diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ModifierAwareArrangementEntry.java b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ModifierAwareArrangementEntry.java index 10acb1bbd263..af25c6f904ac 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ModifierAwareArrangementEntry.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/arrangement/match/ModifierAwareArrangementEntry.java @@ -18,6 +18,8 @@ package com.intellij.psi.codeStyle.arrangement.match; import com.intellij.psi.codeStyle.arrangement.ArrangementEntry; import org.jetbrains.annotations.NotNull; +import java.util.Set; + /** * @author Denis Zhdanov * @since 7/17/12 8:57 PM @@ -25,5 +27,5 @@ import org.jetbrains.annotations.NotNull; public interface ModifierAwareArrangementEntry extends ArrangementEntry { @NotNull - ArrangementModifier getModifier(); + Set getModifiers(); } diff --git a/platform/lang-api/testSources/com/intellij/psi/codeStyle/arrangement/match/DefaultArrangementRuleSerializerTest.java b/platform/lang-api/testSources/com/intellij/psi/codeStyle/arrangement/match/DefaultArrangementRuleSerializerTest.java index 37948f38fa31..547b35b49b4f 100644 --- a/platform/lang-api/testSources/com/intellij/psi/codeStyle/arrangement/match/DefaultArrangementRuleSerializerTest.java +++ b/platform/lang-api/testSources/com/intellij/psi/codeStyle/arrangement/match/DefaultArrangementRuleSerializerTest.java @@ -17,7 +17,7 @@ package com.intellij.psi.codeStyle.arrangement.match; import com.intellij.psi.codeStyle.arrangement.ArrangementRule; import com.intellij.psi.codeStyle.arrangement.ArrangementRuleSerializer; -import com.intellij.psi.codeStyle.arrangement.ArrangementRuleUtil; +import com.intellij.psi.codeStyle.arrangement.ArrangementUtil; import com.intellij.psi.codeStyle.arrangement.DefaultArrangementRuleSerializer; import org.jdom.Element; import org.jetbrains.annotations.NotNull; @@ -48,13 +48,13 @@ public class DefaultArrangementRuleSerializerTest { @Test public void compositeMatchers() { - doTest(ArrangementRuleUtil.or( + doTest(ArrangementUtil.or( new ByTypeArrangementEntryMatcher(ArrangementEntryType.FIELD), new ByTypeArrangementEntryMatcher(ArrangementEntryType.METHOD)) ); - doTest(ArrangementRuleUtil.and( - ArrangementRuleUtil.or( + doTest(ArrangementUtil.and( + ArrangementUtil.or( new ByTypeArrangementEntryMatcher(ArrangementEntryType.METHOD), new ByNameArrangementEntryMatcher("get*") ) diff --git a/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementEngine.java b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementEngine.java new file mode 100644 index 000000000000..9072ca1c8b8a --- /dev/null +++ b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementEngine.java @@ -0,0 +1,232 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.psi.codeStyle.arrangement.engine; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.ex.DocumentEx; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.psi.codeStyle.arrangement.ArrangementEntry; +import com.intellij.psi.codeStyle.arrangement.ArrangementRule; +import com.intellij.psi.codeStyle.arrangement.Rearranger; +import com.intellij.util.containers.Stack; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * @author Denis Zhdanov + * @since 7/20/12 1:56 PM + */ +public class ArrangementEngine { + + /** + * Arranges given PSI root contents that belong to the given ranges. + * + * @param file target PSI root + * @param ranges target ranges to use within the given root + */ + @SuppressWarnings("MethodMayBeStatic") + public void arrange(@NotNull PsiFile file, @NotNull Collection ranges) { + final Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file); + if (document == null) { + return; + } + + CodeStyleSettings settings = CodeStyleSettingsManager.getInstance(file.getProject()).getCurrentSettings(); + final List arrangementRules = settings.getCommonSettings(file.getLanguage()).getArrangementRules(); + if (arrangementRules.isEmpty()) { + return; + } + + Rearranger rearranger = Rearranger.EXTENSION.forLanguage(file.getLanguage()); + if (rearranger == null) { + return; + } + + final Collection entriesToProcess = rearranger.parse(file, document, ranges); + final DocumentEx documentEx; + if (document instanceof DocumentEx && !((DocumentEx)document).isInBulkUpdate()) { + documentEx = (DocumentEx)document; + } + else { + documentEx = null; + } + + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + if (documentEx != null) { + documentEx.setInBulkUpdate(true); + } + try { + doArrange(document, arrangementRules, entriesToProcess); + } + finally { + if (documentEx != null) { + documentEx.setInBulkUpdate(false); + } + } + } + }); + } + + private static void doArrange(@NotNull final Document document, + @NotNull List arrangementRules, + @NotNull Collection entriesToProcess) + { + // The general idea is to process entries bottom-up where every processed group belongs to the same parent. We may not bother + // with entries text ranges then. We use a list and a stack for achieving that than. + // + // Example: + // Entry1 Entry2 + // / \ / \ + // Entry11 Entry12 Entry21 Entry22 + // + // -------------------------- + // Stage 1: + // list: Entry1 Entry2 + // stack: [0, 0, 2] + // -------------------------- + // Stage 2: + // list: Entry1 Entry2 Entry11 Entry12 + // stack: [0, 1, 2] + // [2, 2, 4] + // -------------------------- + // Stage 3: + // list: Entry1 Entry2 Entry11 Entry12 + // stack: [0, 1, 2] + // [2, 3, 4] + // -------------------------- + // Stage 4: + // list: Entry1 Entry2 Entry11 Entry12 + // stack: [0, 1, 2] + // [2, 4, 4] + // -------------------------- + // arrange 'Entry11 Entry12' + // -------------------------- + // Stage 5: + // list: Entry1 Entry2 + // stack: [0, 1, 2] + // -------------------------- + // Stage 6: + // list: Entry1 Entry2 Entry21 Entry22 + // stack: [0, 2, 2] + // [2, 2, 4] + // -------------------------- + // Stage 7: + // list: Entry1 Entry2 Entry21 Entry22 + // stack: [0, 2, 2] + // [2, 3, 4] + // -------------------------- + // Stage 8: + // list: Entry1 Entry2 Entry21 Entry22 + // stack: [0, 2, 2] + // [2, 4, 4] + // -------------------------- + // arrange 'Entry21 Entry22' + // -------------------------- + // Stage 9: + // list: Entry1 Entry2 + // stack: [0, 2, 2] + // -------------------------- + // arrange 'Entry1 Entry2' + + List entries = new ArrayList(); + Stack stack = new Stack(); + entries.addAll(entriesToProcess); + stack.push(new StackEntry(0, entriesToProcess.size())); + while (!stack.isEmpty()) { + StackEntry stackEntry = stack.peek(); + if (stackEntry.current >= stackEntry.end) { + List subEntries = entries.subList(stackEntry.start, stackEntry.end); + if (subEntries.size() > 1) { + doArrange(arrangementRules, subEntries, document); + } + subEntries.clear(); + stack.pop(); + } + else { + ArrangementEntry entry = entries.get(stackEntry.current++); + Collection children = entry.getChildren(); + if (!children.isEmpty()) { + entries.addAll(children); + stack.push(new StackEntry(stackEntry.end, children.size())); + } + } + } + } + + private static void doArrange(@NotNull List rules, + @NotNull List entries, + @NotNull Document document) + { + List arranged = new ArrayList(); + Set unprocessed = new LinkedHashSet(entries); + + for (ArrangementRule rule : rules) { + for (ArrangementEntry entry : entries) { + if (entry.canBeMatched() && unprocessed.contains(entry) && rule.getMatcher().isMatched(entry)) { + arranged.add(entry); + unprocessed.remove(entry); + } + } + } + arranged.addAll(unprocessed); + + if (arranged.equals(entries)) { + return; + } + + // We apply changes from the last position to the first position in order not to bother with offsets shifts. + ArrangementEntry parent = entries.get(0).getParent(); + final String initial; + final int shift; + if (parent == null) { + initial = document.getCharsSequence().toString(); + shift = 0; + } + else { + initial = document.getCharsSequence().subSequence(parent.getStartOffset(), parent.getEndOffset()).toString(); + shift = parent.getStartOffset(); + } + for (int i = arranged.size() - 1; i >= 0; i--) { + ArrangementEntry arrangedEntry = arranged.get(i); + ArrangementEntry initialEntry = entries.get(i); + if (!arrangedEntry.equals(initialEntry)) { + String text = initial.substring(arrangedEntry.getStartOffset() - shift, arrangedEntry.getEndOffset() - shift); + document.replaceString(initialEntry.getStartOffset(), initialEntry.getEndOffset(), text); + } + } + } + + private static class StackEntry { + + public int start; + public int current; + public int end; + + StackEntry(int start, int count) { + this.start = start; + current = start; + end = start + count; + } + } +} diff --git a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml index 187bb8a1311b..5f657f13666a 100644 --- a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml @@ -123,6 +123,7 @@ beanClass="com.intellij.lang.LanguageExtensionPoint"/> + diff --git a/platform/platform-resources/src/META-INF/LangExtensions.xml b/platform/platform-resources/src/META-INF/LangExtensions.xml index 1f635d2026e8..302bc00f4117 100644 --- a/platform/platform-resources/src/META-INF/LangExtensions.xml +++ b/platform/platform-resources/src/META-INF/LangExtensions.xml @@ -177,6 +177,8 @@ + + diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index 62e9ef562fea..ec9efc4e0729 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -1163,6 +1163,17 @@ public class StringUtil extends StringUtilRt { return join((Iterable)items, f, separator); } + public static String join(@NotNull Iterable items, @NotNull @NonNls String separator) { + StringBuilder result = new StringBuilder(); + for (Object item : items) { + result.append(item).append(separator); + } + if (result.length() > 0) { + result.setLength(result.length() - 1); + } + return result.toString(); + } + @NotNull public static String join(@NotNull Iterable items, @NotNull Function f, @NotNull @NonNls String separator) { final StringBuilder result = new StringBuilder(); diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index 5cc31d889e58..df858b547c50 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -764,7 +764,8 @@ - + +