Inline code! – Medium Engineering


I wrote this on Hatch on Sept. 22, 2016. Presented here with minor edits for clarity. See Hatching Inside Medium for more context.

Check it out: you can create inline code on Medium now! The invocation is ` + any other character. You should see the backtick disappear, and your character should appear as inline code. WYSIWYG! Yay.

Keep typing, then exit the mode by typing another `. You can also select a piece of text and then hit ` to convert it into inline code 😱

OK. Now I’ll go through some recent history, some implementation details, and one million edge cases 🙌

Context/History

Fun fact: we tried three different invocations for inline code before settling on this one.

The internal-only implementation we had for a while was actually most similar to how it works now: ` to enter the mode, and another ` to exit. Pros: familiar and intuitive to engineers. Cons: there’s no way to get two literal backticks in the editor, and on international keyboards, backtick followed by another character makes a diacritical character (like è), and we would be overwriting that behavior.

When we started talking about polishing inline code for release, we talked through these tradeoffs and initially decided on the super unsatisfying invocation cmd-opt-`, both as a way to enter and exit the mode, and as a way to convert selected text. Joe wrote up a product spec, and we built and released this version internally.

The response was lukewarm.

One night Marcin pinged me on Slack and told me that cmd-opt-` was not working for him. His keyboard was the same as mine, and it was weirdly only breaking in Chrome. We debugged unsuccessfully for a few hours, then decided, hell, there wasn’t anything special about cmd-opt-`. We already have a bunch of cmd-opt-n shortcuts in the editor, including cmd-opt-6 for code blocks. Why not cmd-opt-7? cmd-opt-7 was working.

So, for a hot second it was going to be cmd-opt-7.

Marcin quickly proposed a better solution: go back to backticks, but instead of a backtick throwing you into the mode right away, we would wait for another character. Then, assuming no diacritical magic, it would convert that character and put you in the mode. You exit the mode by typing another backtick, but only on the rightmost edge of an existing piece of inline code (this way you can still have backticks within inline code: `, and you can also write strings of literal backticks: “`).

Koop suggested that selecting a piece of text and hitting ` should convert it to inline code, much like selecting a piece of text and hitting ( or " wraps it.

This is way better.

Implementation

Our editor does a fair bit of smart text substitution. For example, if you type a < followed by a 3, we remove both characters and insert a ❤.

We accomplish this by always looking for the final character in these sequences. In the <3 case, we look for 3, and then we construct a paragraph model to check the character before it, to see if it’s a <. Since 3 doesn’t really come around that often, this is fine. But the equivalent check for inline code would be triggered by any ASCII character between 33 and 126 (basically every keystroke in the editor), so instead of creating a paragraph model every time, we first dig through goog.dom.TextRange and manually check if the previous character was a backtick.

The insertion code, when called, further ensures that our backtick was preceded by either a space or the beginning of a paragraph, then deletes the backtick and the following character, and re-inserts the character marked up as <code>.

We have a plugin that handles most of our formatting keyboard shortcuts (including cmd-b for strong, cmd-i for em, and cmd-k for links), so it wasn’t hard to create a new keyboard shortcut for inline code, and then pass the shortcut through IFF 1) something’s selected, or 2) we’re at the right edge of a piece of inline code.

That’s pretty much it!

Edge Cases

And now for the part that took up 90% of engineering time on this project: edge cases! There were a bunch of them, and some of them were pretty weird. Here are the big ones:

  • How does inline code interact with other elements in the editor? Can it exist within headings, quotes, code blocks, or image captions? No, we decided, it can’t. Turns out this disabling (for inline code, and also for other markups like strong and em) happens entirely in CSS. That way, you can convert a piece of text containing inline code into a heading and back, and it will preserve the inline code, but not display it within the heading.
  • We disabled smart text replacement within inline code. That’s the thing I talked about earlier that converts <3 into ❤. It also converts quotes to the curly kind, two adjacent hyphens into an emdash, and it calls up a typeahead popover when you type a @. None of these things are great when they happen in inline code.
  • We had to explicitly make inline code copy and paste-able, since we’re pretty opinionated about the sorts of things you’re allowed to paste into the editor. But since we use <code> tags to display inline code, once it was possible to copy and paste from one Medium article to another, we also got the ability to paste code from Github (and other places that use <code> tags) for free!
  • We decided that inline code would be sticky on the right, but not the left. In other words, when you type immediately adjacent to a piece of inline code on the right, the new text you type will be absorbed into the inline code. On the left, it won’t.
  • For a while, up and down arrow keys were behaving really strangely around inline code. When going down, instead of going past inline code onto the next line, the cursor would stop at the beginning of every piece of inline code, then the end, and then continue. Nick helped me debug this: turns out, when you hit the down arrow in our editor, we actually create a fake cursor that advances character by character looking for the correct place to put the cursor on the next line. When the vertical location of the current position is not the same as that of the previous position, we assume we’re on a new line. But since inline code is rendered in a different font and different size than regular text, advancing into a piece of inline code was making us think we had reached a new line when we hadn’t. We fixed this by making the vertical position comparison a little fuzzier (anything within 10px counts as the same line).

Future Work

There’s one more edge case we identified, but decided not to address for now: the blinking cursor doesn’t always reflect stickiness correctly at the edges of inline code.

For example, when you type a backtick to exit the mode, the cursor doesn’t move outside the gray padding box. There’s a whole other engineering blog post here, but TLDR: we try to maintain a one-to-one mapping between possible cursor positions and offsets in our paragraph model. But when you’re on the right edge of inline code, we would ideally have two cursor positions, one inside the markup and one outside. These two cursor positions map to the same offset in the paragraph, which causes all sorts of problems.

Koop drew this picture, which I will probably use to explain this problem forever.

Someday we will come back and fix this.

Cool. Now you know everything I know. Go forth and inline code! 🎉



Source link