/*
* 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.impl;
import com.intellij.lang.ASTNode;
import com.intellij.lang.FileASTNode;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.*;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.StandardProgressIndicatorBase;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
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.*;
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.impl.source.tree.ForeignLeafPsiElement;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.text.BlockSupport;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.Processor;
import com.intellij.util.SmartList;
import com.intellij.util.concurrency.BoundedTaskExecutor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashSetQueue;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.jetbrains.ide.PooledThreadExecutor;
import javax.swing.*;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DocumentCommitThread implements Runnable, Disposable, DocumentCommitProcessor {
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.DocumentCommitThread");
private static final String SYNC_COMMIT_REASON = "Sync commit";
private final ExecutorService executor = new BoundedTaskExecutor("Document committing pool", PooledThreadExecutor.INSTANCE, 1, this);
private final Object lock = new Object();
private final HashSetQueue<CommitTask> documentsToCommit = new HashSetQueue<>(); // guarded by lock
private final HashSetQueue<CommitTask> documentsToApplyInEDT = new HashSetQueue<>(); // guarded by lock
private final ApplicationEx myApplication;
private volatile boolean isDisposed;
private CommitTask currentTask; // guarded by lock
private boolean myEnabled; // true if we can do commits. set to false temporarily during the write action. guarded by lock
public static DocumentCommitThread getInstance() {
return (DocumentCommitThread)ServiceManager.getService(DocumentCommitProcessor.class);
}
public DocumentCommitThread(final ApplicationEx application) {
myApplication = application;
// install listener in EDT to avoid missing events in case we are inside write action right now
application.invokeLater(() -> {
if (application.isDisposed()) return;
assert !application.isWriteAccessAllowed() || application.isUnitTestMode(); // crazy stuff happens in tests, e.g. UIUtil.dispatchInvocationEvents() inside write action
application.addApplicationListener(new ApplicationAdapter() {
@Override
public void beforeWriteActionStart(@NotNull Object action) {
disable("Write action started: " + action);
}
@Override
public void afterWriteActionFinished(@NotNull Object action) {
// crazy things happen when running tests, like starting write action in one thread but firing its end in the other
enable("Write action finished: " + action);
}
}, this);
enable("Listener installed, started");
});
}
@Override
public void dispose() {
isDisposed = true;
synchronized (lock) {
documentsToCommit.clear();
}
cancel("Stop thread");
}
private void disable(@NonNls @NotNull Object reason) {
// write action has just started, all commits are useless
synchronized (lock) {
cancel(reason);
myEnabled = false;
}
log(null, "disabled", null, reason);
}
private void enable(@NonNls @NotNull Object reason) {
synchronized (lock) {
myEnabled = true;
wakeUpQueue();
}
log(null, "enabled", null, reason);
}
// under lock
private void wakeUpQueue() {
if (!isDisposed && !documentsToCommit.isEmpty()) {
executor.execute(this);
}
}
private void cancel(@NonNls @NotNull Object reason) {
startNewTask(null, reason);
}
@Override
public void commitAsynchronously(@NotNull final Project project,
@NotNull final Document document,
@NonNls @NotNull Object reason,
@Nullable TransactionId context) {
assert !isDisposed : "already disposed";
if (!project.isInitialized()) return;
PsiFile psiFile = PsiDocumentManager.getInstance(project).getCachedPsiFile(document);
if (psiFile == null) return;
doQueue(project, document, getAllFileNodes(psiFile), reason, context,
PsiDocumentManager.getInstance(project).getLastCommittedText(document));
}
private void doQueue(@NotNull Project project,
@NotNull Document document,
@NotNull List<Pair<PsiFileImpl, FileASTNode>> oldFileNodes,
@NotNull Object reason,
@Nullable TransactionId context,
@NotNull CharSequence lastCommittedText) {
synchronized (lock) {
if (!project.isInitialized()) return; // check the project is disposed under lock.
CommitTask newTask = createNewTaskAndCancelSimilar(project, document, oldFileNodes, reason, context,
lastCommittedText);
documentsToCommit.offer(newTask);
log(project, "Queued", newTask, reason);
wakeUpQueue();
}
}
@NotNull
private CommitTask createNewTaskAndCancelSimilar(@NotNull Project project,
@NotNull Document document,
@NotNull List<Pair<PsiFileImpl, FileASTNode>> oldFileNodes,
@NotNull Object reason,
@Nullable TransactionId context,
@NotNull CharSequence lastCommittedText) {
synchronized (lock) {
for (Pair<PsiFileImpl, FileASTNode> pair : oldFileNodes) {
assert pair.first.getProject() == project;
}
CommitTask newTask = new CommitTask(project, document, oldFileNodes, createProgressIndicator(), reason, context,
lastCommittedText);
cancelAndRemoveFromDocsToCommit(newTask, reason);
cancelAndRemoveCurrentTask(newTask, reason);
cancelAndRemoveFromDocsToApplyInEDT(newTask, reason);
return newTask;
}
}
@SuppressWarnings({"NonConstantStringShouldBeStringBuffer", "StringConcatenationInLoop"})
public void log(Project project, @NonNls String msg, @Nullable CommitTask task, @NonNls Object... args) {
if (true) return;
String indent = new SimpleDateFormat("hh:mm:ss:SSSS").format(new Date()) +
(SwingUtilities.isEventDispatchThread() ? "-(EDT) " :
"-(" + Thread.currentThread()+ " ");
@NonNls
String s = indent + msg + (task == null ? " - " : "; task: " + task);
for (Object arg : args) {
if (!StringUtil.isEmpty(String.valueOf(arg))) {
s += "; "+arg;
if (arg instanceof Document) {
Document document = (Document)arg;
s+= " (\""+StringUtil.first(document.getImmutableCharSequence(), 40, true).toString().replaceAll("\n", " ")+"\")";
}
}
}
if (task != null) {
if (task.indicator.isCanceled()) {
s += "; indicator: " + task.indicator;
}
Document document = task.getDocument();
boolean stillUncommitted = !task.project.isDisposed() &&
((PsiDocumentManagerBase)PsiDocumentManager.getInstance(task.project)).isInUncommittedSet(document);
if (stillUncommitted) {
s += "; still uncommitted";
}
Set<Document> uncommitted = project == null || project.isDisposed() ? Collections.emptySet() :
((PsiDocumentManagerBase)PsiDocumentManager.getInstance(project)).myUncommittedDocuments;
if (!uncommitted.isEmpty()) {
s+= "; uncommitted: "+uncommitted;
}
}
synchronized (lock) {
int size = documentsToCommit.size();
if (size != 0) {
s += " (" + size + " documents still in queue: ";
int i = 0;
for (CommitTask commitTask : documentsToCommit) {
s += commitTask + "; ";
if (++i > 4) {
s += " ... ";
break;
}
}
s += ")";
}
}
LOG.debug(s);
}
// cancels all pending commits
@TestOnly // under lock
private void cancelAll() {
String reason = "Cancel all in tests";
cancel(reason);
for (CommitTask commitTask : documentsToCommit) {
commitTask.cancel(reason, this);
log(commitTask.project, "Removed from background queue", commitTask);
}
documentsToCommit.clear();
for (CommitTask commitTask : documentsToApplyInEDT) {
commitTask.cancel(reason, this);
log(commitTask.project, "Removed from EDT apply queue (sync commit called)", commitTask);
}
documentsToApplyInEDT.clear();
CommitTask task = currentTask;
if (task != null) {
cancelAndRemoveFromDocsToCommit(task, reason);
}
cancel("Sync commit intervened");
((BoundedTaskExecutor)executor).clearAndCancelAll();
}
@TestOnly
public void clearQueue() {
synchronized (lock) {
cancelAll();
wakeUpQueue();
}
}
private void cancelAndRemoveCurrentTask(@NotNull CommitTask newTask, @NotNull Object reason) {
CommitTask currentTask = this.currentTask;
if (currentTask != null && currentTask.equals(newTask)) {
cancelAndRemoveFromDocsToCommit(currentTask, reason);
cancel(reason);
}
}
private void cancelAndRemoveFromDocsToApplyInEDT(@NotNull CommitTask newTask, @NotNull Object reason) {
boolean removed = cancelAndRemoveFromQueue(newTask, documentsToApplyInEDT, reason);
if (removed) {
log(newTask.project, "Removed from EDT apply queue", newTask);
}
}
private void cancelAndRemoveFromDocsToCommit(@NotNull final CommitTask newTask, @NotNull Object reason) {
boolean removed = cancelAndRemoveFromQueue(newTask, documentsToCommit, reason);
if (removed) {
log(newTask.project, "Removed from background queue", newTask);
}
}
private boolean cancelAndRemoveFromQueue(@NotNull CommitTask newTask, @NotNull HashSetQueue<CommitTask> queue, @NotNull Object reason) {
CommitTask queuedTask = queue.find(newTask);
if (queuedTask != null) {
assert queuedTask != newTask;
queuedTask.cancel(reason, this);
}
return queue.remove(newTask);
}
@Override
public void run() {
while (!isDisposed) {
try {
boolean polled = pollQueue();
if (!polled) break;
}
catch(Throwable e) {
LOG.error(e);
}
}
}
// returns true if queue changed
private boolean pollQueue() {
assert !myApplication.isDispatchThread() : Thread.currentThread();
boolean success = false;
Document document = null;
Project project = null;
CommitTask task = null;
Object failureReason = null;
try {
ProgressIndicator indicator;
synchronized (lock) {
if (!myEnabled || (task = documentsToCommit.poll()) == null) {
return false;
}
document = task.getDocument();
indicator = task.indicator;
project = task.project;
if (project.isDisposed() || !((PsiDocumentManagerBase)PsiDocumentManager.getInstance(project)).isInUncommittedSet(document)) {
log(project, "Abandon and proceed to next", task);
return true;
}
if (indicator.isCanceled()) {
return true; // document has been marked as removed, e.g. by synchronous commit
}
startNewTask(task, "Pulled new task");
// transfer to documentsToApplyInEDT
documentsToApplyInEDT.add(task);
}
if (indicator.isCanceled()) {
success = false;
}
else {
final CommitTask commitTask = task;
final Ref<Pair<Runnable, Object>> result = new Ref<>();
ProgressManager.getInstance().executeProcessUnderProgress(() -> result.set(commitUnderProgress(commitTask, false)), indicator);
final Runnable finishRunnable = result.get().first;
success = finishRunnable != null;
failureReason = result.get().second;
if (success) {
assert !myApplication.isDispatchThread();
TransactionGuardImpl guard = (TransactionGuardImpl)TransactionGuard.getInstance();
guard.submitTransaction(project, task.myCreationContext, finishRunnable);
}
}
}
catch (ProcessCanceledException e) {
cancel(e + " (cancel reason: "+((UserDataHolder)task.indicator).getUserData(CANCEL_REASON)+")"); // leave queue unchanged
success = false;
failureReason = e;
}
catch (Throwable e) {
cancel(e);
failureReason = ExceptionUtil.getThrowableText(e);
}
if (!success && task != null) {
final Project finalProject = project;
final Document finalDocument = document;
Object finalFailureReason = failureReason;
CommitTask finalTask = task;
ReadAction.run(() -> {
if (finalProject.isDisposed()) return;
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(finalProject);
if (documentManager.isCommitted(finalDocument)) return; // sync commit hasn't intervened
CharSequence lastCommittedText = documentManager.getLastCommittedText(finalDocument);
PsiFile file = documentManager.getPsiFile(finalDocument);
List<Pair<PsiFileImpl, FileASTNode>> oldFileNodes = file == null ? null : getAllFileNodes(file);
if (oldFileNodes != null) {
doQueue(finalProject, finalDocument, oldFileNodes, "re-added on failure: " + finalFailureReason,
finalTask.myCreationContext,
lastCommittedText);
}
});
}
synchronized (lock) {
currentTask = null; // do not cancel, it's being invokeLatered
}
return true;
}
@Override
public void commitSynchronously(@NotNull Document document, @NotNull Project project, @NotNull PsiFile psiFile) {
assert !isDisposed;
if (!project.isInitialized() && !project.isDefault()) {
@NonNls String s = project + "; Disposed: "+project.isDisposed()+"; Open: "+project.isOpen();
try {
Disposer.dispose(project);
}
catch (Throwable ignored) {
// do not fill log with endless exceptions
}
throw new RuntimeException(s);
}
List<Pair<PsiFileImpl, FileASTNode>> allFileNodes = getAllFileNodes(psiFile);
Lock documentLock = getDocumentLock(document);
CommitTask task;
synchronized (lock) {
// synchronized to ensure no new similar tasks can start before we hold the document's lock
task = createNewTaskAndCancelSimilar(project, document, allFileNodes, SYNC_COMMIT_REASON, TransactionGuard.getInstance().getContextTransaction(),
PsiDocumentManager.getInstance(project).getLastCommittedText(document));
documentLock.lock();
}
try {
assert !task.indicator.isCanceled();
Pair<Runnable, Object> result = commitUnderProgress(task, true);
Runnable finish = result.first;
log(project, "Committed sync", task, finish, task.indicator);
assert finish != null;
finish.run();
}
finally {
documentLock.unlock();
}
// will wake itself up on write action end
}
@NotNull
private static List<Pair<PsiFileImpl, FileASTNode>> getAllFileNodes(@NotNull PsiFile file) {
if (!file.isValid()) {
throw new PsiInvalidElementAccessException(file, "File " + file + " is invalid, can't commit");
}
if (file instanceof PsiCompiledFile) {
throw new IllegalArgumentException("Can't commit ClsFile: "+file);
}
return ContainerUtil.map(file.getViewProvider().getAllFiles(), root -> Pair.create((PsiFileImpl)root, root.getNode()));
}
@NotNull
protected ProgressIndicator createProgressIndicator() {
return new StandardProgressIndicatorBase();
}
private void startNewTask(@Nullable CommitTask task, @NotNull Object reason) {
synchronized (lock) { // sync to prevent overwriting
CommitTask cur = currentTask;
if (cur != null) {
cur.cancel(reason, this);
}
currentTask = task;
}
}
// returns (finish commit Runnable (to be invoked later in EDT), null) on success or (null, failure reason) on failure
@NotNull
private Pair<Runnable, Object> commitUnderProgress(@NotNull final CommitTask task, final boolean synchronously) {
if (synchronously) {
assert !task.indicator.isCanceled();
}
final Document document = task.getDocument();
final Project project = task.project;
final PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project);
final List<Processor<Document>> finishProcessors = new SmartList<>();
Runnable runnable = () -> {
myApplication.assertReadAccessAllowed();
if (project.isDisposed()) return;
Lock lock = getDocumentLock(document);
if (!lock.tryLock()) {
task.cancel("Can't obtain document lock", this);
return;
}
boolean canceled = false;
try {
if (documentManager.isCommitted(document)) return;
if (!task.isStillValid()) {
canceled = true;
return;
}
FileViewProvider viewProvider = documentManager.getCachedViewProvider(document);
if (viewProvider == null) {
finishProcessors.add(handleCommitWithoutPsi(documentManager, task));
return;
}
for (Pair<PsiFileImpl, FileASTNode> pair : task.myOldFileNodes) {
PsiFileImpl file = pair.first;
if (file.isValid()) {
FileASTNode oldFileNode = pair.second;
Processor<Document> finishProcessor = doCommit(task, file, oldFileNode);
if (finishProcessor != null) {
finishProcessors.add(finishProcessor);
}
}
else {
// file became invalid while sitting in the queue
if (task.reason.equals(SYNC_COMMIT_REASON)) {
throw new PsiInvalidElementAccessException(file, "File " + file + " invalidated during sync commit");
}
commitAsynchronously(project, document, "File " + file + " invalidated during background commit; task: "+task,
task.myCreationContext);
}
}
}
finally {
lock.unlock();
if (canceled) {
task.cancel("Task invalidated", this);
}
}
};
if (synchronously) {
runnable.run();
}
else if (!myApplication.tryRunReadAction(runnable)) {
log(project, "Could not start read action", task, myApplication.isReadAccessAllowed(), Thread.currentThread());
return new Pair<>(null, "Could not start read action");
}
boolean canceled = task.indicator.isCanceled();
assert !synchronously || !canceled;
if (canceled) {
return new Pair<>(null, "Indicator was canceled");
}
Runnable result = createEdtRunnable(task, synchronously, finishProcessors);
return Pair.create(result, null);
}
@NotNull
private Runnable createEdtRunnable(@NotNull final CommitTask task,
final boolean synchronously,
@NotNull final List<Processor<Document>> finishProcessors) {
return () -> {
myApplication.assertIsDispatchThread();
Document document = task.getDocument();
Project project = task.project;
PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project);
boolean committed = project.isDisposed() || documentManager.isCommitted(document);
synchronized (lock) {
documentsToApplyInEDT.remove(task);
if (committed) {
log(project, "Marked as already committed in EDT apply queue, return", task);
return;
}
}
boolean changeStillValid = task.isStillValid();
boolean success = changeStillValid && documentManager.finishCommit(document, finishProcessors, synchronously, task.reason);
if (synchronously) {
assert success;
}
if (!changeStillValid) {
log(project, "document changed; ignore", task);
return;
}
if (synchronously || success) {
assert !documentManager.isInUncommittedSet(document);
}
if (success) {
log(project, "Commit finished", task);
}
else {
// add document back to the queue
commitAsynchronously(project, document, "Re-added back", task.myCreationContext);
}
};
}
@NotNull
private Processor<Document> handleCommitWithoutPsi(@NotNull final PsiDocumentManagerBase documentManager,
@NotNull final CommitTask task) {
return document -> {
log(task.project, "Finishing without PSI", task);
if (!task.isStillValid() || documentManager.getCachedViewProvider(document) != null) {
return false;
}
documentManager.handleCommitWithoutPsi(document);
return true;
};
}
boolean isEnabled() {
synchronized (lock) {
return myEnabled;
}
}
@Override
public String toString() {
return "Document commit thread; application: "+myApplication+"; isDisposed: "+isDisposed+"; myEnabled: "+isEnabled();
}
@TestOnly
public void waitForAllCommits() throws ExecutionException, InterruptedException, TimeoutException {
ApplicationManager.getApplication().assertIsDispatchThread();
assert !ApplicationManager.getApplication().isWriteAccessAllowed();
((BoundedTaskExecutor)executor).waitAllTasksExecuted(100, TimeUnit.SECONDS);
UIUtil.dispatchAllInvocationEvents();
}
private static final Key<Object> CANCEL_REASON = Key.create("CANCEL_REASON");
private class CommitTask {
@NotNull private final Document document;
@NotNull final Project project;
private final int modificationSequence; // store initial document modification sequence here to check if it changed later before commit in EDT
// when queued it's not started
// when dequeued it's started
// when failed it's canceled
@NotNull final ProgressIndicator indicator; // progress to commit this doc under.
@NotNull final Object reason;
@Nullable final TransactionId myCreationContext;
private final CharSequence myLastCommittedText;
@NotNull final List<Pair<PsiFileImpl, FileASTNode>> myOldFileNodes;
CommitTask(@NotNull final Project project,
@NotNull final Document document,
@NotNull final List<Pair<PsiFileImpl, FileASTNode>> oldFileNodes,
@NotNull ProgressIndicator indicator,
@NotNull Object reason,
@Nullable TransactionId context,
@NotNull CharSequence lastCommittedText) {
this.document = document;
this.project = project;
this.indicator = indicator;
this.reason = reason;
myCreationContext = context;
myLastCommittedText = lastCommittedText;
myOldFileNodes = oldFileNodes;
modificationSequence = ((DocumentEx)document).getModificationSequence();
}
@NonNls
@Override
public String toString() {
Document document = getDocument();
String docInfo = document + " (\"" + StringUtil.first(document.getImmutableCharSequence(), 40, true).toString().replaceAll("\n", " ") + "\")";
String indicatorInfo = indicator.isCanceled() ? " (Canceled: " + ((UserDataHolder)indicator).getUserData(CANCEL_REASON) + ")" : "";
String reasonInfo = " Reason: " + reason + (isStillValid() ? ""
: "; changed: old seq=" + modificationSequence + ", new seq=" +
((DocumentEx)document).getModificationSequence());
return "Doc: " + docInfo + indicatorInfo + reasonInfo;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CommitTask)) return false;
CommitTask task = (CommitTask)o;
return Comparing.equal(getDocument(),task.getDocument()) && project.equals(task.project);
}
@Override
public int hashCode() {
int result = getDocument().hashCode();
result = 31 * result + project.hashCode();
return result;
}
boolean isStillValid() {
Document document = getDocument();
return ((DocumentEx)document).getModificationSequence() == modificationSequence;
}
private void cancel(@NotNull Object reason, @NotNull DocumentCommitThread commitProcessor) {
if (!indicator.isCanceled()) {
commitProcessor.log(project, "indicator cancel", this);
indicator.cancel();
((UserDataHolder)indicator).putUserData(CANCEL_REASON, reason);
synchronized (lock) {
documentsToCommit.remove(this);
documentsToApplyInEDT.remove(this);
}
}
}
@NotNull
Document getDocument() {
return document;
}
}
// public for Upsource
@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,
@NotNull final FileASTNode oldFileNode) {
Document document = task.getDocument();
final CharSequence newDocumentText = document.getImmutableCharSequence();
final TextRange changedPsiRange = getChangedPsiRange(file, task.myLastCommittedText, newDocumentText);
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, oldFileNode, changedPsiRange, newDocumentText, task.indicator, task.myLastCommittedText);
return document1 -> {
FileViewProvider viewProvider = file.getViewProvider();
if (!task.isStillValid() ||
((PsiDocumentManagerBase)PsiDocumentManager.getInstance(file.getProject())).getCachedViewProvider(document1) != viewProvider) {
return false; // optimistic locking failed
}
if (file.isPhysical() && !ApplicationManager.getApplication().isWriteAccessAllowed()) {
VirtualFile vFile = viewProvider.getVirtualFile();
LOG.error("Write action expected" +
"; document=" + document1 +
"; file=" + file + " of " + file.getClass() +
"; file.valid=" + file.isValid() +
"; file.eventSystemEnabled=" + viewProvider.isEventSystemEnabled() +
"; viewProvider=" + viewProvider + " of " + viewProvider.getClass() +
"; language=" + file.getLanguage() +
"; vFile=" + vFile + " of " + vFile.getClass() +
"; free-threaded=" + SingleRootFileViewProvider.isFreeThreaded(viewProvider));
}
doActualPsiChange(file, diffLog);
assertAfterCommit(document1, file, (FileElement)oldFileNode);
return true;
};
}
private static int getLeafMatchingLength(CharSequence leafText, CharSequence pattern, int patternIndex, int finalPatternIndex, int direction) {
int leafIndex = direction == 1 ? 0 : leafText.length() - 1;
int finalLeafIndex = direction == 1 ? leafText.length() - 1 : 0;
int result = 0;
while (leafText.charAt(leafIndex) == pattern.charAt(patternIndex)) {
result++;
if (leafIndex == finalLeafIndex || patternIndex == finalPatternIndex) {
break;
}
leafIndex += direction;
patternIndex += direction;
}
return result;
}
private static int getMatchingLength(@NotNull FileElement treeElement, @NotNull CharSequence text, boolean fromStart) {
int patternIndex = fromStart ? 0 : text.length() - 1;
int finalPatternIndex = fromStart ? text.length() - 1 : 0;
int direction = fromStart ? 1 : -1;
ASTNode leaf = fromStart ? TreeUtil.findFirstLeaf(treeElement, false) : TreeUtil.findLastLeaf(treeElement, false);
int result = 0;
while (leaf != null && (fromStart ? patternIndex <= finalPatternIndex : patternIndex >= finalPatternIndex)) {
if (!(leaf instanceof ForeignLeafPsiElement)) {
CharSequence chars = leaf.getChars();
if (chars.length() > 0) {
int matchingLength = getLeafMatchingLength(chars, text, patternIndex, finalPatternIndex, direction);
result += matchingLength;
if (matchingLength != chars.length()) {
break;
}
patternIndex += fromStart ? matchingLength : -matchingLength;
}
}
leaf = fromStart ? TreeUtil.nextLeaf(leaf, false) : TreeUtil.prevLeaf(leaf, false);
}
return result;
}
@Nullable
public static TextRange getChangedPsiRange(@NotNull PsiFile file, @NotNull FileElement treeElement, @NotNull CharSequence newDocumentText) {
int psiLength = treeElement.getTextLength();
if (!file.getViewProvider().supportsIncrementalReparse(file.getLanguage())) {
return new TextRange(0, psiLength);
}
int commonPrefixLength = getMatchingLength(treeElement, newDocumentText, true);
if (commonPrefixLength == newDocumentText.length() && newDocumentText.length() == psiLength) {
return null;
}
int commonSuffixLength = Math.min(getMatchingLength(treeElement, newDocumentText, false), psiLength - commonPrefixLength);
return new TextRange(commonPrefixLength, psiLength - commonSuffixLength);
}
@Nullable
private static TextRange getChangedPsiRange(@NotNull PsiFile file,
@NotNull CharSequence oldDocumentText,
@NotNull CharSequence newDocumentText) {
int psiLength = oldDocumentText.length();
if (!file.getViewProvider().supportsIncrementalReparse(file.getLanguage())) {
return new TextRange(0, psiLength);
}
int commonPrefixLength = StringUtil.commonPrefixLength(oldDocumentText, newDocumentText);
if (commonPrefixLength == newDocumentText.length() && newDocumentText.length() == psiLength) {
return null;
}
int commonSuffixLength = Math.min(StringUtil.commonSuffixLength(oldDocumentText, newDocumentText), psiLength - commonPrefixLength);
return new TextRange(commonPrefixLength, psiLength - commonSuffixLength);
}
public static void doActualPsiChange(@NotNull final PsiFile file, @NotNull final DiffLog diffLog) {
CodeStyleManager.getInstance(file.getProject()).performActionWithFormatterDisabled((Runnable)() -> {
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 FileElement oldFileNode) {
if (oldFileNode.getTextLength() != document.getTextLength()) {
final String documentText = document.getText();
String fileText = file.getText();
boolean sameText = Comparing.equal(fileText, documentText);
LOG.error("commitDocument() left PSI inconsistent: " + DebugUtil.diagnosePsiDocumentInconsistency(file, document) +
"; node.length=" + oldFileNode.getTextLength() +
"; doc.text" + (sameText ? "==" : "!=") + "file.text" +
"; file name:" + file.getName()+
"; type:"+file.getFileType()+
"; lang:"+file.getLanguage()
);
file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
try {
BlockSupport blockSupport = BlockSupport.getInstance(file.getProject());
final DiffLog diffLog = blockSupport.reparseRange(file, file.getNode(), new TextRange(0, documentText.length()), documentText, createProgressIndicator(),
oldFileNode.getText());
doActualPsiChange(file, diffLog);
if (oldFileNode.getTextLength() != document.getTextLength()) {
LOG.error("PSI is broken beyond repair in: " + file);
}
}
finally {
file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null);
}
}
}
/**
* @return an internal lock object to prevent read & write phases of commit from running simultaneously for free-threaded PSI
*/
private static Lock getDocumentLock(Document document) {
Lock lock = document.getUserData(DOCUMENT_LOCK);
return lock != null ? lock : ((UserDataHolderEx)document).putUserDataIfAbsent(DOCUMENT_LOCK, new ReentrantLock());
}
private static final Key<Lock> DOCUMENT_LOCK = Key.create("DOCUMENT_LOCK");
void cancelTasksOnProjectDispose(@NotNull final Project project) {
synchronized (lock) {
cancelTasksOnProjectDispose(project, documentsToCommit);
cancelTasksOnProjectDispose(project, documentsToApplyInEDT);
}
}
private void cancelTasksOnProjectDispose(@NotNull Project project, @NotNull HashSetQueue<CommitTask> queue) {
for (HashSetQueue.PositionalIterator<CommitTask> iterator = queue.iterator(); iterator.hasNext(); ) {
CommitTask commitTask = iterator.next();
if (commitTask.project == project) {
iterator.remove();
commitTask.cancel("project is disposed", this);
}
}
}
}