[threading] IJPL-184084: Do not require read lock for interacting with editor models

GitOrigin-RevId: e90351dd339f4436a66debf6c84060a3b568fbb2
This commit is contained in:
Konstantin Nisht
2025-05-15 09:59:45 +00:00
committed by intellij-monorepo-bot
parent 009d4c4ded
commit d91afee6e2
9 changed files with 246 additions and 60 deletions
@@ -172,6 +172,11 @@ f:com.intellij.openapi.editor.impl.EditorImpl
- java.awt.event.FocusListener
- *:addHighlightingPredicate(com.intellij.openapi.util.Key,com.intellij.openapi.editor.impl.EditorHighlightingPredicate):com.intellij.openapi.editor.impl.EditorHighlightingPredicate
- *:removeHighlightingPredicate(com.intellij.openapi.util.Key):com.intellij.openapi.editor.impl.EditorHighlightingPredicate
*f:com.intellij.openapi.editor.impl.EditorThreading
- sf:INSTANCE:com.intellij.openapi.editor.impl.EditorThreading
- sf:assertInteractionAllowed():V
- sf:compute(com.intellij.openapi.util.ThrowableComputable):java.lang.Object
- sf:run(java.lang.Runnable):V
*f:com.intellij.openapi.editor.impl.event.EditorGutterHoverEvent
- java.util.EventObject
- <init>(com.intellij.openapi.editor.ex.EditorGutterComponentEx,com.intellij.openapi.editor.markup.GutterIconRenderer):V
@@ -658,7 +658,7 @@ public final class CaretImpl extends UserDataHolderBase implements Caret, Dumpab
@Override
public int getOffset() {
assertNotUpdating();
ApplicationManager.getApplication().assertReadAccessAllowed();
EditorThreading.assertInteractionAllowed();
while (true) {
PositionMarker marker = myPositionMarker;
if (marker == null) return 0; // caret was disposed
@@ -910,7 +910,7 @@ public final class CaretImpl extends UserDataHolderBase implements Caret, Dumpab
@Override
public int getSelectionStart() {
ApplicationManager.getApplication().assertReadAccessAllowed();
EditorThreading.assertInteractionAllowed();
if (hasSelection()) {
RangeMarker marker = mySelectionMarker;
if (marker != null) {
@@ -963,7 +963,7 @@ public final class CaretImpl extends UserDataHolderBase implements Caret, Dumpab
@Override
public int getSelectionEnd() {
ApplicationManager.getApplication().assertReadAccessAllowed();
EditorThreading.assertInteractionAllowed();
if (hasSelection()) {
RangeMarker marker = mySelectionMarker;
if (marker != null) {
@@ -1016,7 +1016,7 @@ public final class CaretImpl extends UserDataHolderBase implements Caret, Dumpab
@Override
public boolean hasSelection() {
ApplicationManager.getApplication().assertReadAccessAllowed();
EditorThreading.assertInteractionAllowed();
SelectionMarker marker = mySelectionMarker;
return hasSelection(marker);
}
@@ -1028,7 +1028,7 @@ public final class CaretImpl extends UserDataHolderBase implements Caret, Dumpab
@Override
public @NotNull TextRange getSelectionRange() {
ApplicationManager.getApplication().assertReadAccessAllowed();
EditorThreading.assertInteractionAllowed();
SelectionMarker marker = mySelectionMarker;
if (hasSelection(marker)) {
return marker.getTextRange();
@@ -1207,7 +1207,7 @@ public final class CaretImpl extends UserDataHolderBase implements Caret, Dumpab
@Override
public int getLeadSelectionOffset() {
ApplicationManager.getApplication().assertReadAccessAllowed();
EditorThreading.assertInteractionAllowed();
int caretOffset = getOffset();
if (hasSelection()) {
RangeMarker marker = mySelectionMarker;
@@ -1335,7 +1335,7 @@ public final class CaretImpl extends UserDataHolderBase implements Caret, Dumpab
}
boolean hasVirtualSelection() {
ApplicationManager.getApplication().assertReadAccessAllowed();
EditorThreading.assertInteractionAllowed();
SelectionMarker marker = mySelectionMarker;
return marker != null && marker.isValid() && isVirtualSelectionEnabled() && marker.hasVirtualSelection();
}
@@ -317,7 +317,7 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla
@DirtyUI
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
if (orientation == SwingConstants.VERTICAL) {
return editor.getLineHeight();
}
@@ -445,7 +445,7 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla
final @NotNull var primaryCaret = caretModel.getPrimaryCaret();
myLastKnownPrimaryCaret = new WeakReference<>(primaryCaret);
ReadAction.run(() -> {
EditorThreading.run(() -> {
myPrimaryCaretLastKnownDot = primaryCaret.getOffset();
myPrimaryCaretLastKnownMark = primaryCaret.getLeadSelectionOffset();
});
@@ -670,13 +670,13 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla
@Override
public int getCaretPosition() {
return ReadAction.compute(() -> editor.getCaretModel().getOffset());
return EditorThreading.compute(() -> editor.getCaretModel().getOffset());
}
@DirtyUI
@Override
public void updateUI() {
ReadAction.run(() -> {
EditorThreading.run(() -> {
// Don't use the default TextUI, BaseTextUI, which does a lot of unnecessary
// work. We do however need to provide a TextUI implementation since some
// screen reader support code will invoke it
@@ -804,8 +804,7 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla
@Override
public String getText(final int offset, final int length) {
return ReadAction
.compute(() -> editor.getDocument().getText(new TextRange(offset, offset + length)));
return EditorThreading.compute(() -> editor.getDocument().getText(new TextRange(offset, offset + length)));
}
@Override
@@ -1293,7 +1292,7 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla
@Override
public int getCaretPosition() {
return ReadAction.compute(() -> editor.getCaretModel().getOffset());
return EditorThreading.compute(() -> editor.getCaretModel().getOffset());
}
@Override
@@ -1330,17 +1329,17 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla
@Override
public int getSelectionStart() {
return ReadAction.compute(() -> editor.getSelectionModel().getSelectionStart());
return EditorThreading.compute(() -> editor.getSelectionModel().getSelectionStart());
}
@Override
public int getSelectionEnd() {
return ReadAction.compute(() -> editor.getSelectionModel().getSelectionEnd());
return EditorThreading.compute(() -> editor.getSelectionModel().getSelectionEnd());
}
@Override
public @Nullable String getSelectedText() {
return ReadAction.compute(() -> editor.getSelectionModel().getSelectedText());
return EditorThreading.compute(() -> editor.getSelectionModel().getSelectedText());
}
// ---- Implements AccessibleEditableText ----
@@ -1392,7 +1391,7 @@ public final class EditorComponentImpl extends JTextComponent implements Scrolla
@Override
public void selectText(int startIndex, int endIndex) {
ReadAction.run(() -> {
EditorThreading.run(() -> {
editor.getSelectionModel().setSelection(startIndex, endIndex);
editor.getCaretModel().moveToOffset(endIndex);
});
@@ -383,7 +383,7 @@ final class EditorGutterComponentImpl extends EditorGutterComponentEx
@Override
public void paintComponent(Graphics g_) {
ReadAction.run(() -> {
EditorThreading.run(() -> {
Rectangle clip = g_.getClipBounds();
if (clip == null || clip.isEmpty()) {
return;
@@ -1605,22 +1605,22 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public @NotNull VisualPosition offsetToVisualPosition(int offset, boolean leanForward, boolean beforeSoftWrap) {
return ReadAction.compute(() -> myView.offsetToVisualPosition(offset, leanForward, beforeSoftWrap));
return EditorThreading.compute(() -> myView.offsetToVisualPosition(offset, leanForward, beforeSoftWrap));
}
public int offsetToVisualColumnInFoldRegion(@NotNull FoldRegion region, int offset, boolean leanTowardsLargerOffsets) {
assertIsDispatchThread();
return ReadAction.compute(() -> myView.offsetToVisualColumnInFoldRegion(region, offset, leanTowardsLargerOffsets));
return EditorThreading.compute(() -> myView.offsetToVisualColumnInFoldRegion(region, offset, leanTowardsLargerOffsets));
}
public int visualColumnToOffsetInFoldRegion(@NotNull FoldRegion region, int visualColumn, boolean leansRight) {
assertIsDispatchThread();
return ReadAction.compute(() -> myView.visualColumnToOffsetInFoldRegion(region, visualColumn, leansRight));
return EditorThreading.compute(() -> myView.visualColumnToOffsetInFoldRegion(region, visualColumn, leansRight));
}
@Override
public @NotNull LogicalPosition offsetToLogicalPosition(int offset) {
return ReadAction.compute(() -> myView.offsetToLogicalPosition(offset));
return EditorThreading.compute(() -> myView.offsetToLogicalPosition(offset));
}
@TestOnly
@@ -1667,7 +1667,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public @NotNull Point logicalPositionToXY(@NotNull LogicalPosition pos) {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
VisualPosition visible = logicalToVisualPosition(pos);
return visualPositionToXY(visible);
});
@@ -1675,7 +1675,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public @NotNull Point visualPositionToXY(@NotNull VisualPosition visible) {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
Point2D point2D = myView.visualPositionToXY(visible);
return new Point((int)point2D.getX(), (int)point2D.getY());
});
@@ -1683,7 +1683,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public @NotNull Point2D visualPositionToPoint2D(@NotNull VisualPosition visible) {
return ReadAction.compute(() -> myView.visualPositionToXY(visible));
return EditorThreading.compute(() -> myView.visualPositionToXY(visible));
}
/**
@@ -1701,12 +1701,12 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
}
public int findNearestDirectionBoundary(int offset, boolean lookForward) {
return ReadAction.compute(() -> myView.findNearestDirectionBoundary(offset, lookForward));
return EditorThreading.compute(() -> myView.findNearestDirectionBoundary(offset, lookForward));
}
@Override
public int visualLineToY(int line) {
return ReadAction.compute(() -> myView.visualLineToY(line));
return EditorThreading.compute(() -> myView.visualLineToY(line));
}
@Override
@@ -1733,7 +1733,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
return;
}
assertIsDispatchThread();
ReadAction.run(() -> {
EditorThreading.run(() -> {
int minEndOffset = Math.min(endOffset, getEditorModel().getDocument().getTextLength());
if (invalidateTextLayout) {
@@ -2200,7 +2200,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
return;
}
ApplicationManager.getApplication().runReadAction(() -> {
EditorThreading.run(() -> {
if (myUpdateCursor && !myPurePaintingMode) {
setCursorPosition();
myUpdateCursor = false;
@@ -2266,7 +2266,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public boolean hasHeaderComponent() {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
JComponent header = getHeaderComponent();
return header != null && header != getPermanentHeaderComponent();
});
@@ -2274,7 +2274,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public @Nullable JComponent getPermanentHeaderComponent() {
return ReadAction.compute(() -> getUserData(PERMANENT_HEADER));
return EditorThreading.compute(() -> getUserData(PERMANENT_HEADER));
}
@Override
@@ -2307,7 +2307,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public @NotNull Color getBackgroundColor() {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
Color forcedBackground = myState.getMyForcedBackground();
return forcedBackground == null ? getBackgroundIgnoreForced() : forcedBackground;
});
@@ -2376,7 +2376,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public int getMaxWidthInRange(int startOffset, int endOffset) {
return ReadAction.compute(() -> myView.getMaxWidthInRange(startOffset, endOffset));
return EditorThreading.compute(() -> myView.getMaxWidthInRange(startOffset, endOffset));
}
public boolean isPaintSelection() {
@@ -2414,7 +2414,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public int getLineHeight() {
return ReadAction.compute(() -> myView.getLineHeight());
return EditorThreading.compute(() -> myView.getLineHeight());
}
public int getDescent() {
@@ -2452,7 +2452,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
}
public @NotNull Dimension getPreferredSize() {
return ReadAction.compute(() -> isReleased ? new Dimension()
return EditorThreading.compute(() -> isReleased ? new Dimension()
: Registry.is("idea.true.smooth.scrolling.dynamic.scrollbars")
? new Dimension(getPreferredWidthOfVisibleLines(), myView.getPreferredHeight())
: myView.getPreferredSize());
@@ -2508,7 +2508,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public @NotNull Dimension getContentSize() {
return ReadAction.compute(() -> isReleased ? new Dimension() : myView.getPreferredSize());
return EditorThreading.compute(() -> isReleased ? new Dimension() : myView.getPreferredSize());
}
@Override
@@ -2528,12 +2528,12 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public Insets getInsets() {
return ReadAction.compute(() -> myScrollPane.getInsets());
return EditorThreading.compute(() -> myScrollPane.getInsets());
}
@Override
public int logicalPositionToOffset(@NotNull LogicalPosition pos) {
return ReadAction.compute(() -> myView.logicalPositionToOffset(pos));
return EditorThreading.compute(() -> myView.logicalPositionToOffset(pos));
}
/**
@@ -2544,17 +2544,17 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
* minus the number of folded lines.
*/
public int getVisibleLineCount() {
return ReadAction.compute(() -> myView.getVisibleLineCount());
return EditorThreading.compute(() -> myView.getVisibleLineCount());
}
@Override
public @NotNull VisualPosition logicalToVisualPosition(@NotNull LogicalPosition logicalPos) {
return ReadAction.compute(() -> myView.logicalToVisualPosition(logicalPos, false));
return EditorThreading.compute(() -> myView.logicalToVisualPosition(logicalPos, false));
}
@Override
public @NotNull LogicalPosition visualToLogicalPosition(@NotNull VisualPosition visiblePos) {
return ReadAction.compute(() -> myView.visualToLogicalPosition(visiblePos));
return EditorThreading.compute(() -> myView.visualToLogicalPosition(visiblePos));
}
private int offsetToLogicalLine(int offset) {
@@ -2678,7 +2678,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public EditorMouseEventArea getMouseEventArea(@NotNull MouseEvent e) {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
if (myGutterComponent != e.getSource()) return EditorMouseEventArea.EDITING_AREA;
int x = myGutterComponent.convertX(e.getX());
@@ -3136,7 +3136,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public boolean setCaretVisible(boolean b) {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
boolean old = myCaretCursor.isActive();
if (b) {
myCaretCursor.activate();
@@ -3150,7 +3150,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public boolean setCaretEnabled(boolean enabled) {
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
boolean old = myCaretCursor.isEnabled();
myCaretCursor.setEnabled(enabled);
return old;
@@ -3185,7 +3185,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public void setEmbeddedIntoDialogWrapper(boolean b) {
assertIsDispatchThread();
ReadAction.run(() -> myState.setEmbeddedIntoDialogWrapper(b));
EditorThreading.run(() -> myState.setEmbeddedIntoDialogWrapper(b));
}
private void isEmbeddedIntoDialogWrapperChanged(ObservableStateListener.PropertyChangeEvent event) {
@@ -3561,7 +3561,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
private @NotNull MyEditable getViewer() {
MyEditable editable = myEditable;
if (editable == null) {
myEditable = editable = ReadAction.compute(() -> new MyEditable());
myEditable = editable = EditorThreading.compute(() -> new MyEditable());
}
return editable;
}
@@ -3694,7 +3694,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
public void setColorsScheme(final @NotNull EditorColorsScheme scheme) {
assertIsDispatchThread();
final EditorImpl finalEditor = this;
ReadAction.run(() -> {
EditorThreading.run(() -> {
final EditorColorsManager colorsManager = ApplicationManager.getApplication().getServiceIfCreated(EditorColorsManager.class);
if (colorsManager == null) {
LOG.info("Skipping attempt to set color scheme without EditorColorsManager");
@@ -3721,7 +3721,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
@Override
public void setVerticalScrollbarOrientation(int type) {
assertIsDispatchThread();
ReadAction.run(() -> myState.setVerticalScrollBarOrientation(type));
EditorThreading.run(() -> myState.setVerticalScrollBarOrientation(type));
}
private void verticalScrollBarOrientationChanged(ObservableStateListener.PropertyChangeEvent event) {
@@ -4035,7 +4035,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
}
private static <T> T execute(@NotNull ThrowableComputable<T, RuntimeException> computable) {
return UIUtil.invokeAndWaitIfNeeded(() -> ReadAction.compute(computable));
return UIUtil.invokeAndWaitIfNeeded(() -> EditorThreading.compute(computable));
}
}
@@ -0,0 +1,71 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor.impl
import com.intellij.openapi.util.ThrowableComputable
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.application
import com.intellij.util.concurrency.ThreadingAssertions
import com.intellij.util.ui.EDT
import org.jetbrains.annotations.ApiStatus
/**
* Historically, Editor models required read lock for reading them.
* We are planning to relax the contracts and allow interacting with Editor from EDT without any locks.
* Hence, editor models should use this class to ensure consistency.
*/
@ApiStatus.Experimental
object EditorThreading {
/**
* Checks that interaction with the editor is allowed in the current context
*/
@JvmStatic
fun assertInteractionAllowed() {
if (Registry.`is`("editor.allow.raw.access.on.edt")) {
if (!EDT.isCurrentThreadEdt() && !application.isReadAccessAllowed) {
throw IllegalStateException("Access to Editor models (caret, selection, etc.) is allowed either from EDT, or under read action. Current thread: ${Thread.currentThread()}")
}
}
else {
ThreadingAssertions.assertReadAccess()
}
}
/**
* Adjust the context for editor accessing operation [action].
* [action] is always executed in place.
*/
@JvmStatic
fun <T, E : Throwable> compute(action: ThrowableComputable<T, E>): T {
return if (Registry.`is`("editor.allow.raw.access.on.edt")) {
if (EDT.isCurrentThreadEdt()) {
action.compute()
}
else {
application.runReadAction(action)
}
}
else {
application.runReadAction(action)
}
}
/**
* Adjust the context for editor accessing operation [action].
* [action] is always executed in place.
*/
@JvmStatic
fun <T, E : Throwable> run(action: Runnable) {
return if (Registry.`is`("editor.allow.raw.access.on.edt")) {
if (EDT.isCurrentThreadEdt()) {
action.run()
}
else {
application.runReadAction(action)
}
}
else {
application.runReadAction(action)
}
}
}
@@ -4,7 +4,6 @@ package com.intellij.openapi.editor.impl.view;
import com.intellij.diagnostic.Dumpable;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.colors.EditorFontType;
@@ -19,7 +18,6 @@ import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.concurrency.ThreadingAssertions;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.*;
@@ -126,12 +124,12 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab
}
public @NotNull LogicalPosition offsetToLogicalPosition(int offset) {
assertReadAccess();
assertEditorAccessible();
return myMapper.offsetToLogicalPosition(offset);
}
public int logicalPositionToOffset(@NotNull LogicalPosition pos) {
assertReadAccess();
assertEditorAccessible();
return myMapper.logicalPositionToOffset(pos);
}
@@ -232,7 +230,7 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab
@RequiresEdt
public @NotNull Dimension getPreferredSize() {
assert !myEditor.isPurePaintingMode();
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
getSoftWrapModel().prepareToMapping();
return mySizeManager.getPreferredSize();
});
@@ -250,7 +248,7 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab
@RequiresEdt
public int getPreferredWidth(int beginLine, int endLine) {
assert !myEditor.isPurePaintingMode();
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
getSoftWrapModel().prepareToMapping();
return mySizeManager.getPreferredWidth(beginLine, endLine);
});
@@ -259,7 +257,7 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab
@RequiresEdt
public int getPreferredHeight() {
assert !myEditor.isPurePaintingMode();
return ReadAction.compute(() -> {
return EditorThreading.compute(() -> {
getSoftWrapModel().prepareToMapping();
return mySizeManager.getPreferredHeight();
});
@@ -694,7 +692,7 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab
}
private void invalidateFoldRegionLayouts() {
ReadAction.run(() -> {
EditorThreading.run(() -> {
for (FoldRegion region : getFoldingModel().getAllFoldRegions()) {
invalidateFoldRegionLayout(region);
}
@@ -802,9 +800,9 @@ public final class EditorView implements TextDrawingCallback, Disposable, Dumpab
}
}
private void assertReadAccess() {
private void assertEditorAccessible() {
if (!myEditorModel.isAd()) {
ThreadingAssertions.assertReadAccess();
EditorThreading.assertInteractionAllowed();
}
}
@@ -0,0 +1,110 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.editor
import com.intellij.openapi.application.UiDispatcherKind
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.ui
import com.intellij.openapi.editor.impl.EditorThreading
import com.intellij.openapi.util.registry.Registry
import com.intellij.testFramework.common.timeoutRunBlocking
import com.intellij.testFramework.junit5.RegistryKey
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.fixture.*
import kotlinx.coroutines.Dispatchers
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory
import org.junit.jupiter.api.assertThrows
@TestApplication
class EditorThreadingTest {
private companion object {
val project = projectFixture()
val module = project.moduleFixture()
val sourceRoot = module.sourceRootFixture()
val file = sourceRoot.psiFileFixture("file.txt", "abcde")
}
private val editor = file.editorFixture()
val actions = listOf(
{ editor.get().caretModel.offset } to "caret",
{ EditorThreading.assertInteractionAllowed() } to "raw threading",
{ editor.get().selectionModel.selectedText } to "selection"
)
fun runTest(processor: (() -> Unit) -> Unit): List<DynamicTest> {
return actions.map {
DynamicTest.dynamicTest(it.second) {
processor { it.first() }
}
}
}
private fun withRawAccess(kind: Boolean, action: () -> Unit) {
val registryValue = Registry.get("editor.allow.raw.access.on.edt")
val current = registryValue.asBoolean()
registryValue.setValue(kind)
try {
action
}
finally {
registryValue.setValue(current)
}
}
@TestFactory
fun `access to editor is not allowed on BGT`() = runTest { action ->
withRawAccess(false) {
timeoutRunBlocking(context = Dispatchers.Default) {
assertThrows<Exception> {
action()
}
}
}
}
@TestFactory
fun `access to editor is not allowed on raw EDT`() = runTest { action ->
withRawAccess(true) {
timeoutRunBlocking(context = Dispatchers.ui(UiDispatcherKind.RELAX)) {
assertThrows<Exception> {
action()
}
}
}
}
@TestFactory
fun `access to editor is allowed under read lock`() = runTest { action ->
withRawAccess(false) {
timeoutRunBlocking(context = Dispatchers.ui(UiDispatcherKind.RELAX)) {
readAction {
action()
}
}
}
}
@TestFactory
fun `access to editor is allowed on raw EDT with flag`() = runTest { action ->
withRawAccess(true) {
timeoutRunBlocking(context = Dispatchers.ui(UiDispatcherKind.RELAX)) {
action()
}
}
}
@TestFactory
@RegistryKey("editor.allow.raw.access.on.edt", "true")
fun `access to editor is allowed under read lock with flag`() = runTest { action ->
withRawAccess(true) {
timeoutRunBlocking(context = Dispatchers.Default) {
readAction {
action()
}
}
}
}
}
@@ -420,6 +420,9 @@ editor.mouseSelectionStateResetDeadZone=4
editor.distraction.free.mode=false
editor.paint.empty.text=true
editor.allow.raw.access.on.edt=false
editor.allow.raw.access.on.edt.description=Enables the possibility to interact with the Editor models (Caret, Selection, etc.) without read lock on EDT
editor.focus.mode.color.light=c0c0c0
editor.focus.mode.color.light.description=Light color for focus mode
editor.focus.mode.color.dark=404040