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;
This commit is contained in:
Denis.Zhdanov
2012-07-24 10:46:44 +04:00
parent 6f3abb799d
commit 7a9eddae63
18 changed files with 1038 additions and 23 deletions
@@ -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<String, ArrangementModifier> MODIFIERS = new HashMap<String, ArrangementModifier>();
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<JavaElementArrangementEntry> myStack = new Stack<JavaElementArrangementEntry>();
@NotNull private final List<JavaElementArrangementEntry> myRootEntries;
@NotNull private Document myDocument;
@NotNull private Collection<TextRange> myRanges;
public JavaArrangementVisitor(@NotNull List<JavaElementArrangementEntry> entries,
@NotNull Document document,
@NotNull Collection<TextRange> 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));
}
}
}
}
@@ -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<ArrangementModifier> 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<ArrangementModifier> 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 ? "<no name>" : myName
);
}
}
@@ -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<JavaElementArrangementEntry> {
@NotNull
@Override
public Collection<JavaElementArrangementEntry> parse(@NotNull PsiElement root,
@NotNull Document document,
@NotNull Collection<TextRange> ranges)
{
// Following entries are subject to arrangement: class, interface, field, method.
List<JavaElementArrangementEntry> result = new ArrayList<JavaElementArrangementEntry>();
root.accept(new JavaArrangementVisitor(result, document, ranges));
return result;
}
}
@@ -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<ArrangementRule> rules,
@NotNull Collection<TextRange> ranges = null)
{
def (String textToUse, List<TextRange> 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 = '<range>'.length() * 2 + 1
def match = text =~ '(?is)<range>.*?</range>'
match.each {
ranges << TextRange.create(match.start() - shift, match.end() - shift - shiftIncrease)
clearText.delete(match.end() - '</range>'.length() - shift, match.end() - shift)
clearText.delete(match.start() - shift, match.start() + '<range>'.length() - shift)
shift += shiftIncrease
}
[clearText.toString(), ranges]
}
}
@@ -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() {}
<range> String outer2() {}
int i;</range>
void test() {
method(new Object() {
void inner1() {}
Object field = new Object() {
<range> void inner2() {}
String s;</range>
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))]
)
}
}
@@ -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<ArrangementRule> getArrangementRules() {
return myArrangementRules;
}
public void setArrangementRules(@NotNull List<ArrangementRule> 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);
}
@@ -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.
* <p/>
* That is achieved for entries which return <code>'false'</code> from this method call.
*
* @return <code>true</code> if current entry can be {@link ArrangementEntryMatcher#isMatched(ArrangementEntry) matched};
* <code>false</code> otherwise
*/
boolean canBeMatched();
}
@@ -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:
* <pre>
* <ul>
* <li>it's start offset is located at the start of the same line where given range starts;</li>
* <li>it's end offset is located at the end of the same line where given range ends;</li>
* <li>all symbols between the resulting range start offset and given range's start offset are white spaces or tabulations;</li>
* <li>all symbols between the given range's end offset and resulting range's end offset are white spaces or tabulations;</li>
* </ul>
* </pre>
* This method is expected to be used in a situation when we want to arrange complete rows.
* Example:
* <pre>
* class Test {
* void test() {
* }
* int i;
* }
* </pre>
* 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:
* <pre>
* class Test {
* int i;
* void test() {
* }
* }
* </pre>
* We can expand field and method range to the whole lines and that would allow to achieve the desired result:
* <pre>
* class Test {
* int i;
* void test() {
* }
* }
* </pre>
*
* @param initialRange anchor range
* @param text target text against which the ranges are built
* @return expanded range if possible; <code>null</code> 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
}
@@ -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<ArrangementEntry> myChildren = new ArrayList<ArrangementEntry>();
@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<? extends ArrangementEntry> 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;
}
}
@@ -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<E extends ArrangementEntry> {
LanguageExtension<Rearranger<?>> EXTENSION = new LanguageExtension<Rearranger<?>>("com.intellij.rearranger.facade");
LanguageExtension<Rearranger<?>> EXTENSION = new LanguageExtension<Rearranger<?>>("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<E> parse(@NotNull PsiElement root, @NotNull TextRange range);
Collection<E> parse(@NotNull PsiElement root, @NotNull Document document, @NotNull Collection<TextRange> ranges);
}
@@ -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;
}
@@ -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<ArrangementModifier> getModifiers();
}
@@ -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*")
)
@@ -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<TextRange> ranges) {
final Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) {
return;
}
CodeStyleSettings settings = CodeStyleSettingsManager.getInstance(file.getProject()).getCurrentSettings();
final List<ArrangementRule> arrangementRules = settings.getCommonSettings(file.getLanguage()).getArrangementRules();
if (arrangementRules.isEmpty()) {
return;
}
Rearranger<?> rearranger = Rearranger.EXTENSION.forLanguage(file.getLanguage());
if (rearranger == null) {
return;
}
final Collection<? extends ArrangementEntry> 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<ArrangementRule> arrangementRules,
@NotNull Collection<? extends ArrangementEntry> 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<ArrangementEntry> entries = new ArrayList<ArrangementEntry>();
Stack<StackEntry> stack = new Stack<StackEntry>();
entries.addAll(entriesToProcess);
stack.push(new StackEntry(0, entriesToProcess.size()));
while (!stack.isEmpty()) {
StackEntry stackEntry = stack.peek();
if (stackEntry.current >= stackEntry.end) {
List<ArrangementEntry> 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<? extends ArrangementEntry> children = entry.getChildren();
if (!children.isEmpty()) {
entries.addAll(children);
stack.push(new StackEntry(stackEntry.end, children.size()));
}
}
}
}
private static void doArrange(@NotNull List<ArrangementRule> rules,
@NotNull List<ArrangementEntry> entries,
@NotNull Document document)
{
List<ArrangementEntry> arranged = new ArrayList<ArrangementEntry>();
Set<ArrangementEntry> unprocessed = new LinkedHashSet<ArrangementEntry>(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;
}
}
}
@@ -123,6 +123,7 @@
beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.formatter" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.importOptimizer" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.rearranger" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.surroundDescriptor" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.unwrapDescriptor" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.parserDefinition" beanClass="com.intellij.lang.LanguageExtensionPoint"/>
@@ -177,6 +177,8 @@
<applicationService serviceInterface="com.intellij.openapi.roots.impl.libraries.JarDirectoryWatcherFactory"
serviceImplementation="com.intellij.openapi.roots.impl.libraries.JarDirectoryWatcherFactoryImpl"/>
<applicationService serviceImplementation="com.intellij.psi.codeStyle.arrangement.engine.ArrangementEngine"/>
<projectService serviceInterface="com.intellij.ui.EditorTextFieldProvider"
serviceImplementation="com.intellij.ui.EditorTextFieldProviderImpl"/>
@@ -1163,6 +1163,17 @@ public class StringUtil extends StringUtilRt {
return join((Iterable<T>)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 <T> String join(@NotNull Iterable<T> items, @NotNull Function<T, String> f, @NotNull @NonNls String separator) {
final StringBuilder result = new StringBuilder();
+2 -1
View File
@@ -764,7 +764,8 @@
<lang.formatter language="JAVA" implementationClass="com.intellij.lang.java.JavaFormattingModelBuilder"/>
<lang.whiteSpaceFormattingStrategy language="JAVA"
implementationClass="com.intellij.psi.formatter.JavadocWhiteSpaceFormattingStrategy"/>
<lang.rearranger language="JAVA" implementationClass="com.intellij.psi.codeStyle.arrangement.JavaRearranger"/>
<lang.documentationProvider language="JAVA" implementationClass="com.intellij.lang.java.JavaDocumentationProvider"/>
<documentationProvider implementation="com.intellij.lang.java.FileDocumentationProvider" order="last"/>