From d97ae9d8584de253a8b848013f799ba074d76e0d Mon Sep 17 00:00:00 2001 From: Dmitry Batrak Date: Thu, 28 May 2015 17:30:27 +0300 Subject: [PATCH] correct navigation in bidirectional text using keyboard arrow keys (with caret stopping and switching direction on ltr/rtl boundary) --- .../com/intellij/openapi/editor/Caret.java | 7 +++++ .../openapi/editor/VisualPosition.java | 7 +++++ .../injected/editor/InjectedCaret.java | 5 ++++ .../actions/MoveCaretLeftOrRightHandler.java | 14 +++++++--- .../openapi/editor/impl/CaretImpl.java | 16 ++++++++--- .../openapi/editor/impl/view/EditorView.java | 8 ++++++ .../openapi/editor/impl/view/LineLayout.java | 11 ++++++++ .../editor/textarea/TextComponentCaret.java | 5 ++++ .../openapi/editor/impl/EditorRtlTest.java | 27 +++++++++++++++++++ .../LightPlatformCodeInsightTestCase.java | 16 +++++++++++ 10 files changed, 108 insertions(+), 8 deletions(-) diff --git a/platform/editor-ui-api/src/com/intellij/openapi/editor/Caret.java b/platform/editor-ui-api/src/com/intellij/openapi/editor/Caret.java index 1ca3f7bac418..f467e246395b 100644 --- a/platform/editor-ui-api/src/com/intellij/openapi/editor/Caret.java +++ b/platform/editor-ui-api/src/com/intellij/openapi/editor/Caret.java @@ -302,4 +302,11 @@ public interface Caret extends UserDataHolderEx, Disposable { * to offset and logical column number in the vicinity of caret. */ boolean isAtRtlLocation(); + + /** + * Returns true if caret is located at a boundary between LTR and RTL text fragments. Caret can located at any side of the + * boundary, exact location can be determined from directionality flags of caret's logical and visual position + * ({@link LogicalPosition#leansForward} and {@link VisualPosition#leansRight}). + */ + boolean isAtDirectionBoundary(); } diff --git a/platform/editor-ui-api/src/com/intellij/openapi/editor/VisualPosition.java b/platform/editor-ui-api/src/com/intellij/openapi/editor/VisualPosition.java index 633d85678b18..62f541df208c 100644 --- a/platform/editor-ui-api/src/com/intellij/openapi/editor/VisualPosition.java +++ b/platform/editor-ui-api/src/com/intellij/openapi/editor/VisualPosition.java @@ -77,6 +77,13 @@ public class VisualPosition { return line > other.line; } + /** + * Constructs a new VisualPosition instance with a given value of {@link #leansRight} flag. + */ + public VisualPosition leanRight(boolean value) { + return new VisualPosition(line, column, value); + } + @NonNls public String toString() { return "VisualPosition: (" + line + ", " + column+")" + (leansRight ? " leans right" : ""); diff --git a/platform/lang-impl/src/com/intellij/injected/editor/InjectedCaret.java b/platform/lang-impl/src/com/intellij/injected/editor/InjectedCaret.java index 508a9bee4f8e..468745cabb59 100644 --- a/platform/lang-impl/src/com/intellij/injected/editor/InjectedCaret.java +++ b/platform/lang-impl/src/com/intellij/injected/editor/InjectedCaret.java @@ -242,4 +242,9 @@ public class InjectedCaret implements Caret { public boolean isAtRtlLocation() { return myDelegate.isAtRtlLocation(); } + + @Override + public boolean isAtDirectionBoundary() { + return myDelegate.isAtDirectionBoundary(); + } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/actions/MoveCaretLeftOrRightHandler.java b/platform/platform-impl/src/com/intellij/openapi/editor/actions/MoveCaretLeftOrRightHandler.java index be2628f6741c..6edbe7a679c4 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/actions/MoveCaretLeftOrRightHandler.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/actions/MoveCaretLeftOrRightHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2015 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. @@ -69,8 +69,14 @@ class MoveCaretLeftOrRightHandler extends EditorActionHandler { } } } - final boolean scrollToCaret = (!(editor instanceof EditorImpl) || ((EditorImpl)editor).isScrollToCaret()) - && caret == editor.getCaretModel().getPrimaryCaret(); - caretModel.moveCaretRelatively(myDirection == Direction.RIGHT ? 1 : -1, 0, false, false, scrollToCaret); + VisualPosition currentPosition = caret.getVisualPosition(); + if (caret.isAtDirectionBoundary() && (myDirection == Direction.RIGHT ^ currentPosition.leansRight)) { + caret.moveToVisualPosition(currentPosition.leanRight(!currentPosition.leansRight)); + } + else { + final boolean scrollToCaret = (!(editor instanceof EditorImpl) || ((EditorImpl)editor).isScrollToCaret()) + && caret == editor.getCaretModel().getPrimaryCaret(); + caretModel.moveCaretRelatively(myDirection == Direction.RIGHT ? 1 : -1, 0, false, false, scrollToCaret); + } } } 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 1bab16ab8981..59cd0b94b504 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 @@ -267,6 +267,8 @@ public class CaretImpl extends UserDataHolderBase implements Caret { int newLineNumber = visualCaret.line + lineShift; int newColumnNumber = visualCaret.column + columnShift; + boolean newLeansRight = lineShift == 0 && columnShift != 0 ? columnShift < 0 : visualCaret.leansRight; + if (desiredX >= 0) { newColumnNumber = myEditor.xyToVisualPosition(new Point(desiredX, Math.max(0, newLineNumber) * myEditor.getLineHeight())).column; } @@ -305,13 +307,14 @@ public class CaretImpl extends UserDataHolderBase implements Caret { VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber); if (!myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) { - LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber)); + LogicalPosition log = myEditor.visualToLogicalPosition(new VisualPosition(newLineNumber, newColumnNumber, newLeansRight)); int offset = myEditor.logicalPositionToOffset(log); if (offset >= document.getTextLength()) { - int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength()).column; + int lastOffsetColumn = myEditor.offsetToVisualPosition(document.getTextLength(), true).column; // We want to move caret to the last column if if it's located at the last line and 'Down' is pressed. if (lastOffsetColumn > newColumnNumber) { newColumnNumber = lastOffsetColumn; + newLeansRight = true; desiredX = -1; lastColumnNumber = -1; } @@ -321,7 +324,7 @@ public class CaretImpl extends UserDataHolderBase implements Caret { if (offset >= 0 && offset < document.getTextLength()) { if (text.charAt(offset) == '\t' && (columnShift <= 0 || offset == myOffset)) { if (columnShift <= 0) { - newColumnNumber = myEditor.offsetToVisualPosition(offset).column; + newColumnNumber = myEditor.offsetToVisualPosition(offset, true).column; } else { SoftWrap softWrap = myEditor.getSoftWrapModel().getSoftWrap(offset + 1); @@ -339,7 +342,7 @@ public class CaretImpl extends UserDataHolderBase implements Caret { } } - pos = new VisualPosition(newLineNumber, newColumnNumber); + pos = new VisualPosition(newLineNumber, newColumnNumber, newLeansRight); if (columnShift != 0 && lineShift == 0 && myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) { LogicalPosition logical = myEditor.visualToLogicalPosition(pos); int softWrapOffset = myEditor.logicalPositionToOffset(logical); @@ -1466,6 +1469,11 @@ public class CaretImpl extends UserDataHolderBase implements Caret { return myEditor.myUseNewRendering && myEditor.myView.isRtlLocation(myOffset, myLogicalCaret.leansForward); } + @Override + public boolean isAtDirectionBoundary() { + return myEditor.myUseNewRendering && myEditor.myView.isDirectionBoundary(myOffset); + } + /** * Encapsulates information about target vertical range info - its 'y' coordinate and height in pixels. */ 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 79f44decbb5b..de7cdeea888a 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 @@ -268,6 +268,14 @@ public class EditorView implements Disposable { return layout.isRtlLocation(offset - myDocument.getLineStartOffset(line), leanForward); } + public boolean isDirectionBoundary(int offset) { + assertIsDispatchThread(); + if (myDocument.getTextLength() == 0) return false; + int line = myDocument.getLineNumber(offset); + LineLayout layout = getLineLayout(line); + return layout.isDirectionBoundary(offset - myDocument.getLineStartOffset(line)); + } + @NotNull LineLayout getLineLayout(int line) { return myTextLayoutCache.getLineLayout(line); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java index 7260caae957b..916b7f05d4d6 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java @@ -212,6 +212,17 @@ class LineLayout { return false; } + boolean isDirectionBoundary(int offset) { + boolean prevIsRtl = false; + for (BidiRun run : myBidiRunsInLogicalOrder) { + boolean curIsRtl = run.isRtl(); + if (offset == run.startOffset && curIsRtl != prevIsRtl) return true; + if (offset < run.endOffset) return false; + prevIsRtl = curIsRtl; + } + return prevIsRtl; + } + private static class BidiRun { private final byte level; private final int startOffset; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentCaret.java b/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentCaret.java index bce02d483344..482694255f8f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentCaret.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentCaret.java @@ -203,6 +203,11 @@ public class TextComponentCaret extends UserDataHolderBase implements Caret { return false; } + @Override + public boolean isAtDirectionBoundary() { + return false; + } + private SelectionModel getSelectionModel() { return myEditor.getSelectionModel(); } diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorRtlTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorRtlTest.java index 0d773e4a94c0..28878e749fe1 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorRtlTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorRtlTest.java @@ -235,6 +235,29 @@ public class EditorRtlTest extends AbstractEditorTest { assertLogicalPositionsEqual("Wrong logical position", lF(1), myEditor.getCaretModel().getLogicalPosition()); } + public void testNavigationWithArrowKeys() throws Exception { + init("llrrll\nllrrll"); + assertCaretPosition(vL(0)); + right(); + assertCaretPosition(vL(1)); + right(); + assertCaretPosition(vL(2)); + right(); + assertCaretPosition(vR(2)); + right(); + assertCaretPosition(vL(3)); + right(); + assertCaretPosition(vL(4)); + down(); + assertCaretPosition(v(1, 4, false)); + left(); + assertCaretPosition(v(1, 3, true)); + left(); + assertCaretPosition(v(1, 2, true)); + up(); + assertCaretPosition(vR(2)); + } + private void init(String text) throws IOException { initText(text.replace(RTL_CHAR_REPRESENTATION, RTL_CHAR)); } @@ -299,6 +322,10 @@ public class EditorRtlTest extends AbstractEditorTest { assertEquals(message, expectedPosition, actualPosition); assertEquals(message + " (direction flag)", expectedPosition.leansRight, actualPosition.leansRight); } + + private static void assertCaretPosition(VisualPosition visualPosition) { + assertVisualPositionsEqual("Wrong caret position", visualPosition, myEditor.getCaretModel().getVisualPosition()); + } // logical position leaning backward private static LogicalPosition lB(int column) { diff --git a/platform/testFramework/src/com/intellij/testFramework/LightPlatformCodeInsightTestCase.java b/platform/testFramework/src/com/intellij/testFramework/LightPlatformCodeInsightTestCase.java index 1caaee7a1c99..776684a8bcca 100644 --- a/platform/testFramework/src/com/intellij/testFramework/LightPlatformCodeInsightTestCase.java +++ b/platform/testFramework/src/com/intellij/testFramework/LightPlatformCodeInsightTestCase.java @@ -574,6 +574,22 @@ public abstract class LightPlatformCodeInsightTestCase extends LightPlatformTest executeAction("EditorSelectLine"); } + protected static void left() { + executeAction("EditorLeft"); + } + + protected static void right() { + executeAction("EditorRight"); + } + + protected static void up() { + executeAction("EditorUp"); + } + + protected static void down() { + executeAction("EditorDown"); + } + protected static void lineComment() { new CommentByLineCommentAction().actionPerformedImpl(getProject(), getEditor()); }