| /* |
| * 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; |
| } |
| } |
| } |