EA-326396 diff: do not start a new command inside document listener

UndoManager fails if command is started with Document bulk mode enabled.
This might have happened if "Original" document is "forUseInNonAWTThread == true".
For example: for "Compare with Clipboard" action on ConsoleViewImpl and later called "Clear All" action.

Copy constraints flags from the original document to the fragment document,
and do start our own command.

GitOrigin-RevId: d6b317f024178670a2243c34899dbf34d7d5fb90
This commit is contained in:
Aleksey Pivovarov
2023-02-01 17:39:13 +01:00
committed by intellij-monorepo-bot
parent 14b4b6fe01
commit f108439628
3 changed files with 72 additions and 38 deletions

View File

@@ -5,6 +5,8 @@ import com.intellij.diff.contents.DiffContentBase;
import com.intellij.diff.contents.DocumentContent;
import com.intellij.diff.util.DiffUserDataKeysEx;
import com.intellij.diff.util.LineCol;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diff.DiffBundle;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.RangeMarker;
@@ -14,6 +16,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -142,18 +145,38 @@ public final class DocumentFragmentContent extends DiffContentBase implements Do
@Override
public void startListen() {
if (myRangeMarker.isValid()) {
myDocument2.setReadOnly(false);
CharSequence nexText = myDocument1.getCharsSequence().subSequence(myRangeMarker.getStartOffset(), myRangeMarker.getEndOffset());
replaceString(myDocument2, 0, myDocument2.getTextLength(), nexText);
myDocument2.setReadOnly(!myDocument1.isWritable());
}
else {
myDocument2.setReadOnly(false);
replaceString(myDocument2, 0, myDocument2.getTextLength(), DiffBundle.message("synchronize.document.and.its.fragment.range.error"));
myDocument2.setReadOnly(true);
}
// no need to set myDuringModification - listeners are not added yet
CommandProcessor.getInstance().runUndoTransparentAction(() -> {
ApplicationManager.getApplication().runWriteAction(() -> {
if (myRangeMarker.isValid()) {
myDocument2.setReadOnly(false);
CharSequence nexText = myRangeMarker.getTextRange().subSequence(myDocument1.getCharsSequence());
myDocument2.setText(nexText);
myDocument2.setReadOnly(!myDocument1.isWritable());
}
else {
myDocument2.setReadOnly(false);
myDocument2.setText(DiffBundle.message("synchronize.document.and.its.fragment.range.error"));
myDocument2.setReadOnly(true);
}
});
});
super.startListen();
}
@RequiresEdt
private void replaceString(@NotNull Document document,
int startOffset,
int endOffset,
@NotNull CharSequence newText) {
try {
myDuringModification = true;
document.replaceString(startOffset, endOffset, newText);
}
finally {
myDuringModification = false;
}
}
}
}

View File

@@ -1,14 +1,12 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.diff.actions;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.diff.DiffBundle;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.editor.impl.EditorFactoryImpl;
import com.intellij.openapi.project.Project;
import com.intellij.util.concurrency.annotations.RequiresEdt;
@@ -20,7 +18,7 @@ import java.beans.PropertyChangeListener;
public abstract class DocumentsSynchronizer {
@NotNull protected final Document myDocument1;
@NotNull protected final Document myDocument2;
@Nullable private final Project myProject;
@Nullable protected final Project myProject;
protected boolean myDuringModification = false;
@@ -64,25 +62,6 @@ public abstract class DocumentsSynchronizer {
protected abstract void onDocumentChanged2(@NotNull DocumentEvent event);
@RequiresEdt
protected void replaceString(@NotNull final Document document,
final int startOffset,
final int endOffset,
@NotNull final CharSequence newText) {
try {
myDuringModification = true;
CommandProcessor.getInstance().executeCommand(
myProject,
() -> ApplicationManager.getApplication().runWriteAction(() -> document.replaceString(startOffset, endOffset, newText)),
DiffBundle.message("synchronize.document.and.its.fragment"),
document
);
}
finally {
myDuringModification = false;
}
}
public void startListen() {
myDocument1.addDocumentListener(myListener1);
myDocument2.addDocumentListener(myListener2);
@@ -97,7 +76,9 @@ public abstract class DocumentsSynchronizer {
public static @NotNull Document createFakeDocument(@NotNull Document original) {
EditorFactoryImpl editorFactory = (EditorFactoryImpl)EditorFactory.getInstance();
Document document = editorFactory.createDocument("", true, false);
boolean acceptsSlashR = original instanceof DocumentImpl && ((DocumentImpl)original).acceptsSlashR();
boolean writeThreadOnly = original instanceof DocumentImpl && ((DocumentImpl)original).isWriteThreadOnly();
Document document = editorFactory.createDocument("", acceptsSlashR, !writeThreadOnly);
document.putUserData(UndoManager.ORIGINAL_DOCUMENT, original);
return document;
}

View File

@@ -6,8 +6,14 @@ import com.intellij.diff.actions.DocumentsSynchronizer
import com.intellij.diff.contents.DiffContentBase
import com.intellij.diff.contents.DocumentContent
import com.intellij.diff.util.DiffUserDataKeysEx
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.diff.DiffBundle
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.impl.EditorFactoryImpl
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.openapi.project.Project
@@ -16,6 +22,7 @@ import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.refactoring.suggested.startOffset
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.util.function.IntUnaryOperator
class TestDiffContent(
@@ -28,12 +35,14 @@ class TestDiffContent(
override fun getContentType(): FileType = FileTypes.PLAIN_TEXT
private val fakeDocument = DocumentsSynchronizer.createFakeDocument(original.document)
private val fakeDocument = (EditorFactory.getInstance() as EditorFactoryImpl).createDocument("", true, false).apply {
putUserData(UndoManager.ORIGINAL_DOCUMENT, original.document)
}
private val synchronizer: DocumentsSynchronizer = object : DocumentsSynchronizer(project, original.document, fakeDocument) {
override fun onDocumentChanged1(event: DocumentEvent) {
PsiDocumentManager.getInstance(project).performForCommittedDocument(document1, Runnable {
val element = elemPtr.element ?: return@Runnable
replaceString(myDocument2, 0, myDocument2.textLength, ElementManipulators.getValueText(element))
replaceString(myDocument2, ElementManipulators.getValueText(element))
})
}
@@ -50,9 +59,30 @@ class TestDiffContent(
}
override fun startListen() {
replaceString(myDocument2, 0, myDocument2.textLength, text)
replaceString(myDocument2, text)
super.startListen()
}
@RequiresEdt
private fun replaceString(document: Document, newText: CharSequence) {
try {
myDuringModification = true
CommandProcessor.getInstance().executeCommand(
myProject,
{
ApplicationManager.getApplication().runWriteAction {
document.replaceString(0, document.textLength, newText)
}
},
DiffBundle.message("synchronize.document.and.its.fragment"),
document
)
}
finally {
myDuringModification = false
}
}
}
private var assignments = 0