correct navigation in bidirectional text using keyboard arrow keys (with caret stoppi...
authorDmitry Batrak <Dmitry.Batrak@jetbrains.com>
Thu, 28 May 2015 14:30:27 +0000 (17:30 +0300)
committerDmitry Batrak <Dmitry.Batrak@jetbrains.com>
Thu, 28 May 2015 15:41:15 +0000 (18:41 +0300)
platform/editor-ui-api/src/com/intellij/openapi/editor/Caret.java
platform/editor-ui-api/src/com/intellij/openapi/editor/VisualPosition.java
platform/lang-impl/src/com/intellij/injected/editor/InjectedCaret.java
platform/platform-impl/src/com/intellij/openapi/editor/actions/MoveCaretLeftOrRightHandler.java
platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretImpl.java
platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java
platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java
platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentCaret.java
platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorRtlTest.java
platform/testFramework/src/com/intellij/testFramework/LightPlatformCodeInsightTestCase.java

index 1ca3f7bac4181d998179a7650c4d76592653f1b0..f467e246395b839ef097ad3b51835d49fbcfb4c5 100644 (file)
@@ -302,4 +302,11 @@ public interface Caret extends UserDataHolderEx, Disposable {
    * to offset and logical column number in the vicinity of caret.
    */
   boolean isAtRtlLocation();
+
+  /**
+   * Returns <code>true</code> 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();
 }
index 633d85678b180210e287e190a2ad0e77307959e7..62f541df208c8580358f3a798cd79402c78af0b2 100644 (file)
@@ -77,6 +77,13 @@ public class VisualPosition {
     return line > other.line;
   }
 
+  /**
+   * Constructs a new <code>VisualPosition</code> 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" : "");
index 508a9bee4f8ead0ec6fa53e0418b42a1e6d87aa7..468745cabb59aa534707a973e0e9f3c66a6a6ea2 100644 (file)
@@ -242,4 +242,9 @@ public class InjectedCaret implements Caret {
   public boolean isAtRtlLocation() {
     return myDelegate.isAtRtlLocation();
   }
+
+  @Override
+  public boolean isAtDirectionBoundary() {
+    return myDelegate.isAtDirectionBoundary();
+  }
 }
index be2628f6741c5166f80e85dce665af2d5259a9fe..6edbe7a679c490f2564f4ea093a5a6ef65f0e377 100644 (file)
@@ -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);
+    }
   }
 }
index 1bab16ab8981faf93370780559d0eeeda9d404b0..59cd0b94b504c1377d1622bc4b195e0523cd4372 100644 (file)
@@ -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 <code>'y'</code> coordinate and height in pixels.
    */
index 79f44decbb5b151d0764dffcda1a5e650db9e51a..de7cdeea888a5acb244708a70aacb292f3c046c1 100644 (file)
@@ -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);
index 7260caae957b46d9dc0a538d0b7100f821797ea1..916b7f05d4d67b7f2c8d48528d15b908b1417ce8 100644 (file)
@@ -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;
index bce02d4833448923a330e0c91b6c8d93df503019..482694255f8f1aef41b08f5d6df8c8459e42f9b9 100644 (file)
@@ -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();
   }
index 0d773e4a94c021cf9ab0eb05aeb888254b3b9aa2..28878e749fe17959b264991bc693258c4ca73a4d 100644 (file)
@@ -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) {
index 1caaee7a1c99f30cc48a3a40e4b300a8478c0c05..776684a8bcca1e09af6faa4a06844605451488ec 100644 (file)
@@ -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());
   }