Example: Right-to-Left Text

To create a basic editor for Arabic or Hebrew text, you only need to style the editor or some parent document with a direction: rtl property.

Of course, in a code editor context, you will often be dealing with a bunch of Latin syntax or tag names, causing right-to-left text to become heavily bidirectional. Editing mixed-direction text is, by its very nature, somewhat messy and confusing, but CodeMirror tries to make it bearable wherever it can.

Cursor motion (as defined in the default keymaps) is visual, meaning that if you press the left arrow your cursor should move left, regardless of the direction of the text at the cursor position.

Some other commands work in a logical direction—for example Backspace deletes before of the cursor, which is to the left in left-to-right text, and to the right in right-to-left text. Similarly, Delete deletes text after the cursor.

When you define custom commands that work in a visual way, you should check the local text direction, and use that to determine which way to go (possibly using the forward argument to something like moveByChar).

function cursorSemicolonLeft(view: EditorView) {
  let from = view.state.selection.main.head
  let dir = view.textDirectionAt(from)
  let line = view.state.doc.lineAt(from)
  let found = dir == Direction.LTR
    ? line.text.lastIndexOf(";", from - line.from)
    : line.text.indexOf(";", from - line.from)
  if (found < 0) return false
  view.dispatch({
    selection: {anchor: found + line.from},
    scrollIntoView: true,
    userEvent: "select"
  })
  return true
}

When writing extensions, take care to not assume a left-to-right layout. Either set up your CSS to use direction-aware properties or, if that doesn't work, explicitly check the global editor direction and adjust your behavior to that.

Bidi Isolation

A common issue with bidirectional programming or markup text is that the standard algorithm for laying the text out associates neutral punctuation characters between two pieces of directional text with the wrong side. See for example this right-to-left HTML code:

النص <span class="blue">الأزرق</span>

Though in the logical text, the <span class="blue"> appears as a coherent string, the algorithm will consider the punctuation "> to be part of the nearby right-to-left text, because that is the line's base direction. This results in an unreadable mess.

Thus, it can be useful to add elements with a unicode-bidi: isolate style around sections that should be ordered separate from the surrounding text. This bit of code does that for HTML tags:

import {EditorView, Direction, ViewPlugin, ViewUpdate,
        Decoration, DecorationSet} from "@codemirror/view"
import {Prec} from "@codemirror/state"
import {syntaxTree} from "@codemirror/language"
import {Tree} from "@lezer/common"

const htmlIsolates = ViewPlugin.fromClass(class {
  isolates: DecorationSet
  tree: Tree

  constructor(view: EditorView) {
    this.isolates = computeIsolates(view)
    this.tree = syntaxTree(view.state)
  }

  update(update: ViewUpdate) {
    if (update.docChanged || update.viewportChanged ||
        syntaxTree(update.state) != this.tree) {
      this.isolates = computeIsolates(update.view)
      this.tree = syntaxTree(update.state)
    }
  }
}, {
  provide: plugin => {
    function access(view: EditorView) {
      return view.plugin(plugin)?.isolates ?? Decoration.none
    }
    return Prec.lowest([EditorView.decorations.of(access),
                        EditorView.bidiIsolatedRanges.of(access)])
  }
})

This computes a set of decorations and keeps it up to date as the editor state changes. It provides the set to both the decoration and isolated range facets—the first makes sure the editable HTML is rendered appropriately, the second that CodeMirror's own order computations match the rendered order.

Because styling something as isolated only works if it is rendered as a single HTML element, we don't want other decorations to break up the isolating decorations. Because lower-precedence decorations are rendered around higher-precedence ones, we use Prec.lowest to give this extension a very low precedence.

computeIsolates uses the syntax tree to compute decorations for HTML tags in the visible ranges.

import {RangeSetBuilder} from "@codemirror/state"

const isolate = Decoration.mark({
  attributes: {style: "direction: ltr; unicode-bidi: isolate"},
  bidiIsolate: Direction.LTR
})

function computeIsolates(view: EditorView) {
  let set = new RangeSetBuilder<Decoration>()
  for (let {from, to} of view.visibleRanges) {
    syntaxTree(view.state).iterate({
      from, to,
      enter(node) {
        if (node.name == "OpenTag" || node.name == "CloseTag" ||
            node.name == "SelfClosingTag")
          set.add(node.from, node.to, isolate)
      }
    })
  }
  return set.finish()
}

Here's an editor showing this extension in action. Note that the HTML tags are shown coherently left-to-right.