IDEA-95410 Arrangement: Preserve fold regions on arrangement

This commit is contained in:
Denis.Zhdanov
2012-12-19 16:24:47 +04:00
parent 499c73e2e0
commit cb279e7b3f
7 changed files with 370 additions and 25 deletions
@@ -0,0 +1,74 @@
/*
* 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
/**
* @author Denis Zhdanov
* @since 11/20/12 3:34 PM
*/
class JavaRearrangerFoldingTest extends AbstractJavaRearrangerTest {
void "test dummy"() {
}
// This should be uncommented as soon as cdr pushed fixes for range markers processing.
// void "test that doc comment folding is preserved"() {
// commonSettings.BLANK_LINES_AROUND_METHOD = 1
// doTest(
// initial: '''\
//import <fold>java.util.List;
//import java.util.Set;</fold>
//
//<fold text="/**...*/">/**
// * Class comment
// */</fold>
//class Test {
//
// <fold text="/**...*/>/**
// * Method comment
// */</fold>
// private void test(List<String> l) {}
//
// <fold text="/**...*/>/**
// * Another method comment
// */</fold>
// public void test(Set<String> s) {}
//}''',
//
// rules: [rule(PUBLIC), rule(PRIVATE)],
//
// expected: '''\
//import <fold>java.util.List;
//import java.util.Set;</fold>
//
//<fold text="/**...*/">/**
// * Class comment
// */</fold>
//class Test {
//
// <fold text="/**...*/>/**
// * Another method comment
// */</fold>
// public void test(Set<String> s) {}
//
// <fold text="/**...*/>/**
// * Method comment
// */</fold>
// private void test(List<String> l) {}
//}'''
// )
// }
}
@@ -91,7 +91,7 @@ public class RearrangeCodeAction extends AnAction {
CommandProcessor.getInstance().executeCommand(project, new Runnable() {
@Override
public void run() {
engine.arrange(file, ranges);
engine.arrange(editor, file, ranges);
}
}, getTemplatePresentation().getText(), null);
}
@@ -0,0 +1,34 @@
/*
* 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 org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* @author Denis Zhdanov
* @since 11/21/12 12:54 PM
*/
public interface ArrangementCallback {
/**
* Callback to be notified when arrangement has been performed.
*
* @param moveInfos information about the changes performed during arrangement
*/
void afterArrangement(@NotNull List<ArrangementMoveInfo> moveInfos);
}
@@ -17,6 +17,7 @@ package com.intellij.psi.codeStyle.arrangement.engine;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
@@ -34,6 +35,7 @@ import com.intellij.psi.codeStyle.arrangement.match.ArrangementMatchRule;
import com.intellij.psi.codeStyle.arrangement.order.ArrangementEntryOrderType;
import com.intellij.psi.codeStyle.arrangement.settings.ArrangementStandardSettingsAware;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.containers.HashSet;
import com.intellij.util.containers.Stack;
import com.intellij.util.text.CharArrayUtil;
@@ -55,6 +57,16 @@ import java.util.*;
*/
public class ArrangementEngine {
public void arrange(@NotNull final Editor editor, @NotNull PsiFile file, Collection<TextRange> ranges) {
arrange(file, ranges, null);
// This should be uncommented as soon as cdr pushed fixes for range markers processing.
//arrange(file, ranges, new RestoreFoldArrangementCallback(editor));
}
public void arrange(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges) {
arrange(file, ranges, null);
}
/**
* Arranges given PSI root contents that belong to the given ranges.
*
@@ -62,7 +74,7 @@ public class ArrangementEngine {
* @param ranges target ranges to use within the given root
*/
@SuppressWarnings("MethodMayBeStatic")
public void arrange(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges) {
public void arrange(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges, @Nullable final ArrangementCallback callback) {
final Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) {
return;
@@ -99,14 +111,17 @@ public class ArrangementEngine {
@Override
public void run() {
if (documentEx != null) {
documentEx.setInBulkUpdate(true);
//documentEx.setInBulkUpdate(true);
}
try {
doArrange(context);
if (callback != null) {
callback.afterArrangement(context.moveInfos);
}
}
finally {
if (documentEx != null) {
documentEx.setInBulkUpdate(false);
//documentEx.setInBulkUpdate(false);
}
}
}
@@ -327,6 +342,8 @@ public class ArrangementEngine {
private static class Context<E extends ArrangementEntry> {
@NotNull public final List<ArrangementMoveInfo> moveInfos = ContainerUtilRt.newArrayList();
@NotNull public final Rearranger<E> rearranger;
@NotNull public final Collection<ArrangementEntryWrapper<E>> wrappers;
@NotNull public final Document document;
@@ -348,6 +365,10 @@ public class ArrangementEngine {
this.changer = changer;
}
public void addMoveInfo(int oldStart, int oldEnd, int newStart) {
moveInfos.add(new ArrangementMoveInfo(oldStart, oldEnd, newStart));
}
public static <T extends ArrangementEntry> Context<T> from(@NotNull Rearranger<T> rearranger,
@NotNull Document document,
@NotNull PsiElement root,
@@ -464,6 +485,9 @@ public class ArrangementEngine {
String newEntryText = myParentText.substring(newWrapper.getStartOffset() - myParentShift, newWrapper.getEndOffset() - myParentShift);
int lineFeedsDiff = desiredBlankLinesNumber - blankLinesBefore;
if (lineFeedsDiff == 0 || desiredBlankLinesNumber < 0) {
context.addMoveInfo(newWrapper.getStartOffset() - myParentShift,
newWrapper.getEndOffset() - myParentShift,
oldWrapper.getStartOffset());
context.document.replaceString(oldWrapper.getStartOffset(), oldWrapper.getEndOffset(), newEntryText);
return;
}
@@ -547,6 +571,7 @@ public class ArrangementEngine {
insertionOffset -= newWrapper.getEndOffset() - newWrapper.getStartOffset();
}
if (newWrapper.getStartOffset() != oldWrapper.getStartOffset() || !newWrapper.equals(oldWrapper)) {
context.addMoveInfo(newWrapper.getStartOffset(), newWrapper.getEndOffset(), oldWrapper.getStartOffset());
myDocument.moveText(newWrapper.getStartOffset(), newWrapper.getEndOffset(), oldWrapper.getStartOffset());
for (int i = myWrappers.size() - 1; i >= 0; i--) {
ArrangementEntryWrapper<E> w = myWrappers.get(i);
@@ -0,0 +1,50 @@
/*
* 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;
/**
* @author Denis Zhdanov
* @since 11/21/12 12:51 PM
*/
public class ArrangementMoveInfo {
private final int myOldStart;
private final int myOldEnd;
private final int myNewStart;
public ArrangementMoveInfo(int oldStart, int oldEnd, int newStart) {
myOldStart = oldStart;
myOldEnd = oldEnd;
myNewStart = newStart;
}
public int getOldStart() {
return myOldStart;
}
public int getOldEnd() {
return myOldEnd;
}
public int getNewStart() {
return myNewStart;
}
@Override
public String toString() {
return String.format("range [%d; %d) to offset %d", myOldStart, myOldEnd, myNewStart);
}
}
@@ -0,0 +1,70 @@
/*
* 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.codeInsight.folding.CodeFoldingManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.FoldRegion;
import com.intellij.openapi.fileEditor.impl.text.CodeFoldingState;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author Denis Zhdanov
* @since 11/21/12 12:57 PM
*/
public class RestoreFoldArrangementCallback implements ArrangementCallback {
@NotNull private final Editor myEditor;
@Nullable private final CodeFoldingState myCodeFoldingState;
public RestoreFoldArrangementCallback(@NotNull Editor editor) {
myEditor = editor;
Project project = editor.getProject();
if (project == null) {
myCodeFoldingState = null;
}
else {
final CodeFoldingManager foldingManager = CodeFoldingManager.getInstance(editor.getProject());
myCodeFoldingState = foldingManager.saveFoldingState(editor);
}
}
@Override
public void afterArrangement(@NotNull final List<ArrangementMoveInfo> moveInfos) {
// Restore state for the PSI elements not affected by arrangement.
Project project = myEditor.getProject();
if (myCodeFoldingState != null && project != null) {
final CodeFoldingManager foldingManager = CodeFoldingManager.getInstance(project);
foldingManager.updateFoldRegions(myEditor);
myEditor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
foldingManager.restoreFoldingState(myEditor, myCodeFoldingState);
}
});
}
}
}
@@ -32,12 +32,17 @@ import com.intellij.psi.codeStyle.arrangement.model.ArrangementMatchCondition
import com.intellij.psi.codeStyle.arrangement.order.ArrangementEntryOrderType
import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase
import org.jetbrains.annotations.NotNull
import org.jetbrains.annotations.Nullable
import org.junit.Assert
/**
* @author Denis Zhdanov
* @since 7/20/12 2:54 PM
*/
abstract class AbstractRearrangerTest extends LightPlatformCodeInsightFixtureTestCase {
static final def RICH_TEXT_HANDLERS = [ new RangeHandler(), new FoldingHandler() ]
FileType fileType
Language language;
@@ -92,38 +97,125 @@ abstract class AbstractRearrangerTest extends LightPlatformCodeInsightFixtureTes
}
protected void doTest(@NotNull args) {
def (String textToUse, List<TextRange> rangesToUse) = parseRanges(args.initial)
if (rangesToUse && args.ranges) {
Info info = parse(args.initial)
if (info.ranges && args.ranges) {
junit.framework.Assert.fail(
"Duplicate ranges info detected: explicitly given: $args.ranges, derived from markup: $rangesToUse. Text:\n$args.initial"
"Duplicate ranges info detected: explicitly given: $args.ranges, derived from markup: ${info.ranges}. Text:\n$args.initial"
)
}
if (!rangesToUse) {
rangesToUse = args.ranges ?: [TextRange.from(0, args.initial.length())]
if (!info.ranges) {
info.ranges = args.ranges ?: [TextRange.from(0, args.initial.length())]
}
myFixture.configureByText(fileType, info.text)
def foldingModel = myFixture.editor.foldingModel
info.foldings.each { FoldingInfo foldingInfo ->
foldingModel.runBatchFoldingOperation {
def region = foldingModel.addFoldRegion(foldingInfo.start, foldingInfo.end, foldingInfo.placeholder)
region.expanded = false
}
}
myFixture.configureByText(fileType, textToUse)
def settings = CodeStyleSettingsManager.getInstance(myFixture.project).currentSettings.getCommonSettings(language)
settings.arrangementSettings = new StdArrangementSettings(args.groups ?: [], args.rules ?: [])
ArrangementEngine engine = ServiceManager.getService(myFixture.project, ArrangementEngine)
engine.arrange(myFixture.file, rangesToUse);
junit.framework.Assert.assertEquals(args.expected, myFixture.editor.document.text);
engine.arrange(myFixture.editor, myFixture.file, info.ranges);
// Check expectation.
info = parse(args.expected)
Assert.assertEquals(info.text, myFixture.editor.document.text)
info.foldings.each {
def foldRegion = foldingModel.getCollapsedRegionAtOffset(it.start)
assertNotNull("Expected to find fold region at offset ${it.start}", foldRegion)
assertEquals(it.end, foldRegion.endOffset)
}
}
@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
private static def parse(@NotNull String text) {
def handlers = [:]
RICH_TEXT_HANDLERS.each { handlers["<${it.marker}"] = it }
def result = new Info()
def buffer = new StringBuilder(text)
int offset = 0
int richTextMarkStart = -1
RichTextHandler handler = null
while (offset < buffer.length()) {
handlers.each { String key, RichTextHandler value ->
int i = buffer.indexOf(key, offset)
if (i >= 0 && (handler == null || i < richTextMarkStart)) {
richTextMarkStart = i
handler = value
}
}
if (handler) {
int openingTagEndOffset = buffer.indexOf('>', richTextMarkStart)
int openTagLength = openingTagEndOffset - richTextMarkStart + 1
def attributes = parseAttributes(buffer.substring(1 + richTextMarkStart + handler.marker.length(), openingTagEndOffset))
def closingTag = "</${handler.marker}>"
int closingTagStart = buffer.indexOf(closingTag)
assert closingTagStart > 0
int closingTagLength = 3 + handler.marker.length() // </marker>
handler.handle(result, attributes, richTextMarkStart, closingTagStart - openTagLength)
buffer.delete(closingTagStart, closingTagStart + closingTagLength)
buffer.delete(richTextMarkStart, openingTagEndOffset + 1)
offset = closingTagStart - openTagLength
richTextMarkStart = -1
handler = null
}
else {
break
}
}
result.text = buffer.toString()
result
}
@NotNull
private static Map<String, String> parseAttributes(@NotNull String text) {
def result = [:]
(text =~ /([^\s]+)=([^\s]+)/).each {
result[it[1]] = it[2]
}
result
}
private static class Info {
String text
@Nullable List<TextRange> ranges = []
List<FoldingInfo> foldings = []
}
private static class FoldingInfo {
def placeholder
def start
def end
}
private interface RichTextHandler {
String getMarker()
void handle(@NotNull Info info, @NotNull Map<String, String> attributes, int start, int end)
}
private static class RangeHandler implements RichTextHandler {
@Override String getMarker() { "range" }
@Override
void handle(Info info, Map<String, String> attributes, int start, int end) {
info.ranges << TextRange.create(start, end)
}
}
private static class FoldingHandler implements RichTextHandler {
@Override String getMarker() { "fold" }
@Override
void handle(Info info, Map<String, String> attributes, int start, int end) {
info.foldings << new FoldingInfo(placeholder: attributes.text ?: '...', start: start, end: end)
}
[clearText.toString(), ranges]
}
}