Caret Navigation in a Hybrid Markdown Editor
- Published
Well-made Markdown editors such as Obsidian and Typora feel comfortable to use in ways that can be hard to pinpoint. There are many reasons, but one that stands out is how carefully they handle caret navigation. Looking at caret movement in these two editors, I have tried to put together a set of principles of my own. Some apply only to Obsidian, some only to Typora, and some to neither.
Minimize layout shifts
The first principle that comes to mind is minimizing layout shifts caused by caret movement. In the following example, entering an inline code span makes the backticks appear, pushing the content to the right.
Inline code usually has a gray background, so there is often already a little space on either side of the text. If we use that space to display the backticks, there is no layout shift. Here is an example of this approach.
The same problem arises with headings (## Heading). Handling them as shown below causes a layout shift.
By contrast, displaying the # characters in a smaller font in the left margin avoids the layout shift. When needed, you can also move the caret naturally to the left to change the heading level.
An immediate connection
Bret Victor’s talk Inventing on Principle has been a great source of inspiration for me. In it, Victor introduces his principle: “Creators need an immediate connection to what they’re creating.” Applied to an editor, I take this to mean that when I change something, I should see the result immediately.
WYSIWYG editors generally follow this principle, though there is still plenty of room for improvement through approaches such as Result Oriented UI. This article is about hybrid editors, which sit between WYSIWYG editors and source editors, so let us consider how they might offer a more immediate connection.
For example, how should we handle a LaTeX math block? Most hybrid editors work like this.
While editing the code, you cannot see the rendered result. What if we displayed the two side by side, as below?
The existing content moves smoothly to the side, and revealing the math block’s source does not push down the sentence below it. You can also see the result in real time as you edit the equation.
Keep the caret from jumping somewhere unexpected
What should happen when the code is taller than its rendered output? This is often the case with Mermaid diagrams that render as wide, horizontal images. Avoiding a layout shift seems difficult here.
One problem with layout shifts is that they often move the caret somewhere other than where the user expected it to be. Even when we cannot prevent the layout shift itself, keeping it from sending the caret somewhere unexpected could at least reduce unnecessary eye movements.
When the caret enters from above, as below, the code expands downward, pushing the content beneath it out of the way. The caret stays where the user expected it, so the change does not catch them off guard.
Conversely, when the caret enters from below, the code should appear by pushing the content above it upward. That keeps the caret from jumping.
What if the caret enters from above and then exits below, as in the next example? On entry, the code should expand by pushing the content below it downward. On exit, it should collapse by pulling the content above it downward. That keeps the caret from jumping.
Do not reveal source without an intent to edit
In the backtick example we started with, displaying the backticks in the available space let us avoid layout shifts altogether.
But what about links? We cannot eliminate the layout shift. We can, however, reduce it by adjusting the font size.
Is that the best we can do? There is a small improvement we can make: leave the source hidden when the user holds down a key to move quickly past it. If it is clear that they are simply passing through, with no intention of editing, there is no need to cause a layout shift.
When intent is ambiguous, minimize the cost of getting it wrong
What should we do when the user’s expectations are unclear and there is no single right answer? For example, where should the caret go when the user presses ⌘→ / End in the following situation?
Some people will expect the caret to land after the “C,” as shown below. Let us call this option A.
Others will expect it to land at the end of the sentence, as below. Let us call this option B.
Whose expectation is correct? There is no right answer. In this situation, we should choose the behavior that takes less effort to recover from when it differs from what the user expected.
If someone expecting A uses a system that behaves like B, they have to reach for the mouse and click next to the “C,” or press the left arrow several times to get the caret there. That is a nuisance. By contrast, someone expecting B who encounters A can simply press ⌘→ / End once more.
So in this case, I chose to make the caret behave like option A.
The same applies to selecting all with ⌘A / Ctrl + A. When the caret is inside a code block, as below, should Select All select just that block or the entire document? Following the same principle, I made it select the code block’s contents first, then the whole document on a second press. (Although I am not particularly fond of it, the block editor Notion also works this way.)
Reversible caret movement
The final principle is reversibility. Put simply, if you move up once and then down once, the caret should, as far as possible, return to exactly where it started. Let us compare examples that follow this principle with ones that do not.
For example, where should the caret go if we move it one position to the right in the following situation?
One possibility is to place it at the beginning of the block, as below. Let us call this option A.
Another is to place it where the actual code begins. Let us call this option B.
You might think B is better because it saves a few keystrokes compared with A. But from the perspective of reversible caret movement, it may be a poor choice that undermines the user’s ability to build habits.
With A, pressing the left arrow once returns the caret to exactly where you expect it. In other words, pressing the right arrow once and then the left arrow once brings it back to its original position. With B, this principle does not hold. The following example shows how B breaks reversibility.
With A, by contrast, reversibility is preserved.
Conclusion
These principles are less a set of definitive answers than a record of my thinking about how to offer a consistent editing experience. I plan to build an editor that follows them, use it regularly, and keep refining them.