From d91afee6e295eaccb4e1336729e5f91f92dbff04 Mon Sep 17 00:00:00 2001 From: Konstantin Nisht Date: Wed, 14 May 2025 11:47:06 +0200 Subject: [PATCH] [threading] IJPL-184084: Do not require read lock for interacting with editor models GitOrigin-RevId: e90351dd339f4436a66debf6c84060a3b568fbb2 --- .../platform-impl/api-dump-experimental.txt | 5 + .../openapi/editor/impl/CaretImpl.java | 14 +-- .../editor/impl/EditorComponentImpl.java | 21 ++-- .../impl/EditorGutterComponentImpl.java | 2 +- .../openapi/editor/impl/EditorImpl.java | 62 +++++----- .../openapi/editor/impl/EditorThreading.kt | 71 +++++++++++ .../openapi/editor/impl/view/EditorView.java | 18 ++- .../openapi/editor/EditorThreadingTest.kt | 110 ++++++++++++++++++ .../util/resources/misc/registry.properties | 3 + 9 files changed, 246 insertions(+), 60 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorThreading.kt create mode 100644 platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorThreadingTest.kt diff --git a/platform/platform-impl/api-dump-experimental.txt b/platform/platform-impl/api-dump-experimental.txt index 5477cea6f22b..47f630f9c7c9 100644 --- a/platform/platform-impl/api-dump-experimental.txt +++ b/platform/platform-impl/api-dump-experimental.txt @@ -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 - (com.intellij.openapi.editor.ex.EditorGutterComponentEx,com.intellij.openapi.editor.markup.GutterIconRenderer):V diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java index 7f2406f7c05a..1bd334497a7a 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java @@ -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(); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java index 89ac4c8430ba..88d16b461454 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorComponentImpl.java @@ -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); }); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java index 5ea289929e3a..9dc489c7f098 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java @@ -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; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index dece5f3dab85..591ed470b2f7 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -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 execute(@NotNull ThrowableComputable computable) { - return UIUtil.invokeAndWaitIfNeeded(() -> ReadAction.compute(computable)); + return UIUtil.invokeAndWaitIfNeeded(() -> EditorThreading.compute(computable)); } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorThreading.kt b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorThreading.kt new file mode 100644 index 000000000000..4b020a543f6a --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorThreading.kt @@ -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 compute(action: ThrowableComputable): 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 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) + } + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java index 1ec53b552dd9..73eca66ce2d3 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java @@ -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(); } } diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorThreadingTest.kt b/platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorThreadingTest.kt new file mode 100644 index 000000000000..41b0b37201ff --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/EditorThreadingTest.kt @@ -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 { + 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 { + action() + } + } + } + } + + @TestFactory + fun `access to editor is not allowed on raw EDT`() = runTest { action -> + withRawAccess(true) { + timeoutRunBlocking(context = Dispatchers.ui(UiDispatcherKind.RELAX)) { + assertThrows { + 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() + } + } + } + } +} \ No newline at end of file diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 54fd23291ad3..700885344fde 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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