diff --git a/platform/lang-impl/src/com/intellij/application/options/DocumentChangesCollector.java b/platform/lang-impl/src/com/intellij/application/options/DocumentChangesCollector.java deleted file mode 100644 index fc429ae452fa..000000000000 --- a/platform/lang-impl/src/com/intellij/application/options/DocumentChangesCollector.java +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright 2000-2011 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.application.options; - -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.TextChange; -import com.intellij.openapi.editor.event.DocumentEvent; -import com.intellij.openapi.editor.event.DocumentListener; -import com.intellij.openapi.editor.impl.TextChangeImpl; -import gnu.trove.TIntArrayList; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; - -/** - * Aggregates, merges and exposes information about {@link Document document} changes. - *

- * Intended usage scenario is to collect information about format preview document changes occurred during - * tweaking format options and highlight corresponding sections. - *

- * Note: current algorithm does consider changing particular text by the same text to be a document change, i.e. consider - * the document like 'abcde'. Suppose it's text from range [2; 4) ('cd') is replaced by the same - * text. It's still to be considered as a change. - *

- * Not thread-safe. - * - * @author Denis Zhdanov - * @since 10/11/10 5:10 PM - */ -public class DocumentChangesCollector implements DocumentListener { - - /** - * Holds merged document changes. - *

- * Is assumed to be always sorted by change start offset in ascending order. - */ - private final List myChanges = new ArrayList(); - - private boolean myCollectChanges; - - @Override - public void beforeDocumentChange(DocumentEvent event) { - } - - @Override - public void documentChanged(DocumentEvent event) { - if (!myCollectChanges) { - return; - } - - // The general algorithm is as follows: - // 1. Drop or cut stored change ranges for removed range if any; - // 2. Update offsets of all ranges that lays after the last change range identified by the given document change event; - // 3. Merge added range with registered changes if any; - // 4. Merge all adjacent ranges if any; - StringBuilder oldText = new StringBuilder(event.getOldFragment()); - - cutChangesIfNecessary(event, oldText); - updateChanges(event, oldText); - mergeChangesIfNecessary(event); - } - - /** - * Cuts or removes stored change ranges for the given interval if any. - * - * @param event event for just occurred document change - * @param oldText our main idea is to merge all changes in order to have aggregated change against original document. So, there is - * a possible case that subsequent change affects text inserted by the previous one. We don't want to reflect that - * at aggregated change as it didn't appear in initial document. Hence, we have a mutable symbols buffer that holds - * symbols from initial document affected by the given change - */ - private void cutChangesIfNecessary(DocumentEvent event, StringBuilder oldText) { - if (myChanges.isEmpty()) { - return; - } - - int start = event.getOffset(); - int end = event.getOffset() + event.getOldLength(); - int diff = event.getNewLength() - event.getOldLength(); - - int forwardIndex = findIndex(start); - int backwardIndexStart = forwardIndex - 1; - if (forwardIndex < 0) { - backwardIndexStart = myChanges.size() - 1; - } - else { - TIntArrayList indices = new TIntArrayList(); - for (; forwardIndex < myChanges.size(); forwardIndex++) { - TextChangeImpl change = myChanges.get(forwardIndex); - // Stored change is not affected by the current change. - if (change.getStart() >= end) { - change.advance(diff); - } - - // Stored change region is completely covered by the given change. - else if (change.getEnd() <= end) { - indices.add(forwardIndex); - - int deleteStart = start - change.getStart(); - deleteStart = Math.max(0, deleteStart); - - int deleteEnd = change.getEnd() - change.getStart(); - deleteEnd = Math.min(oldText.length(), Math.max(0, deleteEnd)); - - oldText.delete(deleteStart, deleteEnd); - oldText.insert(0, change.getText()); - } - - // Stored change region's start is covered by the current change. - else { - int deleteStart = change.getStart() - start; - deleteStart = Math.min(oldText.length(), Math.max(0, deleteStart)); - - int deleteEnd = oldText.length(); - deleteEnd = Math.min(oldText.length(), Math.max(0, deleteEnd)); - - oldText.delete(deleteStart, deleteEnd); - myChanges.set(forwardIndex, new TextChangeImpl(change.getText(), end + diff, change.getEnd() + diff)); - } - } - - if (!indices.isEmpty()) { - for (int i = indices.size() - 1; i >= 0; i--) { - myChanges.remove(indices.get(i)); - } - } - } - - for (int i = Math.min(backwardIndexStart, myChanges.size() - 1); i >= 0; i--) { - TextChangeImpl change = myChanges.get(i); - if (change.getEnd() <= start) { - break; - } - - CharSequence textToUse = change.getText(); - int symbolsToCut = Math.min(change.getEnd(), end) - start; - if (textToUse.length() >= symbolsToCut) { - oldText.insert(symbolsToCut, textToUse.subSequence(textToUse.length() - symbolsToCut, textToUse.length())); - textToUse = textToUse.subSequence(0, textToUse.length() - symbolsToCut); - } - oldText.delete(0, symbolsToCut); - myChanges.set(i, new TextChangeImpl(textToUse, change.getStart(), start)); - if (change.getEnd() > end) { - int shift = event.getOffset() + event.getNewLength() - end; - TextChangeImpl changeTail = new TextChangeImpl("", end + shift, change.getEnd() + shift); - if (i >= myChanges.size() - 1) { - myChanges.add(changeTail); - } - else { - myChanges.add(i + 1, changeTail); - } - } - } - } - - /** - * Updates current object's state on the basis of the given event assuming that there are no stored change ranges that - * start after offset denoted by the given event. - * - * @param event event for just occurred document change - * @param oldText our main idea is to merge all changes in order to have aggregated change against original document. So, there is - * a possible case that subsequent change affects text inserted by the previous one. We don't want to reflect that - * at aggregated change as it didn't appear in initial document. Hence, we have a mutable symbols buffer that holds - * symbols from initial document affected by the given change - */ - private void updateChanges(DocumentEvent event, StringBuilder oldText) { - int i = findIndex(event.getOffset()); - int endOffset = event.getOffset() + event.getNewLength(); - TextChangeImpl change = new TextChangeImpl(oldText, event.getOffset(), endOffset); - if (i < 0) { - myChanges.add(change); - } - else { - myChanges.add(i, change); - } - } - - private void mergeChangesIfNecessary(DocumentEvent event) { - // There is a possible case that we had more than one scattered change (e.g. (3; 5) and (8; 10)) and current document change affects - // both of them (e.g. remove all symbols from offset (4; 9)). We have two changes then: (3; 4) and (4; 5) and want to merge them - // into a single one. - if (myChanges.size() < 2) { - return; - } - TextChangeImpl next = myChanges.get(myChanges.size() - 1); - for (int i = myChanges.size() - 2; i >= 0; i--) { - TextChangeImpl current = myChanges.get(i); - if (current.getEnd() < event.getOffset()) { - // Assuming that change ranges are always kept at normalized form. - break; - } - if (current.getEnd() == next.getStart()) { - myChanges.set(i, next = new TextChangeImpl(current.getText().toString() + next.getText(), current.getStart(), next.getEnd())); - myChanges.remove(i + 1); - } - else { - next = current; - } - } - } - - /** - * Returns aggregated and merged document changes sorted by their start offsets in ascending order. - *

- * Example - *

-   * 
    - *
  1. Suppose we have a document with text '0123456';
  2. - *
  3. - * Document range [1; 3) is removed (text '12' is removed, current text is - * '03456'); - *
  4. - *
  5. - * Document range [1; 4) ('345') is replaced to 'abc' - * (current text is '0abc6'); - *
  6. - *
- *
- *

- * So, initial document is '0123456'; final one is '0abc6'. We expect single aggregated change - * to be returned then - [1; 4) with text '12345', i.e. the range defines offset within the - * current document and text shows initial document text at that range. - * - * @return aggregated and merged document changes sorted by their start offsets in ascending order - */ - @NotNull - public List getChanges() { - return myChanges; - } - - /** - * Allows to switch 'collect document changes' mode. - *

- * Default value is 'false', i.e. document changes are not collected. - * - * @param collectChanges new value of 'collect document changes' mode - */ - public void setCollectChanges(boolean collectChanges) { - myCollectChanges = collectChanges; - } - - /** - * Forces current object to drop all information about document changes if any. - */ - public void reset() { - myChanges.clear(); - } - - /** - * Finds index of the {@link #myCollectChanges first stored changed document ranges} which start offset is equal or greater to the - * given one. - * - * @param offset start offset of target document change - * @return non-negative value as an indication that there is not stored changed document range which start offset is equal - * or greater than the given offset; negative value otherwise - */ - private int findIndex(int offset) { - if (myChanges.isEmpty()) { - return -1; - } - - // We assume that document is changed sequentially from start to end, hence, it's worth to perform quick offset comparison with - // the last stored change if any - TextChangeImpl change = myChanges.get(myChanges.size() - 1); - if (offset > change.getStart()) { - return -1; - } - - int start = 0; - int end = myChanges.size() - 1; - int result = -1; - - // We inline binary search here mainly because TextChange class is immutable and we don't want unnecessary expenses on - // new key object construction on every method call. - while (start <= end) { - result = (end + start) >>> 1; - change = myChanges.get(result); - if (change.getStart() < offset) { - start = ++result; - continue; - } - if (change.getStart() > offset) { - end = result - 1; - continue; - } - break; - } - - return result; - } -} diff --git a/platform/platform-tests/testSrc/com/intellij/application/options/DocumentChangesCollectorTest.java b/platform/platform-tests/testSrc/com/intellij/application/options/DocumentChangesCollectorTest.java deleted file mode 100644 index de4a35cf2e95..000000000000 --- a/platform/platform-tests/testSrc/com/intellij/application/options/DocumentChangesCollectorTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.intellij.application.options; - -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.TextChange; -import com.intellij.openapi.editor.impl.TextChangeImpl; -import com.intellij.openapi.editor.impl.event.DocumentEventImpl; -import org.jmock.Expectations; -import org.jmock.Mockery; -import org.jmock.integration.junit4.JUnit4Mockery; -import org.jmock.lib.legacy.ClassImposteriser; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import java.util.List; - -import static java.util.Arrays.asList; -import static org.junit.Assert.*; -import static org.junit.Assert.assertEquals; - -/** - * @author Denis Zhdanov - * @since 10/12/2010 - */ -public class DocumentChangesCollectorTest { - - private static final String TEXT = "0123456789abcdefghijklmnopqrstuvwxyz"; - - private static long ourCounter; - - private StringBuilder buffer; - private DocumentChangesCollector myCollector; - private Mockery myMockery; - private Document myDocument; - - @Before - public void setUp() { - myCollector = new DocumentChangesCollector(); - buffer = new StringBuilder(TEXT); - myCollector.setCollectChanges(true); - myMockery = new JUnit4Mockery() {{ - setImposteriser(ClassImposteriser.INSTANCE); - }}; - - myDocument = myMockery.mock(Document.class); - - myMockery.checking(new Expectations() {{ - allowing(myDocument).getTextLength(); will(returnValue(TEXT.length())); - }}); - } - - @After - public void checkExpectations() { - myMockery.assertIsSatisfied(); - } - - @Test - public void emptyCollector() { - assertNotNull(myCollector.getChanges()); - } - - @Test - public void scatteredChanges() { - populate(0, 0, "AB"); - populate(5, 5, "CD"); - checkResult(new TextChangeImpl("", 0, 2), new TextChangeImpl("", 5, 7)); - } - - @Test - public void insertAndReplaceAtEnd() { - populate(0, 0, "AB"); - populate(1, 4, "CD"); - checkResult(new TextChangeImpl("01", 0, 3)); - } - - @Test - public void insertAndReplaceAtStart() { - populate(2, 2, "AB"); - populate(1, 3, "CD"); - checkResult(new TextChangeImpl("1", 1, 4)); - } - - @Test - public void removeAndReplace() { - populate(1, 3, ""); - populate(1, 4, "ABC"); - checkResult(new TextChangeImpl("12345", 1, 4)); - } - - @Test - public void removedChangeWithNonStrictBoundaryMatch() { - populate(3, 5, "AB"); - populate(2, 4, ""); - checkResult(new TextChangeImpl("234", 2, 3)); - } - - @Test - public void removedChangeWithStrictBoundaryMatch() { - populate(3, 5, "AB"); - populate(3, 5, ""); - checkResult(new TextChangeImpl("34", 3, 3)); - } - - @Test - public void cutChangeFromEnd() { - populate(3, 3, "ABCD"); - populate(4, 8, ""); - checkResult(new TextChangeImpl("3", 3, 4)); - } - - @Test - public void cutChangeFromStart() { - populate(3, 3, "ABCD"); - populate(2, 4, ""); - checkResult(new TextChangeImpl("2", 2, 5)); - } - - @Test - public void cutInTheMiddleOfInsertedText() { - populate(3, 3, "ABCDEF"); - populate(5, 8, ""); - checkResult(new TextChangeImpl("", 3, 6)); - } - - @Test - public void cutInTheMiddleOfReplacedText() { - populate(3, 9, "ABCDEF"); - populate(5, 8, ""); - checkResult(new TextChangeImpl("345678", 3, 6)); - } - - @Test - public void cutMultipleChanges() { - populate(3, 3, "ABC"); - populate(8, 8, "DEF"); - populate(5, 9, ""); - checkResult(new TextChangeImpl("34", 3, 7)); - } - - @Test - public void mergeChangeFromStart() { - populate(3, 3, "ABC"); - populate(4, 4, "DEF"); - checkResult(new TextChangeImpl("", 3, 9)); - } - - @Test - public void mergeChangeFromEnd() { - populate(3, 3, "AB"); - populate(1, 4, "CDEF"); - checkResult(new TextChangeImpl("12", 1, 6)); - } - - @Test - public void mergeMultipleChangesInOne() { - populate(3, 3, "AB"); - populate(9, 9, "CD"); - populate(5, 9, "EFGH"); - checkResult(new TextChangeImpl("3456", 3, 11)); - } - - @Test - public void mergeChangesSequence() { - populate(1, 1, "AB"); - populate(3, 3, "CD"); - populate(5, 5, "EFGH"); - checkResult(new TextChangeImpl("", 1, 9)); - } - - @Test - public void replaceAndInsertAtEnd() { - populate(4, 8, "ABCD"); - populate(5, 5, "EFGHIJKL"); - checkResult(new TextChangeImpl("4567", 4, 16)); - } - - private void populate(int start, int end, String newText) { - DocumentEventImpl event = new DocumentEventImpl( - myDocument, start, buffer.substring(start, end), newText, ++ourCounter, false - ); - buffer.replace(start, end, newText); - myCollector.beforeDocumentChange(event); - myCollector.documentChanged(event); - } - - private void checkResult(TextChange ... expected) { - List actual = myCollector.getChanges(); - assertEquals(asList(expected), actual); - } -} \ No newline at end of file