RTL editor: can't type any thing after a LTR text if it's at the end of the line

Hello codemirror society!
First of all, I really appreciate your great work on codemirror!
I encountered an issue while using codemirror in editing Arabic documents. If the text editor direction is RTL but the line ends with text that is LTR, you can’t type any thing after that LTR text! Here is an illustration of what I mean from your RTL example:
Screencast 2024-10-02 20_16_06
As you see, I typed 10 at the end of the line. I can’t type a space after the 10 since codemirror will insert the space BEFORE the 10. I can’t also break the line since codemirror will insert the line break BEFORE 10 too.
Thanks for attention. Best regards.

Editing on direction boundaries is messy, but I don’t think this is really a bug. It matches Firefox’s native editing behavior in this situation (though Chrome indeed behaves differently). You can put your cursor at the end (right) of the number to insert right-to-left text after it. In fact, just continuing to type after you type the number seems to work fine. It’s only after you move the start (left) of the number that inserted text will go before it.

2 Likes

Thanks for clarification! However, I’m used to the way Libreoffice ans MS word implement cursor movement. In both programs, the cursor moves logically, and not visually (e.g. left arrow in RTL text moves cursor to next character, whether it’s on the left or right). Is there a way to configure codemirror cursor to behave the same way?

Yes, you should be able to bind ArrowLeft to cursorCharBackward and ArrowRight to cursorCharForward instead of the defaults (cursorCharLeft/cursorCharRight).

Thank you! I’ll try to implement it.

I tried making my keymap, but the cursor still moves visually, here is my implementation:

const LogicalKeyMap = [
      {key: "ArrowLeft", run: cursorCharForward, shift: selectCharForward, preventDefault: true},
  {key: "Mod-ArrowLeft", mac: "Alt-ArrowLeft", run: cursorGroupForward, shift: selectGroupForward, preventDefault: true},

  {key: "ArrowRight", run: cursorCharBackward, shift: selectCharBackward, preventDefault: true},
  {key: "Mod-ArrowRight", mac: "Alt-ArrowRight", run: cursorGroupBackward, shift: selectGroupBackward, preventDefault: true},

I also tried calling cursorCharForward every second, to see how the cursor will move:

m = function(){cursorCharForward(Editor); setTimeout(m,1000)}
m()

Here is the result:
Screencast 2024-10-06 22_35_54
Am I doing something wrong?

After looking at the source code, all what cursorCharForward does is moving the cursor visually away from the line start.

You’re right! I thought I had already implemented such commands, but I hadn’t. This patch adds them.

Great! I also suggest adding cursorLogicalLeft and cursorLogicalRight. What cursorLogicalLeft does for example is moving the cursor logically forward if the editor is RTL, but logically backward if the editor is LTR. cursorLogicalRight does the opposite. This is my unprofessional :sweat_smile: implementation I’ve made recently:

function cursorCharForwardLogical(editor, range){
      let cursor_position;
      if(range.from == range.to){
            cursor_position = EditorSelection.create([EditorSelection.range(
                Math.min(range.from + 1, editor.state.doc.length), 
                Math.min(range.from + 1, editor.state.doc.length)
            )]);
      }
      else{
            cursor_position = EditorSelection.create([EditorSelection.range(
                range.to, 
                range.to
            )]);
      }
    const mutation = {selection:cursor_position}
    editor.dispatch(mutation);
    return true;    
}
function cursorCharBackwardLogical(editor, range){
      let cursor_position;
      if(range.from == range.to){
            cursor_position = EditorSelection.create([EditorSelection.range(
                Math.max(range.from - 1, 0),
                Math.max(range.from - 1, 0),
            )]);
      }
      else{
            cursor_position = EditorSelection.create([EditorSelection.range(
                range.from, 
                range.from
            )]);
      }
    const mutation = {selection:cursor_position}
    editor.dispatch(mutation);
    return true;
}
function cursorLogicalLeft(editor){
    let range = editor.state.selection.ranges[0];
    if(editor.textDirection == 1){ //RTL
        return cursorCharForwardLogical(editor, range);
    }
    else{
        return cursorCharBackwardLogical(editor, range);
    }
    
}
function cursorLogicalRight(editor){
    let range = editor.state.selection.ranges[0];
    if(editor.textDirection == 1){ //RTL
        return cursorCharBackwardLogical(editor, range);
    }
    else{
        return cursorCharForwardLogical(editor, range);
    }
}
function cursorGroupForwardLogical(editor, range){
    // range.from == range.to
    let doc = editor.state.doc.toString();
    let i = range.from;
    while(i < doc.length){
        if(doc[i].match(/\p{Z}/u)){
            i++;
        }
        else{
            break
        }
    }
    while(i < doc.length){
        if(doc[i].match(/\p{L}|_|\p{N}|\p{M}/u)){
            i++;
        }
        else{
            break
        }
    }
    if(i == range.from){
        i = Math.min(i + 1, doc.length);
    }
      
    const cursor_position = EditorSelection.create([EditorSelection.range(
        i, 
        i
    )]);
    const mutation = {selection:cursor_position}
    editor.dispatch(mutation);
    return true;    
}
function cursorGroupBackwardLogical(editor, range){
    // range.from == range.to
    let doc = editor.state.doc.toString();
    let i = range.from;
    while(i > -1){
        if(doc[i].match(/\p{Z}/u)){
            i--;
        }
        else{
            break
        }
    }
    while(i > -1){
        if(doc[i].match(/\p{L}|_|\p{N}|\p{M}/u)){
            i--;
        }
        else{
            break
        }
    }
    if(i == range.from){
        i = Math.max(i - 1, 0);
    }
      
    const cursor_position = EditorSelection.create([EditorSelection.range(
        i, 
        i
    )]);
    const mutation = {selection:cursor_position}
    editor.dispatch(mutation);
    return true;  
}
function cursorGroupLogicalLeft(editor){
    let range = editor.state.selection.ranges[0];
    if(range.from != range.to){
        return cursorLogicalLeft(editor);
    }
    if(editor.textDirection == 1){ //RTL
        return cursorGroupForwardLogical(editor, range);
    }
    else{
        return cursorGroupBackwardLogical(editor, range);
    }
}
function cursorGroupLogicalRight(editor){
    let range = editor.state.selection.ranges[0];
    if(range.from != range.to){
        return cursorLogicalRight(editor);
    }
    if(editor.textDirection == 1){ //RTL
        return cursorGroupBackwardLogical(editor, range);
    }
    else{
        return cursorGroupForwardLogical(editor, range);
    }
}

const LogicalKeyMap = [
      {key: "ArrowLeft", run: cursorLogicalLeft, shift: selectCharLeft, preventDefault: true},
  {key: "Mod-ArrowLeft", mac: "Alt-ArrowLeft", run: cursorGroupLogicalLeft, shift: selectGroupLeft, preventDefault: true},
  
  {key: "ArrowRight", run: cursorLogicalRight, shift: selectCharRight, preventDefault: true},
  {key: "Mod-ArrowRight", mac: "Alt-ArrowRight", run: cursorGroupLogicalRight, shift: selectGroupRight, preventDefault: true},
]

Now the cursor moves as expected:
Screencast 2024-10-07 13_28_33

1 Like