blob: 670aaf1d849360456854026fe86f7e7e4861e4fd [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.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.PomManager;
import com.intellij.pom.PomModel;
import com.intellij.pom.event.PomModelEvent;
import com.intellij.pom.impl.PomTransactionBase;
import com.intellij.pom.tree.TreeAspect;
import com.intellij.pom.tree.TreeAspectEvent;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiLock;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.source.PsiFileImpl;
import com.intellij.psi.impl.source.text.DiffLog;
import com.intellij.psi.impl.source.tree.FileElement;
import com.intellij.psi.text.BlockSupport;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public abstract class DocumentCommitProcessor {
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.DocumentCommitThread");
public abstract void commitSynchronously(@NotNull Document document, @NotNull Project project);
public abstract void commitAsynchronously(@NotNull final Project project, @NotNull final Document document, @NonNls @NotNull Object reason);
protected static class CommitTask {
@NotNull public final Document document;
@NotNull public final Project project;
// when queued it's not started
// when dequeued it's started
// when failed it's canceled
@NotNull public final ProgressIndicator indicator; // progress to commit this doc under.
@NotNull public final Object reason;
public boolean removed; // task marked as removed, should be ignored.
public CommitTask(@NotNull Document document,
@NotNull Project project,
@NotNull ProgressIndicator indicator,
@NotNull Object reason) {
this.document = document;
this.project = project;
this.indicator = indicator;
this.reason = reason;
}
@NonNls
@Override
public String toString() {
return "Project: " + project.getName()
+ ", Doc: "+ document +" ("+ StringUtil.first(document.getImmutableCharSequence(), 12, true).toString().replaceAll("\n", " ")+")"
+(indicator.isCanceled() ? " (Canceled)" : "") + (removed ? "Removed" : "");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CommitTask)) return false;
CommitTask task = (CommitTask)o;
return document.equals(task.document) && project.equals(task.project);
}
@Override
public int hashCode() {
int result = document.hashCode();
result = 31 * result + project.hashCode();
return result;
}
}
@Nullable("returns runnable to execute under write action in AWT to finish the commit")
public Processor<Document> doCommit(@NotNull final CommitTask task,
@NotNull final PsiFile file,
final boolean synchronously) {
Document document = task.document;
final long startDocModificationTimeStamp = document.getModificationStamp();
final FileElement myTreeElementBeingReparsedSoItWontBeCollected = ((PsiFileImpl)file).calcTreeElement();
final CharSequence chars = document.getImmutableCharSequence();
final CharSequence oldPsiText = myTreeElementBeingReparsedSoItWontBeCollected.getChars();
final TextRange changedPsiRange = getChangedPsiRange(file, oldPsiText, chars);
if (changedPsiRange == null) {
return null;
}
final Boolean data = document.getUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY);
if (data != null) {
document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null);
file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, data);
}
BlockSupport blockSupport = BlockSupport.getInstance(file.getProject());
final DiffLog diffLog = blockSupport.reparseRange(file, changedPsiRange, chars, task.indicator);
return new Processor<Document>() {
@Override
public boolean process(Document document) {
ApplicationManager.getApplication().assertWriteAccessAllowed();
log("Finishing", task, synchronously, document.getModificationStamp(), startDocModificationTimeStamp);
if (document.getModificationStamp() != startDocModificationTimeStamp ||
((PsiDocumentManagerBase)PsiDocumentManager.getInstance(file.getProject())).getCachedViewProvider(document) != file.getViewProvider()) {
return false; // optimistic locking failed
}
doActualPsiChange(file, diffLog);
assertAfterCommit(document, file, oldPsiText, myTreeElementBeingReparsedSoItWontBeCollected);
return true;
}
};
}
@Nullable
public static TextRange getChangedPsiRange(@NotNull PsiFile file, @NotNull CharSequence oldPsiText, @NotNull CharSequence newDocumentText) {
if (!file.getViewProvider().supportsIncrementalReparse(file.getLanguage())) {
return new TextRange(0, newDocumentText.length());
}
int commonPrefixLength = StringUtil.commonPrefixLength(oldPsiText, newDocumentText);
if (commonPrefixLength == newDocumentText.length() && newDocumentText.length() == oldPsiText.length()) {
return null;
}
int commonSuffixLength = StringUtil.commonSuffixLength(oldPsiText, newDocumentText);
return new TextRange(commonPrefixLength, Math.max(commonPrefixLength, oldPsiText.length() - commonSuffixLength));
}
public static void doActualPsiChange(@NotNull final PsiFile file, @NotNull final DiffLog diffLog) {
CodeStyleManager.getInstance(file.getProject()).performActionWithFormatterDisabled(new Runnable() {
@Override
public void run() {
synchronized (PsiLock.LOCK) {
file.getViewProvider().beforeContentsSynchronized();
final Document document = file.getViewProvider().getDocument();
PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(file.getProject());
PsiToDocumentSynchronizer.DocumentChangeTransaction transaction = documentManager.getSynchronizer().getTransaction(document);
final PsiFileImpl fileImpl = (PsiFileImpl)file;
if (transaction == null) {
final PomModel model = PomManager.getModel(fileImpl.getProject());
model.runTransaction(new PomTransactionBase(fileImpl, model.getModelAspect(TreeAspect.class)) {
@Override
public PomModelEvent runInner() {
return new TreeAspectEvent(model, diffLog.performActualPsiChange(file));
}
});
}
else {
diffLog.performActualPsiChange(file);
}
}
}
});
}
private void assertAfterCommit(@NotNull Document document,
@NotNull final PsiFile file,
@NotNull CharSequence oldPsiText,
@NotNull FileElement myTreeElementBeingReparsedSoItWontBeCollected) {
if (myTreeElementBeingReparsedSoItWontBeCollected.getTextLength() != document.getTextLength()) {
final String documentText = document.getText();
String fileText = file.getText();
LOG.error("commitDocument left PSI inconsistent: " + file +
"; file len=" + myTreeElementBeingReparsedSoItWontBeCollected.getTextLength() +
"; doc len=" + document.getTextLength() +
"; doc.getText() == file.getText(): " + Comparing.equal(fileText, documentText),
new Attachment("file psi text", fileText),
new Attachment("old text", documentText),
new Attachment("old psi file text", oldPsiText.toString()));
file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
try {
BlockSupport blockSupport = BlockSupport.getInstance(file.getProject());
final DiffLog diffLog = blockSupport.reparseRange(file, new TextRange(0, documentText.length()), documentText, createProgressIndicator());
doActualPsiChange(file, diffLog);
if (myTreeElementBeingReparsedSoItWontBeCollected.getTextLength() != document.getTextLength()) {
LOG.error("PSI is broken beyond repair in: " + file);
}
}
finally {
file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null);
}
}
}
public void log(@NonNls String msg, @Nullable CommitTask task, boolean synchronously, @NonNls Object... args) {
}
@NotNull
protected ProgressIndicator createProgressIndicator() {
return new EmptyProgressIndicator();
}
}