Editing text is central to users' experience in our note-taking app Lockbook. We need our text editor to be:
Customizable so we can build our ideal note-taking experience
Cross-platform for a consistent editing experience on any device
Fast to render large documents at high frame rates
We also want to use Markdown, a "lightweight markup language for creating formatted text using a plain-text editor." Not only is it an ergonomic way to style text, but it's also easy to build into an app due to the availability of supporting libraries. It's increasingly popular in the note-taking ecosystem as well as productivity apps like Slack. Finally, since Markdown is written as plain text, it lets us support external editors like Vim using our CLI.
Existing solutions dissatisfied us, typically in all three dimensions. That led us to write our own text editor using a lightweight UI library called egui and a fast markdown parser called pulldown-cmark. This post describes challenges that motivated our editor's behavior and how we rationalize our solution.
Prior Work
Different apps have different experiences for editing Markdown.
Markdown editors like GitHub, StackEdit, and VSCode offer a side-by-side preview by rendering the Markdown to HTML and either lightly style or don’t style the text you're editing.
This seems to be the most established workflow for editing Markdown, but the editing experience leaves us looking for something better. We want users spending their time looking at beautiful rendered Markdown instead of the underlying plain text. On mobile platforms where screen space is limited, we can't offer a side-by-side preview, so when you'd want to confirm you've got the syntax correct so far you'd have to switch to a preview view then back to the editor.
The editors in Slack, Discord and Confluence style the text as you're writing. When you place the finishing asterisk around your **bold text** or the finishing backtick around your `inline code`, you're instantly rewarded with breathtaking rendered Markdown that sends your dopamine circuits firing on all cylinders. Except for in the handful of weird edge cases.
Character Capture Edge Cases
When an editor hides Markdown characters as it applies text styles, we call it character capture. In editors that do character capture, ambiguous situations arise because capturing characters removes distinct, valid cursor positions. In `inline code`, the cursor position before the final backtick is inside the styled section and the cursor position after it is outside, but when the backtick is captured, these two positions aren't visually distinguished.
One way to handle this is to visually distinguish them. In Slack, `inline code` is styled with a background and spaced apart from other text, and when the cursor is inside an inline code section, it's drawn in same color as the text. Using the right arrow to move the cursor from before the final backtick (inside the code block) to after (outside) moves the cursor outside the background and draws it in the font color. The cursor color and position tells the user whether they're inside or outside the styled section.
Enter the edge cases. In Slack, if you type `inline code` including the trailing backtick, your cursor ends up outside the styled section. Then if you type one character and delete it, your cursor confusingly ends inside the section. You can use the right arrow key to move the cursor back outside, but this also inserts a space after the section.
If there's no text before the beginning of the section, enough left arrow keystrokes will move cursor to the start of the section but won't insert a space or move to outside the section like right arrow did. With the cursor at the start of the section, the only way to type unstyled text before the code section is to use a keyboard shortcut or toolbar button to disable the inline code style.
Once there's any text before the code section, the arrow keys will skip the cursor position at the start of the code section, so the only way to type styled text at the beginning of the styled section is to place the cursor before the start of the section and use a keyboard shortcut or toolbar button to enable the inline code style.
Don't worry if you didn't follow all of that precisely. The short version is, the editing experience becomes complicated and sometimes requires doing something other than typing. Undoubtedly, Slack's developers negotiated their constraints carefully. They tried to deliver a good experience for most users most of the time knowing it would never be perfect.
Internally, we tested generalizing Slack's editing UX for inline code (they handle other styles like bold and italic differently) but found it undesirable, even with a styled cursor and careful attention to edge cases. For example, once the Markdown characters for links are captured, it becomes difficult to modify the link URL. That's because the URL itself is captured, so it's invisible even as you're typing it. Slack works around this by opening a small link menu when you place the cursor in a link, which breaks an otherwise text-only editing experience.
My Editor Teacher
We're building our Markdown editor as enthusiasts, but if we want Lockbook to be a great replacement for the ubiquitous Apple Notes, we need it to be user-friendly for those among us who are unenthusiastic about Markdown. Users shouldn't need to learn Markdown to use our app. That said, we think writing Markdown is the best way to experience Lockbook. We balance these values by offering an experience where users can use familiar shortcuts and toolbar buttons to apply Markdown-based styles and the editor will show what changes it made to achieve the styles. Lockbook teaches users how Markdown works so we can help them toward the best experience without pressuring them to learn something new.
Toolbar buttons and shortcuts can only be implemented by editing the underlying Markdown. From a development perspective, it involves manipulating text based on the abstract syntax tree from our parser. Using the 'bold' toolbar button when text is selected inserts two leading and two trailing asterisks.
Using the 'bold' toolbar button when bold text is selected removes the leading and trailing asterisks, unless the selection includes only a middle part of the bold text, in which case the bold section needs to be split into two separate sections with the selected text left unbolded in-between. Applying the bold style to text when the selection contains some bold text and some not bold text, or when it contains text with other styles applied, produces more complicated results. This is an area of active development for Lockbook's editor.
Another example of editing Markdown without typing is inserting a link. In Markdown, a link contains the link text between square brackets followed by a URL between parentheses. We support the ctrl+k/cmd+k keyboard shortcut for applying a link to selected text which is familiar to users of Google Docs and many other apps. Here, we take some inspiration from GitHub's Markdown editor, which handles the shortcut by wrapping the selected text in brackets, inserting a pair of empty parentheses after, and places the cursor between the parentheses where you can type or paste your link's URL.
From there, we take it one step further - when you hit enter with the cursor in a link's URL, we advance the cursor to after the link instead of inserting a newline. This means that with a link in your clipboard, you can select text, use ctrl+k/cmd+k to apply a link, ctrl+v/cmd+v to paste the URL, then hit enter to continue typing, just like in Google Docs. The way applying a link modifies the underlying Markdown is subtle and delightful.
In these cases and more, we'd like users to see the way the Markdown syntax characters are manipulated by toolbar operations and keyboard shortcuts so they can learn Markdown passively. The key to our solution is to not capture characters for sections that contain the cursor. When the cursor is in the text for an inline code section, the backticks that apply the style are rendered so that you can see what you're doing. The section is still styled as inline code and the backticks are rendered in grey font for feedback, but all the cursor positions are distinguishable so edge cases are avoided. Then, when the cursor leaves the section, the backticks are captured, leaving behind nothing but gorgeous, gorgeous rendered Markdown. This solution works similarly to Obsidian which is a source of inspiration for us.
Conclusion
We build our own text editor for Lockbook because that's what it takes to build the best note-taking app. We balance needs of casual users and power users using a design that passively teaches the language, taking inspiration from existing apps.
I'll be following this post with ones that discuss our editor's implementation, sometimes at a high level and sometimes in detail. Stay tuned!
If Lockbook piqued your interest and you'd like to learn more, check out our website, GitHub, and Discord.