diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java index e16c4003e485..b0ecaee69afe 100644 --- a/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/DocumentImpl.java @@ -705,17 +705,45 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { assertNotNestedModification(); myChangeInProgress = true; + DelayedExceptions exceptions = new DelayedExceptions(); try { DocumentEvent event = new DocumentEventImpl(this, offset, oldString, newString, myModificationStamp, wholeTextReplaced, initialStartOffset, initialOldLength); - beforeChangedUpdate(event); + beforeChangedUpdate(event, exceptions); myTextString = null; ImmutableCharSequence prevText = myText; myText = newText; sequence.incrementAndGet(); // increment sequence before firing events so that modification sequence on commit will match this sequence now - changedUpdate(event, newModificationStamp, prevText); + changedUpdate(event, newModificationStamp, prevText, exceptions); } finally { myChangeInProgress = false; + exceptions.rethrowPCE(); + } + } + + private class DelayedExceptions { + Throwable myException = null; + + void register(Throwable e) { + if (myException == null) { + myException = e; + } else { + myException.addSuppressed(e); + } + + if (!(e instanceof ProcessCanceledException)) { + LOG.error(e); + } + else if (myAssertThreading) { + LOG.error("ProcessCanceledException must not be thrown from document listeners for real document", new Throwable(e)); + } + } + + void rethrowPCE() { + if (myException instanceof ProcessCanceledException) { + // the case of some wise inspection modifying non-physical document during highlighting to be interrupted + throw (ProcessCanceledException)myException; + } } } @@ -724,7 +752,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { return sequence.get(); } - private void beforeChangedUpdate(DocumentEvent event) { + private void beforeChangedUpdate(DocumentEvent event, DelayedExceptions exceptions) { Application app = ApplicationManager.getApplication(); if (app != null) { FileDocumentManager manager = FileDocumentManager.getInstance(); @@ -743,11 +771,8 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { try { listeners[i].beforeDocumentChange(event); } - catch (ProcessCanceledException e) { - throw e; // the case of some wise inspection modifying non-physical document during highlighting to be interrupted - } catch (Throwable e) { - LOG.error(e); + exceptions.register(e); } } } @@ -764,7 +789,7 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { } } - private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp, @NotNull CharSequence prevText) { + private void changedUpdate(@NotNull DocumentEvent event, long newModificationStamp, @NotNull CharSequence prevText, DelayedExceptions exceptions) { try { if (LOG.isDebugEnabled()) LOG.debug(event.toString()); @@ -784,16 +809,8 @@ public class DocumentImpl extends UserDataHolderBase implements DocumentEx { try { listener.documentChanged(event); } - catch (ProcessCanceledException e) { - if (!myAssertThreading) { - throw e; - } - else { - LOG.error("ProcessCanceledException must not be thrown from document listeners for real document", new Throwable(e)); - } - } catch (Throwable e) { - LOG.error(e); + exceptions.register(e); } } } diff --git a/platform/platform-tests/testSrc/com/intellij/psi/impl/PsiDocumentManagerImplTest.java b/platform/platform-tests/testSrc/com/intellij/psi/impl/PsiDocumentManagerImplTest.java index df4718bbe141..d4e2e45f66f3 100644 --- a/platform/platform-tests/testSrc/com/intellij/psi/impl/PsiDocumentManagerImplTest.java +++ b/platform/platform-tests/testSrc/com/intellij/psi/impl/PsiDocumentManagerImplTest.java @@ -26,8 +26,10 @@ import com.intellij.openapi.application.impl.LaterInvocator; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.DocumentEx; import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; +import com.intellij.openapi.editor.ex.PrioritizedDocumentListener; import com.intellij.openapi.editor.impl.DocumentImpl; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.editor.impl.TrailingSpacesStripper; @@ -35,8 +37,10 @@ import com.intellij.openapi.editor.impl.event.DocumentEventImpl; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.fileTypes.PlainTextFileType; import com.intellij.openapi.fileTypes.PlainTextLanguage; import com.intellij.openapi.fileTypes.StdFileTypes; +import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; @@ -807,7 +811,7 @@ public class PsiDocumentManagerImplTest extends PlatformTestCase { Document document = file.getViewProvider().getDocument(); Semaphore semaphore = new Semaphore(1); - TransactionGuard.submitTransaction(getTestRootDisposable(), () -> { + TransactionGuard.submitTransaction(getTestRootDisposable(), () -> WriteCommandAction.runWriteCommandAction(myProject, () -> { document.insertString(0, "x"); @@ -817,8 +821,7 @@ public class PsiDocumentManagerImplTest extends PlatformTestCase { getPsiDocumentManager().commitAndRunReadAction(() -> semaphore.up()); } }, new ProgressWindow(false, myProject)); - }); - }); + })); int iteration = 0; while (!semaphore.waitFor(10)) { UIUtil.dispatchAllInvocationEvents(); @@ -829,6 +832,30 @@ public class PsiDocumentManagerImplTest extends PlatformTestCase { } } + public void testNoLeaksAfterPCEInListener() { + PsiFile file = PsiFileFactory.getInstance(myProject).createFileFromText("a.txt", PlainTextFileType.INSTANCE, ""); + Document document = file.getViewProvider().getDocument(); + document.addDocumentListener(new PrioritizedDocumentListener() { + @Override + public int getPriority() { + return 0; + } + + @Override + public void beforeDocumentChange(DocumentEvent event) { + throw new ProcessCanceledException(); + } + }); + try { + document.insertString(0, "a"); + fail("PCE expected"); + } + catch (ProcessCanceledException ignored) { + } + waitTenSecondsForCommit(document); + LeakHunter.checkLeak(getPsiDocumentManager(), Document.class, d -> d == document); + } + private static void assertLargeFileContentLimited(@NotNull String content, @NotNull VirtualFile vFile, @NotNull Document document) { Charset charset = EncodingManager.getInstance().getEncoding(vFile, false); float bytesPerChar = charset == null ? 2 : charset.newEncoder().averageBytesPerChar();