From cb279e7b3f2754a6b9b7942ce657f23b65d27ced Mon Sep 17 00:00:00 2001 From: "Denis.Zhdanov" Date: Wed, 19 Dec 2012 16:24:28 +0400 Subject: [PATCH] IDEA-95410 Arrangement: Preserve fold regions on arrangement --- .../JavaRearrangerFoldingTest.groovy | 74 ++++++++++ .../action/RearrangeCodeAction.java | 2 +- .../engine/ArrangementCallback.java | 34 +++++ .../arrangement/engine/ArrangementEngine.java | 31 +++- .../engine/ArrangementMoveInfo.java | 50 +++++++ .../RestoreFoldArrangementCallback.java | 70 +++++++++ .../arrangement/AbstractRearrangerTest.groovy | 134 +++++++++++++++--- 7 files changed, 370 insertions(+), 25 deletions(-) create mode 100644 java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerFoldingTest.groovy create mode 100644 platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementCallback.java create mode 100644 platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementMoveInfo.java create mode 100644 platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/RestoreFoldArrangementCallback.java diff --git a/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerFoldingTest.groovy b/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerFoldingTest.groovy new file mode 100644 index 000000000000..9166eaf5d4ae --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/psi/codeStyle/arrangement/JavaRearrangerFoldingTest.groovy @@ -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 java.util.List; +//import java.util.Set; +// +///** +// * Class comment +// */ +//class Test { +// +// /** +// * Another method comment +// */ +// public void test(Set s) {} +//}''', +// +// rules: [rule(PUBLIC), rule(PRIVATE)], +// +// expected: '''\ +//import java.util.List; +//import java.util.Set; +// +///** +// * Class comment +// */ +//class Test { +// +// /** +// * Method comment +// */ +// private void test(List l) {} +//}''' +// ) +// } +} diff --git a/platform/lang-impl/src/com/intellij/application/options/codeStyle/arrangement/action/RearrangeCodeAction.java b/platform/lang-impl/src/com/intellij/application/options/codeStyle/arrangement/action/RearrangeCodeAction.java index 1906a47aa724..062ddec25790 100644 --- a/platform/lang-impl/src/com/intellij/application/options/codeStyle/arrangement/action/RearrangeCodeAction.java +++ b/platform/lang-impl/src/com/intellij/application/options/codeStyle/arrangement/action/RearrangeCodeAction.java @@ -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); } diff --git a/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementCallback.java b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementCallback.java new file mode 100644 index 000000000000..3c512509f5af --- /dev/null +++ b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementCallback.java @@ -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 moveInfos); +} 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 index 4abd8d4f6b08..644971e992b4 100644 --- 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 @@ -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 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 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 ranges) { + public void arrange(@NotNull PsiFile file, @NotNull Collection 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 { + @NotNull public final List moveInfos = ContainerUtilRt.newArrayList(); + @NotNull public final Rearranger rearranger; @NotNull public final Collection> 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 Context from(@NotNull Rearranger 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 w = myWrappers.get(i); diff --git a/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementMoveInfo.java b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementMoveInfo.java new file mode 100644 index 000000000000..86e87209b69c --- /dev/null +++ b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/ArrangementMoveInfo.java @@ -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); + } +} diff --git a/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/RestoreFoldArrangementCallback.java b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/RestoreFoldArrangementCallback.java new file mode 100644 index 000000000000..8f93afe4cc50 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/psi/codeStyle/arrangement/engine/RestoreFoldArrangementCallback.java @@ -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 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); + } + }); + } + } +} diff --git a/platform/testFramework/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy b/platform/testFramework/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy index d9df73b46e20..ffe119f8bbec 100644 --- a/platform/testFramework/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy +++ b/platform/testFramework/testSrc/com/intellij/psi/codeStyle/arrangement/AbstractRearrangerTest.groovy @@ -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 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 = ''.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 + 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 = "" + int closingTagStart = buffer.indexOf(closingTag) + assert closingTagStart > 0 + int closingTagLength = 3 + handler.marker.length() // + 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 parseAttributes(@NotNull String text) { + def result = [:] + (text =~ /([^\s]+)=([^\s]+)/).each { + result[it[1]] = it[2] + } + result + } + + private static class Info { + String text + @Nullable List ranges = [] + List foldings = [] + } + + private static class FoldingInfo { + def placeholder + def start + def end + } + + private interface RichTextHandler { + String getMarker() + void handle(@NotNull Info info, @NotNull Map attributes, int start, int end) + } + + private static class RangeHandler implements RichTextHandler { + @Override String getMarker() { "range" } + + @Override + void handle(Info info, Map 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 attributes, int start, int end) { + info.foldings << new FoldingInfo(placeholder: attributes.text ?: '...', start: start, end: end) } - - [clearText.toString(), ranges] } }