/* * Copyright 2000-2016 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.pom.core.impl; import com.intellij.lang.ASTNode; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressIndicatorProvider; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.UserDataHolderBase; import com.intellij.pom.PomModel; import com.intellij.pom.PomModelAspect; import com.intellij.pom.PomTransaction; import com.intellij.pom.event.PomModelEvent; import com.intellij.pom.event.PomModelListener; import com.intellij.pom.impl.PomTransactionBase; import com.intellij.pom.tree.TreeAspect; import com.intellij.pom.tree.TreeAspectEvent; import com.intellij.psi.*; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.psi.impl.*; import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl; import com.intellij.psi.impl.source.DummyHolder; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.psi.impl.source.text.BlockSupportImpl; import com.intellij.psi.impl.source.text.DiffLog; import com.intellij.psi.impl.source.tree.FileElement; import com.intellij.psi.impl.source.tree.LeafElement; import com.intellij.psi.impl.source.tree.TreeElement; import com.intellij.psi.impl.source.tree.TreeUtil; import com.intellij.psi.text.BlockSupport; import com.intellij.psi.tree.IElementType; import com.intellij.psi.tree.IReparseableLeafElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.util.PsiUtilCore; import com.intellij.util.IncorrectOperationException; import com.intellij.util.ThrowableRunnable; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.Stack; import com.intellij.util.lang.CompoundRuntimeException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class PomModelImpl extends UserDataHolderBase implements PomModel { private static final Logger LOG = Logger.getInstance("#com.intellij.pom.core.impl.PomModelImpl"); private final Project myProject; private final Map<Class<? extends PomModelAspect>, PomModelAspect> myAspects = new HashMap<>(); private final Map<PomModelAspect, List<PomModelAspect>> myIncidence = new HashMap<>(); private final Map<PomModelAspect, List<PomModelAspect>> myInvertedIncidence = new HashMap<>(); private final Collection<PomModelListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); public PomModelImpl(Project project) { myProject = project; } @Override public <T extends PomModelAspect> T getModelAspect(@NotNull Class<T> aClass) { //noinspection unchecked return (T)myAspects.get(aClass); } @Override public void registerAspect(@NotNull Class<? extends PomModelAspect> aClass, @NotNull PomModelAspect aspect, @NotNull Set<PomModelAspect> dependencies) { myAspects.put(aClass, aspect); final Iterator<PomModelAspect> iterator = dependencies.iterator(); final List<PomModelAspect> deps = new ArrayList<>(); // todo: reorder dependencies while (iterator.hasNext()) { final PomModelAspect depend = iterator.next(); deps.addAll(getAllDependencies(depend)); } deps.add(aspect); // add self to block same aspect transactions from event processing and update for (final PomModelAspect pomModelAspect : deps) { final List<PomModelAspect> pomModelAspects = myInvertedIncidence.get(pomModelAspect); if (pomModelAspects != null) { pomModelAspects.add(aspect); } else { myInvertedIncidence.put(pomModelAspect, new ArrayList<>(Collections.singletonList(aspect))); } } myIncidence.put(aspect, deps); } //private final Pair<PomModelAspect, PomModelAspect> myHolderPair = new Pair<PomModelAspect, PomModelAspect>(null, null); private List<PomModelAspect> getAllDependencies(PomModelAspect aspect){ List<PomModelAspect> pomModelAspects = myIncidence.get(aspect); return pomModelAspects != null ? pomModelAspects : Collections.emptyList(); } private List<PomModelAspect> getAllDependants(PomModelAspect aspect){ List<PomModelAspect> pomModelAspects = myInvertedIncidence.get(aspect); return pomModelAspects != null ? pomModelAspects : Collections.emptyList(); } @Override public void addModelListener(@NotNull PomModelListener listener) { myListeners.add(listener); } @Override public void addModelListener(@NotNull final PomModelListener listener, @NotNull Disposable parentDisposable) { addModelListener(listener); Disposer.register(parentDisposable, new Disposable() { @Override public void dispose() { removeModelListener(listener); } }); } @Override public void removeModelListener(@NotNull PomModelListener listener) { myListeners.remove(listener); } private final Stack<Pair<PomModelAspect, PomTransaction>> myBlockedAspects = new Stack<>(); @Override public void runTransaction(@NotNull PomTransaction transaction) throws IncorrectOperationException{ if (!isAllowPsiModification()) { throw new IncorrectOperationException("Must not modify PSI inside save listener"); } synchronized(PsiLock.LOCK){ List<Throwable> throwables = new ArrayList<>(0); final PomModelAspect aspect = transaction.getTransactionAspect(); startTransaction(transaction); try{ DebugUtil.startPsiModification(null); myBlockedAspects.push(Pair.create(aspect, transaction)); final PomModelEvent event; try{ transaction.run(); event = transaction.getAccumulatedEvent(); } catch (ProcessCanceledException e) { throw e; } catch(Exception e){ LOG.error(e); return; } finally{ myBlockedAspects.pop(); } final Pair<PomModelAspect,PomTransaction> block = getBlockingTransaction(aspect, transaction); if(block != null){ final PomModelEvent currentEvent = block.getSecond().getAccumulatedEvent(); currentEvent.merge(event); return; } { // update final Set<PomModelAspect> changedAspects = event.getChangedAspects(); final Collection<PomModelAspect> dependants = new LinkedHashSet<>(); for (final PomModelAspect pomModelAspect : changedAspects) { dependants.addAll(getAllDependants(pomModelAspect)); } for (final PomModelAspect modelAspect : dependants) { if (!changedAspects.contains(modelAspect)) { modelAspect.update(event); } } } for (final PomModelListener listener : myListeners) { final Set<PomModelAspect> changedAspects = event.getChangedAspects(); for (PomModelAspect modelAspect : changedAspects) { if (listener.isAspectChangeInteresting(modelAspect)) { listener.modelChanged(event); break; } } } } catch (ProcessCanceledException e) { throw e; } catch (Throwable t) { throwables.add(t); } finally { try { commitTransaction(transaction); } catch (ProcessCanceledException e) { throw e; } catch (Throwable t) { throwables.add(t); } finally { DebugUtil.finishPsiModification(); } if (!throwables.isEmpty()) CompoundRuntimeException.throwIfNotEmpty(throwables); } } } @Nullable private Pair<PomModelAspect,PomTransaction> getBlockingTransaction(final PomModelAspect aspect, PomTransaction transaction) { final List<PomModelAspect> allDependants = getAllDependants(aspect); for (final PomModelAspect pomModelAspect : allDependants) { final ListIterator<Pair<PomModelAspect, PomTransaction>> blocksIterator = myBlockedAspects.listIterator(myBlockedAspects.size()); while (blocksIterator.hasPrevious()) { final Pair<PomModelAspect, PomTransaction> pair = blocksIterator.previous(); if (pomModelAspect == pair.getFirst() && // aspect dependence PsiTreeUtil.isAncestor(pair.getSecond().getChangeScope(), transaction.getChangeScope(), false) && // target scope contain current getContainingFileByTree(pair.getSecond().getChangeScope()) != null // target scope physical ) { return pair; } } } return null; } private void commitTransaction(final PomTransaction transaction) { final ProgressIndicator progressIndicator = ProgressIndicatorProvider.getGlobalProgressIndicator(); final PsiDocumentManagerBase manager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); final PsiToDocumentSynchronizer synchronizer = manager.getSynchronizer(); final PsiFile containingFileByTree = getContainingFileByTree(transaction.getChangeScope()); Document document = containingFileByTree != null ? manager.getCachedDocument(containingFileByTree) : null; boolean docSynced = false; if (document != null) { final int oldLength = containingFileByTree.getTextLength(); docSynced = synchronizer.commitTransaction(document); if (docSynced) { BlockSupportImpl.sendAfterChildrenChangedEvent((PsiManagerImpl)PsiManager.getInstance(myProject), containingFileByTree, oldLength, true); } } if (containingFileByTree != null) { boolean isFromCommit = ApplicationManager.getApplication().isDispatchThread() && ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject)).isCommitInProgress(); if (!isFromCommit && !synchronizer.isIgnorePsiEvents()) { reparseParallelTrees(containingFileByTree, synchronizer); if (docSynced) { containingFileByTree.getViewProvider().contentsSynchronized(); } } } if (progressIndicator != null) progressIndicator.finishNonCancelableSection(); } private void reparseParallelTrees(PsiFile changedFile, PsiToDocumentSynchronizer synchronizer) { List<PsiFile> allFiles = changedFile.getViewProvider().getAllFiles(); if (allFiles.size() <= 1) { return; } CharSequence newText = changedFile.getNode().getChars(); for (final PsiFile file : allFiles) { FileElement fileElement = file == changedFile ? null : ((PsiFileImpl)file).getTreeElement(); Runnable changeAction = fileElement == null ? null : reparseFile(file, fileElement, newText); if (changeAction == null) continue; synchronizer.setIgnorePsiEvents(true); try { CodeStyleManager.getInstance(file.getProject()).performActionWithFormatterDisabled(changeAction); } finally { synchronizer.setIgnorePsiEvents(false); } } } @Nullable private Runnable reparseFile(@NotNull final PsiFile file, @NotNull FileElement treeElement, @NotNull CharSequence newText) { TextRange changedPsiRange = DocumentCommitThread.getChangedPsiRange(file, treeElement, newText); if (changedPsiRange == null) return null; Runnable reparseLeaf = tryReparseOneLeaf(treeElement, newText, changedPsiRange); if (reparseLeaf != null) return reparseLeaf; final DiffLog log = BlockSupport.getInstance(myProject).reparseRange(file, treeElement, changedPsiRange, newText, new EmptyProgressIndicator(), treeElement.getText()); return () -> runTransaction(new PomTransactionBase(file, getModelAspect(TreeAspect.class)) { @Nullable @Override public PomModelEvent runInner() throws IncorrectOperationException { return new TreeAspectEvent(PomModelImpl.this, log.performActualPsiChange(file)); } }); } @Nullable private static Runnable tryReparseOneLeaf(@NotNull FileElement treeElement, @NotNull CharSequence newText, @NotNull TextRange changedPsiRange) { final LeafElement leaf = treeElement.findLeafElementAt(changedPsiRange.getStartOffset()); IElementType leafType = leaf == null ? null : leaf.getElementType(); if (!(leafType instanceof IReparseableLeafElementType)) return null; CharSequence newLeafText = getLeafChangedText(leaf, treeElement, newText, changedPsiRange); //noinspection unchecked final ASTNode copy = newLeafText == null ? null : ((IReparseableLeafElementType)leafType).reparseLeaf(leaf, newLeafText); return copy == null ? null : () -> leaf.getTreeParent().replaceChild(leaf, copy); } private static CharSequence getLeafChangedText(LeafElement leaf, FileElement treeElement, CharSequence newFileText, TextRange changedPsiRange) { if (leaf.getTextRange().getEndOffset() >= changedPsiRange.getEndOffset()) { int leafStart = leaf.getTextRange().getStartOffset(); int newLeafEnd = newFileText.length() - (treeElement.getTextLength() - leaf.getTextRange().getEndOffset()); if (newLeafEnd > leafStart) { return newFileText.subSequence(leafStart, newLeafEnd); } } return null; } private void startTransaction(@NotNull PomTransaction transaction) { final ProgressIndicator progressIndicator = ProgressIndicatorProvider.getGlobalProgressIndicator(); if(progressIndicator != null) progressIndicator.startNonCancelableSection(); final PsiDocumentManagerBase manager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); final PsiToDocumentSynchronizer synchronizer = manager.getSynchronizer(); final PsiElement changeScope = transaction.getChangeScope(); final PsiFile containingFileByTree = getContainingFileByTree(changeScope); if (containingFileByTree != null && !(containingFileByTree instanceof DummyHolder) && !manager.isCommitInProgress()) { PsiUtilCore.ensureValid(containingFileByTree); } boolean physical = changeScope.isPhysical(); if (physical && synchronizer.toProcessPsiEvent()) { // fail-fast to prevent any psi modifications that would cause psi/document text mismatch // PsiToDocumentSynchronizer assertions happen inside event processing and are logged by PsiManagerImpl.fireEvent instead of being rethrown // so it's important to throw something outside event processing if (isDocumentUncommitted(containingFileByTree)) { throw new IllegalStateException("Attempt to modify PSI for non-committed Document!"); } CommandProcessor commandProcessor = CommandProcessor.getInstance(); if (!commandProcessor.isUndoTransparentActionInProgress() && commandProcessor.getCurrentCommand() == null) { throw new IncorrectOperationException("Must not change PSI outside command or undo-transparent action. See com.intellij.openapi.command.WriteCommandAction or com.intellij.openapi.command.CommandProcessor"); } } if (containingFileByTree != null) { ((SmartPointerManagerImpl) SmartPointerManager.getInstance(myProject)).fastenBelts(containingFileByTree.getViewProvider().getVirtualFile()); if (containingFileByTree instanceof PsiFileImpl) { ((PsiFileImpl)containingFileByTree).beforeAstChange(); } } BlockSupportImpl.sendBeforeChildrenChangeEvent((PsiManagerImpl)PsiManager.getInstance(myProject), changeScope, true); Document document = containingFileByTree == null ? null : physical ? manager.getDocument(containingFileByTree) : manager.getCachedDocument(containingFileByTree); if(document != null) { synchronizer.startTransaction(myProject, document, changeScope); } } private boolean isDocumentUncommitted(@Nullable PsiFile file) { if (file == null) return false; PsiDocumentManager manager = PsiDocumentManager.getInstance(myProject); Document cachedDocument = manager.getCachedDocument(file); return cachedDocument != null && manager.isUncommited(cachedDocument); } @Nullable private static PsiFile getContainingFileByTree(@NotNull final PsiElement changeScope) { // there could be pseudo physical trees (JSPX/JSP/etc.) which must not translate // any changes to document and not to fire any PSI events final PsiFile psiFile; final ASTNode node = changeScope.getNode(); if (node == null) { psiFile = changeScope.getContainingFile(); } else { final FileElement fileElement = TreeUtil.getFileElement((TreeElement)node); // assert fileElement != null : "Can't find file element for node: " + node; // Hack. the containing tree can be invalidated if updating supplementary trees like HTML in JSP. if (fileElement == null) return null; psiFile = (PsiFile)fileElement.getPsi(); } return psiFile.getNode() != null ? psiFile : null; } private static volatile boolean allowPsiModification = true; public static <T extends Throwable> void guardPsiModificationsIn(@NotNull ThrowableRunnable<T> runnable) throws T { ApplicationManager.getApplication().assertWriteAccessAllowed(); boolean old = allowPsiModification; try { allowPsiModification = false; runnable.run(); } finally { allowPsiModification = old; } } public static boolean isAllowPsiModification() { return allowPsiModification; } }