blob: 60bae75fbe23b2fa8aadd3b013f699de05c3d692 [file] [log] [blame]
/*
* Copyright 2000-2014 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.impl;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.openapi.application.ApplicationManager;
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.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.ForeignLeafPsiElement;
import com.intellij.util.messages.MessageBus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.*;
public class PsiToDocumentSynchronizer extends PsiTreeChangeAdapter {
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiToDocumentSynchronizer");
private static final Key<Boolean> PSI_DOCUMENT_ATOMIC_ACTION = Key.create("PSI_DOCUMENT_ATOMIC_ACTION");
private final PsiDocumentManagerBase myPsiDocumentManager;
private final MessageBus myBus;
private final Map<Document, Pair<DocumentChangeTransaction, Integer>> myTransactionsMap = new HashMap<Document, Pair<DocumentChangeTransaction, Integer>>();
private volatile Document mySyncDocument = null;
public PsiToDocumentSynchronizer(PsiDocumentManagerBase psiDocumentManager, MessageBus bus) {
myPsiDocumentManager = psiDocumentManager;
myBus = bus;
}
@Nullable
public DocumentChangeTransaction getTransaction(final Document document) {
final Pair<DocumentChangeTransaction, Integer> pair = myTransactionsMap.get(document);
return pair != null ? pair.getFirst() : null;
}
public boolean isInSynchronization(@NotNull Document document) {
return mySyncDocument == document;
}
@TestOnly
void cleanupForNextTest() {
myTransactionsMap.clear();
mySyncDocument = null;
}
private interface DocSyncAction {
void syncDocument(@NotNull Document document, @NotNull PsiTreeChangeEventImpl event);
}
private void checkPsiModificationAllowed(@NotNull final PsiTreeChangeEvent event) {
if (!toProcessPsiEvent()) return;
final PsiFile psiFile = event.getFile();
if (psiFile == null || psiFile.getNode() == null) return;
boolean forceDocument = !psiFile.getViewProvider().isPhysical();
final Document document = forceDocument ? myPsiDocumentManager.getDocument(psiFile)
: myPsiDocumentManager.getCachedDocument(psiFile);
if (document != null && myPsiDocumentManager.isUncommited(document)) {
throw new IllegalStateException("Attempt to modify PSI for non-committed Document!");
}
}
private DocumentEx getCachedDocument(PsiFile psiFile, boolean force) {
final DocumentEx document = (DocumentEx)myPsiDocumentManager.getCachedDocument(psiFile);
if (document == null || document instanceof DocumentWindow || !force && getTransaction(document) == null) {
return null;
}
return document;
}
private void doSync(@NotNull final PsiTreeChangeEvent event, boolean force, @NotNull final DocSyncAction syncAction) {
if (!toProcessPsiEvent()) return;
final PsiFile psiFile = event.getFile();
if (psiFile == null || psiFile.getNode() == null) return;
final DocumentEx document = getCachedDocument(psiFile, force);
if (document == null) return;
performAtomically(psiFile, new Runnable() {
@Override
public void run() {
syncAction.syncDocument(document, (PsiTreeChangeEventImpl)event);
}
});
final boolean insideTransaction = myTransactionsMap.containsKey(document);
if (!insideTransaction) {
document.setModificationStamp(psiFile.getViewProvider().getModificationStamp());
if (LOG.isDebugEnabled()) {
PsiDocumentManagerBase.checkConsistency(psiFile, document);
}
}
psiFile.getViewProvider().contentsSynchronized();
}
boolean isInsideAtomicChange(@NotNull PsiFile file) {
return file.getUserData(PSI_DOCUMENT_ATOMIC_ACTION) == Boolean.TRUE;
}
public void performAtomically(@NotNull PsiFile file, @NotNull Runnable runnable) {
assert !isInsideAtomicChange(file);
file.putUserData(PSI_DOCUMENT_ATOMIC_ACTION, Boolean.TRUE);
try {
runnable.run();
}
finally {
file.putUserData(PSI_DOCUMENT_ATOMIC_ACTION, null);
}
}
@Override
public void beforeChildAddition(@NotNull PsiTreeChangeEvent event) {
checkPsiModificationAllowed(event);
}
@Override
public void beforeChildRemoval(@NotNull PsiTreeChangeEvent event) {
checkPsiModificationAllowed(event);
}
@Override
public void beforeChildReplacement(@NotNull PsiTreeChangeEvent event) {
checkPsiModificationAllowed(event);
}
@Override
public void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) {
checkPsiModificationAllowed(event);
}
@Override
public void childAdded(@NotNull final PsiTreeChangeEvent event) {
if (!(event.getChild() instanceof ForeignLeafPsiElement)) {
doSync(event, false, new DocSyncAction() {
@Override
public void syncDocument(@NotNull Document document, @NotNull PsiTreeChangeEventImpl event) {
insertString(document, event.getOffset(), event.getChild().getText());
}
});
}
}
@Override
public void childRemoved(@NotNull final PsiTreeChangeEvent event) {
if (!(event.getChild() instanceof ForeignLeafPsiElement)) {
doSync(event, false, new DocSyncAction() {
@Override
public void syncDocument(@NotNull Document document, @NotNull PsiTreeChangeEventImpl event) {
deleteString(document, event.getOffset(), event.getOffset() + event.getOldLength());
}
});
}
}
@Override
public void childReplaced(@NotNull final PsiTreeChangeEvent event) {
doSync(event, false, new DocSyncAction() {
@Override
public void syncDocument(@NotNull Document document, @NotNull PsiTreeChangeEventImpl event) {
int oldLength = event.getOldChild() instanceof ForeignLeafPsiElement ? 0 : event.getOldLength();
String newText = event.getNewChild() instanceof ForeignLeafPsiElement ? "" : event.getNewChild().getText();
replaceString(document, event.getOffset(), event.getOffset() + oldLength, newText);
}
});
}
@Override
public void childrenChanged(@NotNull final PsiTreeChangeEvent event) {
doSync(event, false, new DocSyncAction() {
@Override
public void syncDocument(@NotNull Document document, @NotNull PsiTreeChangeEventImpl event) {
replaceString(document, event.getOffset(), event.getOffset() + event.getOldLength(), event.getParent().getText());
}
});
}
private boolean myIgnorePsiEvents;
public void setIgnorePsiEvents(boolean ignorePsiEvents) {
myIgnorePsiEvents = ignorePsiEvents;
}
public boolean isIgnorePsiEvents() {
return myIgnorePsiEvents;
}
public boolean toProcessPsiEvent() {
return !myIgnorePsiEvents && !ApplicationManager.getApplication().hasWriteAction(IgnorePsiEventsMarker.class);
}
public void replaceString(Document document, int startOffset, int endOffset, String s) {
final DocumentChangeTransaction documentChangeTransaction = getTransaction(document);
if(documentChangeTransaction != null) {
documentChangeTransaction.replace(startOffset, endOffset - startOffset, s);
}
}
public void insertString(Document document, int offset, String s) {
final DocumentChangeTransaction documentChangeTransaction = getTransaction(document);
if(documentChangeTransaction != null){
documentChangeTransaction.replace(offset, 0, s);
}
}
private void deleteString(Document document, int startOffset, int endOffset){
final DocumentChangeTransaction documentChangeTransaction = getTransaction(document);
if(documentChangeTransaction != null){
documentChangeTransaction.replace(startOffset, endOffset - startOffset, "");
}
}
public void startTransaction(@NotNull Project project, @NotNull Document doc, @NotNull PsiElement scope) {
LOG.assertTrue(!project.isDisposed());
Pair<DocumentChangeTransaction, Integer> pair = myTransactionsMap.get(doc);
if (pair == null) {
final PsiFile psiFile = scope.getContainingFile();
pair = new Pair<DocumentChangeTransaction, Integer>(new DocumentChangeTransaction(doc, psiFile), 0);
myBus.syncPublisher(PsiDocumentTransactionListener.TOPIC).transactionStarted(doc, psiFile);
}
else {
pair = new Pair<DocumentChangeTransaction, Integer>(pair.getFirst(), pair.getSecond().intValue() + 1);
}
myTransactionsMap.put(doc, pair);
}
public boolean commitTransaction(final Document document){
ApplicationManager.getApplication().assertIsDispatchThread();
final DocumentChangeTransaction documentChangeTransaction = removeTransaction(document);
if(documentChangeTransaction == null) return false;
final PsiElement changeScope = documentChangeTransaction.getChangeScope();
try {
mySyncDocument = document;
final PsiTreeChangeEventImpl fakeEvent = new PsiTreeChangeEventImpl(changeScope.getManager());
fakeEvent.setParent(changeScope);
fakeEvent.setFile(changeScope.getContainingFile());
checkPsiModificationAllowed(fakeEvent);
doSync(fakeEvent, true, new DocSyncAction() {
@Override
public void syncDocument(@NotNull Document document, @NotNull PsiTreeChangeEventImpl event) {
doCommitTransaction(document, documentChangeTransaction);
}
});
myBus.syncPublisher(PsiDocumentTransactionListener.TOPIC).transactionCompleted(document, (PsiFile)changeScope);
}
finally {
mySyncDocument = null;
}
return true;
}
private static void doCommitTransaction(@NotNull Document document, @NotNull DocumentChangeTransaction documentChangeTransaction) {
DocumentEx ex = (DocumentEx) document;
ex.suppressGuardedExceptions();
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);
}
}
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();
}
}
@Nullable
private DocumentChangeTransaction removeTransaction(Document doc) {
Pair<DocumentChangeTransaction, Integer> pair = myTransactionsMap.get(doc);
if(pair == null) return null;
int nestedCount = pair.getSecond().intValue();
if(nestedCount > 0){
pair = Pair.create(pair.getFirst(), nestedCount - 1);
myTransactionsMap.put(doc, pair);
return null;
}
myTransactionsMap.remove(doc);
return pair.getFirst();
}
public boolean isDocumentAffectedByTransactions(Document document) {
return myTransactionsMap.containsKey(document);
}
public static class DocumentChangeTransaction{
private final Set<Pair<MutableTextRange,StringBuffer>> myAffectedFragments = new TreeSet<Pair<MutableTextRange, StringBuffer>>(new Comparator<Pair<MutableTextRange, StringBuffer>>() {
@Override
public int compare(final Pair<MutableTextRange, StringBuffer> o1,
final Pair<MutableTextRange, StringBuffer> o2) {
return o1.getFirst().getStartOffset() - o2.getFirst().getStartOffset();
}
});
private final Document myDocument;
private final PsiFile myChangeScope;
public DocumentChangeTransaction(@NotNull Document doc, @NotNull PsiFile scope) {
myDocument = doc;
myChangeScope = scope;
}
@NotNull
public Set<Pair<MutableTextRange, StringBuffer>> getAffectedFragments() {
return myAffectedFragments;
}
@NotNull
public PsiFile getChangeScope() {
return myChangeScope;
}
public void replace(int initialStart, 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;
int newStartInReplace = 0;
int newEndInReplace = replaceLength;
while (newStartInReplace < replaceLength && start < end && replace.charAt(newStartInReplace) == chars.charAt(start)) {
start++;
newStartInReplace++;
}
while (start < end && newStartInReplace < newEndInReplace && replace.charAt(newEndInReplace - 1) == chars.charAt(end - 1)) {
newEndInReplace--;
end--;
}
// optimization: when delete fragment from the middle of the text, prefer split at the line boundaries
if (newStartInReplace == newEndInReplace && start > 0 && start < end && StringUtil.indexOf(chars, '\n', start, end) != -1) {
// try to align to the line boundaries
while (start > 0 &&
newStartInReplace > 0 &&
chars.charAt(start - 1) == chars.charAt(end - 1) &&
chars.charAt(end - 1) != '\n'
) {
start--;
end--;
newStartInReplace--;
newEndInReplace--;
}
}
//[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();
while (start < charsSequence.length() && end < charsSequence.length() && start > 0 &&
charsSequence.subSequence(start, end).toString().endsWith("><") && charsSequence.charAt(start - 1) == '<') {
start--;
newStartInReplace--;
end--;
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);
}
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;
}
if(currentNewDocumentOffset < end){
final int effectiveStart = Math.max(currentNewDocumentOffset, start);
text.append(myDocument.getCharsSequence(),
effectiveStart - currentNewDocumentOffset + currentOldDocumentOffset,
end- currentNewDocumentOffset + currentOldDocumentOffset);
}
return text.toString();
}
private Pair<MutableTextRange, StringBuffer> getFragmentByRange(int start, final int length) {
final StringBuffer fragmentBuffer = new StringBuffer();
int end = start + length;
// 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();
}
}
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;
}
public int getEndOffset() {
return myStartOffset + myLength;
}
public int getLength() {
return myLength;
}
public String toString() {
return "[" + getStartOffset() + ", " + getEndOffset() + "]";
}
public void shift(final int lengthDiff) {
myStartOffset += lengthDiff;
}
}
}