fix PsiToDocumentSynchronizer handling of overlapping ranges rubymine/142.4534
authorpeter <peter@jetbrains.com>
Thu, 3 Sep 2015 06:18:03 +0000 (08:18 +0200)
committerpeter <peter@jetbrains.com>
Thu, 3 Sep 2015 06:29:28 +0000 (08:29 +0200)
platform/core-impl/src/com/intellij/psi/impl/PsiToDocumentSynchronizer.java
platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/RangeMarkerTest.java
xml/tests/src/com/intellij/codeInsight/XmlTagTest.java

index ef5398db1f0cd2366b2b7a713cc46c8a13dcb269..22861d1d4ff68dfdb114275a44132f1ba4c5e059 100644 (file)
@@ -22,12 +22,16 @@ import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.editor.Document;
 import com.intellij.openapi.editor.ex.DocumentEx;
 import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
 import com.intellij.openapi.util.Key;
 import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.util.TextRange;
 import com.intellij.openapi.util.text.StringUtil;
 import com.intellij.psi.*;
 import com.intellij.psi.impl.source.tree.ForeignLeafPsiElement;
+import com.intellij.util.containers.ContainerUtil;
 import com.intellij.util.messages.MessageBus;
+import com.intellij.util.text.ImmutableText;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.jetbrains.annotations.TestOnly;
@@ -120,7 +124,7 @@ public class PsiToDocumentSynchronizer extends PsiTreeChangeAdapter {
   public void performAtomically(@NotNull PsiFile file, @NotNull Runnable runnable) {
     assert !isInsideAtomicChange(file);
     file.putUserData(PSI_DOCUMENT_ATOMIC_ACTION, Boolean.TRUE);
-    
+
     try {
       runnable.run();
     }
@@ -247,7 +251,7 @@ public class PsiToDocumentSynchronizer extends PsiTreeChangeAdapter {
     ApplicationManager.getApplication().assertIsDispatchThread();
     final DocumentChangeTransaction documentChangeTransaction = removeTransaction(document);
     if(documentChangeTransaction == null) return false;
-    final PsiElement changeScope = documentChangeTransaction.getChangeScope();
+    final PsiElement changeScope = documentChangeTransaction.myChangeScope;
     try {
       mySyncDocument = document;
 
@@ -275,28 +279,12 @@ public class PsiToDocumentSynchronizer extends PsiTreeChangeAdapter {
     try {
       boolean isReadOnly = !document.isWritable();
       ex.setReadOnly(false);
-      final Set<Pair<MutableTextRange, StringBuffer>> affectedFragments = documentChangeTransaction.getAffectedFragments();
-      for (final Pair<MutableTextRange, StringBuffer> pair : affectedFragments) {
-        final StringBuffer replaceBuffer = pair.getSecond();
-        final MutableTextRange range = pair.getFirst();
-        if (replaceBuffer.length() == 0) {
-          ex.deleteString(range.getStartOffset(), range.getEndOffset());
-        }
-        else if (range.getLength() == 0) {
-          ex.insertString(range.getStartOffset(), replaceBuffer);
-        }
-        else {
-          ex.replaceString(range.getStartOffset(),
-                           range.getEndOffset(),
-                           replaceBuffer);
-        }
+
+      for (Map.Entry<TextRange, CharSequence> entry : documentChangeTransaction.myAffectedFragments.descendingMap().entrySet()) {
+        ex.replaceString(entry.getKey().getStartOffset(), entry.getKey().getEndOffset(), entry.getValue());
       }
 
       ex.setReadOnly(isReadOnly);
-      //if(documentChangeTransaction.getChangeScope() != null) {
-      //  LOG.assertTrue(document.getText().equals(documentChangeTransaction.getChangeScope().getText()),
-      //                 "Psi to document synchronization failed (send to IK)");
-      //}
     }
     finally {
       ex.unSuppressGuardedExceptions();
@@ -323,40 +311,36 @@ public class PsiToDocumentSynchronizer extends PsiTreeChangeAdapter {
 
 
   public static class DocumentChangeTransaction{
-    private final Set<Pair<MutableTextRange,StringBuffer>> myAffectedFragments = new TreeSet<Pair<MutableTextRange, StringBuffer>>(new Comparator<Pair<MutableTextRange, StringBuffer>>() {
+    private final TreeMap<TextRange, CharSequence> myAffectedFragments = new TreeMap<TextRange, CharSequence>(new Comparator<TextRange>() {
       @Override
-      public int compare(final Pair<MutableTextRange, StringBuffer> o1,
-                         final Pair<MutableTextRange, StringBuffer> o2) {
-        return o1.getFirst().getStartOffset() - o2.getFirst().getStartOffset();
+      public int compare(TextRange o1, TextRange o2) {
+        return o1.getStartOffset() - o2.getStartOffset();
       }
     });
-    private final Document myDocument;
     private final PsiFile myChangeScope;
+    private ImmutableText myDocText;
+    private ImmutableText myPsiText;
 
     public DocumentChangeTransaction(@NotNull Document doc, @NotNull PsiFile scope) {
-      myDocument = doc;
       myChangeScope = scope;
+      myDocText = ImmutableText.valueOf(doc.getImmutableCharSequence());
+      myPsiText = myDocText;
     }
 
     @NotNull
-    public Set<Pair<MutableTextRange, StringBuffer>> getAffectedFragments() {
+    public Map<TextRange, CharSequence> getAffectedFragments() {
       return myAffectedFragments;
     }
 
-    @NotNull
-    public PsiFile getChangeScope() {
-      return myChangeScope;
-    }
-
-    public void replace(int initialStart, int length, @NotNull String replace) {
+    public void replace(int psiStart, int length, @NotNull String replace) {
       // calculating fragment
       // minimize replace
       int start = 0;
       int end = start + length;
 
       final int replaceLength = replace.length();
-      final String chars = getText(start + initialStart, end + initialStart);
-      if (chars.equals(replace)) return;
+      final CharSequence chars = myPsiText.subSequence(psiStart, psiStart + length);
+      if (StringUtil.equals(chars, replace)) return;
 
       int newStartInReplace = 0;
       int newEndInReplace = replaceLength;
@@ -385,12 +369,13 @@ public class PsiToDocumentSynchronizer extends PsiTreeChangeAdapter {
         }
       }
 
+      start += psiStart;
+      end += psiStart;
+
       //[mike] dirty hack for xml:
       //make sure that deletion of <t> in: <tag><t/><tag> doesn't remove t/><
       //which is perfectly valid but invalidates range markers
-      start += initialStart;
-      end += initialStart;
-      final CharSequence charsSequence = myDocument.getCharsSequence();
+      final CharSequence charsSequence = myPsiText;
       while (start < charsSequence.length() && end < charsSequence.length() && start > 0 &&
              charsSequence.subSequence(start, end).toString().endsWith("><") && charsSequence.charAt(start - 1) == '<') {
         start--;
@@ -399,147 +384,71 @@ public class PsiToDocumentSynchronizer extends PsiTreeChangeAdapter {
         newEndInReplace--;
       }
 
-      replace = replace.substring(newStartInReplace, newEndInReplace);
-      length = end - start;
-
-      final Pair<MutableTextRange, StringBuffer> fragment = getFragmentByRange(start, length);
-      final StringBuffer fragmentReplaceText = fragment.getSecond();
-      final int startInFragment = start - fragment.getFirst().getStartOffset();
-
-      // text range adjustment
-      final int lengthDiff = replace.length() - length;
-      final Iterator<Pair<MutableTextRange, StringBuffer>> iterator = myAffectedFragments.iterator();
-      boolean adjust = false;
-      while (iterator.hasNext()) {
-        final Pair<MutableTextRange, StringBuffer> pair = iterator.next();
-        if (adjust) pair.getFirst().shift(lengthDiff);
-        if (pair == fragment) adjust = true;
-      }
-
-      fragmentReplaceText.replace(startInFragment, startInFragment + length, replace);
+      updateFragments(start, end, replace.substring(newStartInReplace, newEndInReplace));
     }
 
-    private String getText(final int start, final int end) {
-      int currentOldDocumentOffset = 0;
-      int currentNewDocumentOffset = 0;
-      StringBuilder text = new StringBuilder();
-      Iterator<Pair<MutableTextRange, StringBuffer>> iterator = myAffectedFragments.iterator();
-      while (iterator.hasNext() && currentNewDocumentOffset < end) {
-        final Pair<MutableTextRange, StringBuffer> pair = iterator.next();
-        final MutableTextRange range = pair.getFirst();
-        final StringBuffer buffer = pair.getSecond();
-        final int fragmentEndInNewDocument = range.getStartOffset() + buffer.length();
-
-        if(range.getStartOffset() <= start && fragmentEndInNewDocument >= end){
-          return buffer.substring(start - range.getStartOffset(), end - range.getStartOffset());
-        }
-
-        if(range.getStartOffset() >= start){
-          final int effectiveStart = Math.max(currentNewDocumentOffset, start);
-          text.append(myDocument.getCharsSequence(),
-                      effectiveStart - currentNewDocumentOffset + currentOldDocumentOffset,
-                      Math.min(range.getStartOffset(), end) - currentNewDocumentOffset + currentOldDocumentOffset);
-          if(end > range.getStartOffset()){
-            text.append(buffer.substring(0, Math.min(end - range.getStartOffset(), buffer.length())));
-          }
-        }
-
-        currentOldDocumentOffset += range.getEndOffset() - currentNewDocumentOffset;
-        currentNewDocumentOffset = fragmentEndInNewDocument;
-      }
+    private void updateFragments(int start, int end, @NotNull String replace) {
+      int docStart = psiToDocumentOffset(start);
+      int docEnd = psiToDocumentOffset(end);
 
-      if(currentNewDocumentOffset < end){
-        final int effectiveStart = Math.max(currentNewDocumentOffset, start);
-        text.append(myDocument.getCharsSequence(),
-                    effectiveStart - currentNewDocumentOffset + currentOldDocumentOffset,
-                    end- currentNewDocumentOffset + currentOldDocumentOffset);
-      }
+      TextRange startRange = findFragment(docStart);
+      TextRange endRange = findFragment(docEnd);
 
-      return text.toString();
-    }
+      myPsiText = myPsiText.delete(start, end).insert(start, replace);
 
-    private Pair<MutableTextRange, StringBuffer> getFragmentByRange(int start, final int length) {
-      final StringBuffer fragmentBuffer = new StringBuffer();
-      int end = start + length;
+      TextRange newFragment = new TextRange(startRange != null ? startRange.getStartOffset() : docStart,
+                                            endRange != null ? endRange.getEndOffset() : docEnd);
+      CharSequence newReplacement = myPsiText.subSequence(documentToPsiOffset(newFragment.getStartOffset(), false),
+                                                          documentToPsiOffset(newFragment.getEndOffset(), true) + replace.length() - (end - start));
 
-      // restoring buffer and remove all subfragments from the list
-      int documentOffset = 0;
-      int effectiveOffset = 0;
-
-      Iterator<Pair<MutableTextRange, StringBuffer>> iterator = myAffectedFragments.iterator();
-      while (iterator.hasNext() && effectiveOffset <= end) {
-        final Pair<MutableTextRange, StringBuffer> pair = iterator.next();
-        final MutableTextRange range = pair.getFirst();
-        final StringBuffer buffer = pair.getSecond();
-        int effectiveFragmentEnd = range.getStartOffset() + buffer.length();
-
-        if(range.getStartOffset() <= start && effectiveFragmentEnd >= end) return pair;
-
-        if(effectiveFragmentEnd >= start){
-          final int effectiveStart = Math.max(effectiveOffset, start);
-          if(range.getStartOffset() > start){
-            fragmentBuffer.append(myDocument.getCharsSequence(),
-                                  effectiveStart - effectiveOffset + documentOffset,
-                                  Math.min(range.getStartOffset(), end)- effectiveOffset + documentOffset);
-          }
-          if(end >= range.getStartOffset()){
-            fragmentBuffer.append(buffer);
-            end = end > effectiveFragmentEnd ? end - (buffer.length() - range.getLength()) : range.getEndOffset();
-            effectiveFragmentEnd = range.getEndOffset();
-            start = Math.min(start, range.getStartOffset());
-            iterator.remove();
-          }
+      for (Iterator<TextRange> iterator = myAffectedFragments.keySet().iterator(); iterator.hasNext(); ) {
+        if (iterator.next().intersects(newFragment)) {
+          iterator.remove();
         }
-
-        documentOffset += range.getEndOffset() - effectiveOffset;
-        effectiveOffset = effectiveFragmentEnd;
       }
-
-      if(effectiveOffset < end){
-        final int effectiveStart = Math.max(effectiveOffset, start);
-        fragmentBuffer.append(myDocument.getCharsSequence(),
-                              effectiveStart - effectiveOffset + documentOffset,
-                              end- effectiveOffset + documentOffset);
-      }
-
-      MutableTextRange newRange = new MutableTextRange(start, end);
-      final Pair<MutableTextRange, StringBuffer> pair = Pair.create(newRange, fragmentBuffer);
-      for (Pair<MutableTextRange, StringBuffer> affectedFragment : myAffectedFragments) {
-        MutableTextRange range = affectedFragment.getFirst();
-        assert end <= range.getStartOffset() || range.getEndOffset() <= start : "Range :"+range+"; Added: "+newRange;
-      }
-      myAffectedFragments.add(pair);
-      return pair;
-    }
-  }
-
-  public static class MutableTextRange {
-    private final int myLength;
-    private int myStartOffset;
-
-    public MutableTextRange(final int startOffset, final int endOffset) {
-      myStartOffset = startOffset;
-      myLength = endOffset - startOffset;
-    }
-
-    public int getStartOffset() {
-      return myStartOffset;
+      myAffectedFragments.put(newFragment, newReplacement);
     }
 
-    public int getEndOffset() {
-      return myStartOffset + myLength;
+    private TextRange findFragment(final int docOffset) {
+      return ContainerUtil.find(myAffectedFragments.keySet(), new Condition<TextRange>() {
+        @Override
+        public boolean value(TextRange range) {
+          return range.containsOffset(docOffset);
+        }
+      });
     }
 
-    public int getLength() {
-      return myLength;
-    }
+    private int psiToDocumentOffset(int offset) {
+      for (Map.Entry<TextRange, CharSequence> entry : myAffectedFragments.entrySet()) {
+        int lengthAfter = entry.getValue().length();
+        TextRange range = entry.getKey();
+        if (range.getStartOffset() + lengthAfter < offset) {
+          offset += range.getLength() - lengthAfter;
+          continue;
+        }
 
-    public String toString() {
-      return "[" + getStartOffset() + ", " + getEndOffset() + "]";
+        // for offsets inside replaced ranges, return the starts of the original affected fragments in document
+        return Math.min(range.getStartOffset(), offset);
+      }
+      return offset;
     }
 
-    public void shift(final int lengthDiff) {
-      myStartOffset += lengthDiff;
+    private int documentToPsiOffset(int offset, boolean greedyRight) {
+      int delta = 0;
+      for (Map.Entry<TextRange, CharSequence> entry : myAffectedFragments.entrySet()) {
+        int lengthAfter = entry.getValue().length();
+        TextRange range = entry.getKey();
+        // for offsets inside affected fragments, return either start or end of the updated range
+        if (range.containsOffset(offset)) {
+          return range.getStartOffset() + delta + (greedyRight ? lengthAfter : 0);
+        }
+        if (range.getStartOffset() > offset) {
+          break;
+        }
+        delta += lengthAfter - range.getLength();
+      }
+      return offset + delta;
     }
   }
+
 }
index 88586514303312bae13d7eb1d803d3c0522e15f0..40b50616c33287280fb39726822acf3152cd13e9 100644 (file)
@@ -420,6 +420,25 @@ public class RangeMarkerTest extends LightPlatformTestCase {
     assertValidMarker(marker, 3, 6);
   }
 
+  public void testPsi2DocTwoReplacements() {
+    RangeMarker marker = createMarker("fooFooFoo fooFooFoo", 10, 19);
+    synchronizer.startTransaction(getProject(), document, psiFile);
+    synchronizer.replaceString(document, 0, 9, "xxx");
+    synchronizer.replaceString(document, 4, 13, "xxx");
+    synchronizer.commitTransaction(document);
+    assertValidMarker(marker, 4, 7);
+  }
+
+  public void testPsi2DocThreeOverlappingReplacements() {
+    createMarker("abc", 0, 0);
+    synchronizer.startTransaction(getProject(), document, psiFile);
+    synchronizer.replaceString(document, 0, 1, "xy");
+    synchronizer.replaceString(document, 3, 4, "yz");
+    synchronizer.replaceString(document, 0, 5, "xxx");
+    synchronizer.commitTransaction(document);
+    assertEquals("xxx", document.getText());
+  }
+
   public void testPsi2DocMergeReplaceAfterAdd() throws Exception {
     StringBuilder buffer = new StringBuilder("0123456789");
     RangeMarker marker = createMarker(buffer.toString(), 2, 5);
@@ -434,8 +453,7 @@ public class RangeMarkerTest extends LightPlatformTestCase {
     synchronizer.replaceString(document, 3, 5, "bb");
     buffer.replace(3, 5, "bb");
     final PsiToDocumentSynchronizer.DocumentChangeTransaction transaction = synchronizer.getTransaction(document);
-    final Set<Pair<PsiToDocumentSynchronizer.MutableTextRange, StringBuffer>> affectedFragments = transaction.getAffectedFragments();
-    assertEquals(affectedFragments.size(), 2);
+    assertSize(2, transaction.getAffectedFragments().keySet());
 
     synchronizer.commitTransaction(document);
 
@@ -457,8 +475,7 @@ public class RangeMarkerTest extends LightPlatformTestCase {
       buffer.insert(i, "" + i);
     }
     final PsiToDocumentSynchronizer.DocumentChangeTransaction transaction = synchronizer.getTransaction(document);
-    final Set<Pair<PsiToDocumentSynchronizer.MutableTextRange, StringBuffer>> affectedFragments = transaction.getAffectedFragments();
-    assertEquals(1, affectedFragments.size());
+    assertSize(1, transaction.getAffectedFragments().keySet());
 
     synchronizer.commitTransaction(document);
 
@@ -473,19 +490,18 @@ public class RangeMarkerTest extends LightPlatformTestCase {
     RangeMarker marker = createMarker(buffer.toString(), 2, 5);
     synchronizer.startTransaction(getProject(), document, psiFile);
     final PsiToDocumentSynchronizer.DocumentChangeTransaction transaction = synchronizer.getTransaction(document);
-    final Set<Pair<PsiToDocumentSynchronizer.MutableTextRange, StringBuffer>> affectedFragments = transaction.getAffectedFragments();
-
+    assertNotNull(transaction);
 
     for (int i = 0; i < 10; i++) {
       synchronizer.insertString(document, i, "" + i);
       buffer.insert(i, "" + i);
     }
 
-    assertEquals(1, affectedFragments.size());
+    assertSize(1, transaction.getAffectedFragments().keySet());
     synchronizer.replaceString(document, 0, 20, "0123456789");
     buffer.replace(0, 20, "0123456789");
 
-    assertEquals(1, affectedFragments.size());
+    assertSize(1, transaction.getAffectedFragments().keySet());
 
     synchronizer.commitTransaction(document);
 
@@ -509,8 +525,7 @@ public class RangeMarkerTest extends LightPlatformTestCase {
     buffer.insert(7, "d");
 
     final PsiToDocumentSynchronizer.DocumentChangeTransaction transaction = synchronizer.getTransaction(document);
-    final Set<Pair<PsiToDocumentSynchronizer.MutableTextRange, StringBuffer>> affectedFragments = transaction.getAffectedFragments();
-    assertEquals(3, affectedFragments.size());
+    assertSize(3, transaction.getAffectedFragments().keySet());
 
     synchronizer.commitTransaction(document);
 
index 4d1c166badf488cbe5cb3c5fd5d0c0efc2748014..d263c5d44f5cb1d21c25ba723aa74fc743585dd2 100644 (file)
@@ -15,6 +15,7 @@
  */
 package com.intellij.codeInsight;
 
+import com.intellij.ide.highlighter.XmlFileType;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.application.Result;
 import com.intellij.openapi.command.CommandProcessor;
@@ -1006,4 +1007,18 @@ public class XmlTagTest extends LightCodeInsightTestCase {
       }
     });
   }
+
+  public void testSetName() {
+    XmlFile file = (XmlFile)PsiFileFactory.getInstance(getProject()).createFileFromText("dummy.xml", XmlFileType.INSTANCE, "<fooBarGoo>1</fooBarGoo>", 0, true);
+    final XmlTag tag = file.getDocument().getRootTag();
+    final Document document = file.getViewProvider().getDocument();
+    ApplicationManager.getApplication().runWriteAction(new Runnable() {
+      @Override
+      public void run() {
+        tag.setName("xxx");
+        assertEquals("<xxx>1</xxx>", tag.getText());
+        assertEquals("<xxx>1</xxx>", document.getText());
+      }
+    });
+  }
 }