diff --git a/platform/core-api/src/com/intellij/psi/tree/IReparseableLeafElementType.java b/platform/core-api/src/com/intellij/psi/tree/IReparseableLeafElementType.java new file mode 100644 index 000000000000..8b2051a061ab --- /dev/null +++ b/platform/core-api/src/com/intellij/psi/tree/IReparseableLeafElementType.java @@ -0,0 +1,42 @@ +/* + * 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.psi.tree; + +import com.intellij.lang.ASTNode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An interface to be implemented by {@link IElementType} instances for leaf AST nodes, providing a possibility for quick reparse. + * @see #reparseLeaf(ASTNode, CharSequence) + * @since 2016.2 + * @author peter + */ +public interface IReparseableLeafElementType { + + /** + * If the IDE detects that the whole changed range to be reparsed is contained within one leaf that has such element type, + * this method is called. It should analyze the provided new leaf text, check whether this text conforms with the language rules, + * and (if yes) return a new leaf of the same type with the new text. The reparse process would then just replace the old leaf with + * the new one. Otherwise, null should be returned, and a full-blown reparse process will happen instead. + * @param leaf the leaf element where the text change has occurred + * @param newText the updated text inside the leaf's range + * @return a replacement leaf with newText text, if it's correct to do such replacement, or null otherwise + */ + @Nullable + T reparseLeaf(@NotNull T leaf, @NotNull CharSequence newText); + +} diff --git a/platform/core-impl/src/com/intellij/pom/core/impl/PomModelImpl.java b/platform/core-impl/src/com/intellij/pom/core/impl/PomModelImpl.java index b596ff57703c..d6a969f20d85 100644 --- a/platform/core-impl/src/com/intellij/pom/core/impl/PomModelImpl.java +++ b/platform/core-impl/src/com/intellij/pom/core/impl/PomModelImpl.java @@ -46,9 +46,12 @@ 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.util.IncorrectOperationException; import com.intellij.util.ThrowableRunnable; @@ -253,7 +256,7 @@ public class PomModelImpl extends UserDataHolderBase implements PomModel { boolean isFromCommit = ApplicationManager.getApplication().isDispatchThread() && ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject)).isCommitInProgress(); if (!isFromCommit && !synchronizer.isIgnorePsiEvents()) { - reparseParallelTrees(containingFileByTree); + reparseParallelTrees(containingFileByTree, synchronizer); if (docSynced) { containingFileByTree.getViewProvider().contentsSynchronized(); } @@ -263,7 +266,7 @@ public class PomModelImpl extends UserDataHolderBase implements PomModel { if (progressIndicator != null) progressIndicator.finishNonCancelableSection(); } - private void reparseParallelTrees(PsiFile changedFile) { + private void reparseParallelTrees(PsiFile changedFile, PsiToDocumentSynchronizer synchronizer) { List allFiles = changedFile.getViewProvider().getAllFiles(); if (allFiles.size() <= 1) { return; @@ -271,40 +274,70 @@ public class PomModelImpl extends UserDataHolderBase implements PomModel { CharSequence newText = changedFile.getNode().getChars(); for (final PsiFile file : allFiles) { - if (file != changedFile) { - FileElement fileElement = ((PsiFileImpl)file).getTreeElement(); - if (fileElement != null) { - reparseFile(file, fileElement, newText); - } + 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); } } } - private void reparseFile(@NotNull final PsiFile file, @NotNull FileElement treeElement, @NotNull CharSequence newText) { - PsiToDocumentSynchronizer synchronizer =((PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject)).getSynchronizer(); + @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; + 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()); - synchronizer.setIgnorePsiEvents(true); - try { - CodeStyleManager.getInstance(file.getProject()).performActionWithFormatterDisabled(new Runnable() { - @Override - public void run() { - runTransaction(new PomTransactionBase(file, getModelAspect(TreeAspect.class)) { - @Nullable - @Override - public PomModelEvent runInner() throws IncorrectOperationException { - return new TreeAspectEvent(PomModelImpl.this, log.performActualPsiChange(file)); - } - }); - } - }); - } - finally { - synchronizer.setIgnorePsiEvents(false); + return new Runnable() { + @Override + public void run() { + 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 : new Runnable() { + @Override + public void run() { + 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) { @@ -313,7 +346,6 @@ public class PomModelImpl extends UserDataHolderBase implements PomModel { final PsiDocumentManagerBase manager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(myProject); final PsiToDocumentSynchronizer synchronizer = manager.getSynchronizer(); final PsiElement changeScope = transaction.getChangeScope(); - LOG.assertTrue(changeScope != null); final PsiFile containingFileByTree = getContainingFileByTree(changeScope); boolean physical = changeScope.isPhysical();