diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/BulkChangesMerger.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/BulkChangesMerger.java index 96eb9afcab7e..c2d8c806038a 100644 --- a/platform/core-impl/src/com/intellij/openapi/editor/impl/BulkChangesMerger.java +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/BulkChangesMerger.java @@ -17,7 +17,6 @@ package com.intellij.openapi.editor.impl; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.TextChange; -import com.intellij.util.text.CharArrayCharSequence; import com.intellij.util.text.StringFactory; import org.jetbrains.annotations.NotNull; @@ -174,6 +173,20 @@ public class BulkChangesMerger { data[i + offset] = text.charAt(i); } } + + /** + * Given an offset of some location in the document, returns offset of this location after application of given changes. List of changes + * is supposed to satisfy the same constraints as required by {@link #mergeToCharSequence(char[], int, List)} method. + */ + public int updateOffset(int originalOffset, @NotNull List changes) { + int offset = originalOffset; + for (TextChange change : changes) { + if (originalOffset > change.getStart()) { + offset += change.getText().length() - (change.getEnd() - change.getStart()); + } + } + return offset; + } private static class Context { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/actions/ReformatCodeProcessor.java b/platform/lang-impl/src/com/intellij/codeInsight/actions/ReformatCodeProcessor.java index 0bfb4cc31238..989a21b81d86 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/actions/ReformatCodeProcessor.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/actions/ReformatCodeProcessor.java @@ -20,6 +20,8 @@ import com.intellij.codeInsight.CodeInsightBundle; import com.intellij.formatting.FormattingProgressTask; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; @@ -34,8 +36,11 @@ import com.intellij.util.diff.FilesTooBigForDiffException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.awt.*; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; @@ -106,8 +111,6 @@ public class ReformatCodeProcessor extends AbstractLayoutCodeProcessor { throws IncorrectOperationException { return new FutureTask(new Callable() { - private Document myDocument; - @Override public Boolean call() throws Exception { FormattingProgressTask.FORMATTING_CANCELLED_FLAG.set(false); @@ -115,16 +118,18 @@ public class ReformatCodeProcessor extends AbstractLayoutCodeProcessor { Collection ranges = getRangesToFormat(processChangedTextOnly, file); CharSequence before = null; + Document document = PsiDocumentManager.getInstance(myProject).getDocument(file); if (getInfoCollector() != null) { - myDocument = PsiDocumentManager.getInstance(myProject).getDocument(file); - LOG.assertTrue(myDocument != null); - before = myDocument.getImmutableCharSequence(); + LOG.assertTrue(document != null); + before = document.getImmutableCharSequence(); } + CaretVisualPositionKeeper caretPositionKeeper = new CaretVisualPositionKeeper(document); CodeStyleManager.getInstance(myProject).reformatText(file, ranges); + caretPositionKeeper.restoreOriginalLocation(); if (before != null) { - prepareUserNotificationMessage(myDocument, before); + prepareUserNotificationMessage(document, before); } return !FormattingProgressTask.FORMATTING_CANCELLED_FLAG.get(); @@ -165,4 +170,32 @@ public class ReformatCodeProcessor extends AbstractLayoutCodeProcessor { return !myRanges.isEmpty() ? myRanges : ContainerUtil.newArrayList(file.getTextRange()); } + + private static class CaretVisualPositionKeeper { + private final Map myCaretRelativeVerticalPositions = new HashMap(); + + private CaretVisualPositionKeeper(@Nullable Document document) { + if (document == null) return; + + Editor[] editors = EditorFactory.getInstance().getEditors(document); + for (Editor editor : editors) { + Rectangle visibleArea = editor.getScrollingModel().getVisibleArea(); + Point pos = editor.visualPositionToXY(editor.getCaretModel().getVisualPosition()); + int relativePosition = pos.y - visibleArea.y; + myCaretRelativeVerticalPositions.put(editor, relativePosition); + } + } + + private void restoreOriginalLocation() { + for (Map.Entry e : myCaretRelativeVerticalPositions.entrySet()) { + Editor editor = e.getKey(); + int relativePosition = e.getValue(); + Point caretLocation = editor.visualPositionToXY(editor.getCaretModel().getVisualPosition()); + int scrollOffset = caretLocation.y - relativePosition; + editor.getScrollingModel().disableAnimation(); + editor.getScrollingModel().scrollVertically(scrollOffset); + editor.getScrollingModel().enableAnimation(); + } + } + } } \ No newline at end of file diff --git a/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java index b4790254c960..029a244e73c2 100644 --- a/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java +++ b/platform/lang-impl/src/com/intellij/formatting/FormatProcessor.java @@ -20,6 +20,8 @@ import com.intellij.lang.ASTNode; import com.intellij.lang.Language; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.TextChange; import com.intellij.openapi.editor.ex.DocumentEx; import com.intellij.openapi.editor.impl.BulkChangesMerger; @@ -349,9 +351,7 @@ class FormatProcessor { { FormattingDocumentModel documentModel = model.getDocumentModel(); Document document = documentModel.getDocument(); - if (document == null) { - return false; - } + CaretOffsetUpdater caretOffsetUpdater = new CaretOffsetUpdater(document); List changes = new ArrayList(); int shift = 0; @@ -363,6 +363,7 @@ class FormatProcessor { whiteSpace.getEndOffset(), block.getNode(), false ); if (changes.size() > 10000) { + caretOffsetUpdater.update(changes); CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes); document.replaceString(0, document.getTextLength(), mergeResult); shift += currentIterationShift; @@ -373,8 +374,10 @@ class FormatProcessor { currentIterationShift += change.getDiff(); changes.add(change); } + caretOffsetUpdater.update(changes); CharSequence mergeResult = BulkChangesMerger.INSTANCE.mergeToCharSequence(document.getChars(), document.getTextLength(), changes); document.replaceString(0, document.getTextLength(), mergeResult); + caretOffsetUpdater.restoreCaretLocations(); cleanupBlocks(blocksToModify); return true; } @@ -1487,4 +1490,28 @@ class FormatProcessor { } } } + + private static class CaretOffsetUpdater { + private final Map myCaretOffsets = new HashMap(); + + private CaretOffsetUpdater(@NotNull Document document) { + Editor[] editors = EditorFactory.getInstance().getEditors(document); + for (Editor editor : editors) { + myCaretOffsets.put(editor, editor.getCaretModel().getOffset()); + } + } + + private void update(@NotNull List changes) { + BulkChangesMerger merger = BulkChangesMerger.INSTANCE; + for (Map.Entry entry : myCaretOffsets.entrySet()) { + entry.setValue(merger.updateOffset(entry.getValue(), changes)); + } + } + + private void restoreCaretLocations() { + for (Map.Entry entry : myCaretOffsets.entrySet()) { + entry.getKey().getCaretModel().moveToOffset(entry.getValue()); + } + } + } } diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/BulkChangesMergerTest.java b/platform/platform-tests/testSrc/com/intellij/formatting/BulkChangesMergerTest.java index 6fdb8a512ab9..4eae92aab3ac 100644 --- a/platform/platform-tests/testSrc/com/intellij/formatting/BulkChangesMergerTest.java +++ b/platform/platform-tests/testSrc/com/intellij/formatting/BulkChangesMergerTest.java @@ -127,6 +127,18 @@ public class BulkChangesMergerTest { doTest("0123456789ABC", "0abc1358d9eBC", c("abc", 1), c("", 2, 3), c("", 4, 5), c("", 6, 8), c("d", 9), c("e", 10, 11)); } + @Test + public void updateOffset() { + doTestUpdateOffset(5, 5, c("a", 6)); + doTestUpdateOffset(5, 6, c("a", 3)); + doTestUpdateOffset(5, 5, c("a", 6, 7)); + doTestUpdateOffset(5, 5, c("a", 3, 4)); + doTestUpdateOffset(5, 4, c("", 3, 4)); + doTestUpdateOffset(5, 5, c("", 6, 7)); + + doTestUpdateOffset(5, 2, c("", 0, 1), c("", 2, 3), c("", 3, 4)); + } + //@Config(inplace = true) //@Test //public void client() { @@ -176,6 +188,10 @@ public class BulkChangesMergerTest { assertEquals(expected, actual.toString()); } } + + private void doTestUpdateOffset(int initialOffset, int expectedOffset, TextChangeImpl... changes) { + assertEquals(expectedOffset, myMerger.updateOffset(initialOffset, Arrays.asList(changes))); + } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)