Fine, I'll build my own text editor
Fine, Iâll build my own text editor!
âThey donât make âem like Sublime Text anymoreâ resonated with a lot of folk. Software these days is garbage. That got me thinking; Iâm good at building garbage!
Why canât I build my own text editor?
VS Code is built upon Monaco Editor which is a <div> soup hellscape. I was late to the VS Code train because for years my Intel inside⢠Mac was too slow. That issue was resolved when I bought Apple silicon. If thatâs the standard I have a lot of room to make mistakes.
Canvas
My first experiment renders everything on a <canvas> element.
You canât tell, but your CPU is doing a lot of work to render that picture at 60â120 frames per second. Lack of interactivity is an obvious problem for a text editor.
I made a list of the âminimum viableâ features and implemented them.
- Pointer down to position text cursor
- Arrow keys to move text cursor
- Highlight current line
- Type to enter text
- Fancy cursor animation
This next demo is interactive, click around and type.
Before you @ me about Vim bindings: shut up, Iâve got more pressing issues. Canvas gives me nothing for free. Amongst many desirable features, Iâm missing:
- Text selection
- Undo/redo history
- Multi-line paste
- Overflow scrolling
That last one is critical. Life is too short to implement custom elastic scrollbars. I decided to cheat and use native browser overflow on a hidden element. A <div> is sized to match the canvas text and the scroll position is used to calculate render offsets on the canvas.
Iâm pleased with how thatâs coming along but Iâm also disheartened because <canvas> is entirely inaccessible. I could continue to add text selection and other features but Iâm not solving the fundamental accessibility issue.
I had a better idea.
Content editable
Instead of rendering text on the <canvas> I can just render it natively in the overflow <div> and make it editable with a contenteditable attribute. That attribute has a plaintext-only value that is perfect for code. All content remains within a single text node.
<div
contenteditable="plaintext-only"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
translate="no">
<!--textgoeshere-->
</div>Attributes like spellcheck must be disabled to avoid input latency spikes. Want to guess how many days it took me to discover that fix? Days!
Using contenteditable gives native text selection and undo history etc. So much accessibility goodness is wired up for free by the browser.
The Selection API provides metrics I use to continue rendering a custom text cursor. ::selection is available so I can style that too. Iâve set the native caret-color invisible, which is probably a no-no.
The contenteditable technique is promising but Iâve noticed strange performance issues beyond a certain character count. Chromium browsers perform worse than WebKit and whatever Firefox is now but itâs unpredictable.
Textarea
Instead of plaintext contenteditable would a simple <textarea> be viable? In short: yes. Turns out a <textarea> is far more performant for longer text.
In this final demo Iâve added syntax highlighting too.
My original plan was to use custom ::highlight on the contenteditable element. <textarea> canât use CSS highlights so a third layer was required. For demo purposes I added some <div> soup for the visible lines to apply MicroLighter.
Edit: Iâm told the new OpaqueRange API unlocks custom highlights for <textarea> â neat!
Edit 2: and the EditContext API improves <canvas> input.
Too many CSS highlights are another performance bottleneck. A more robust solution would be to use Tree-sitter to generate a syntax tree and walk that to generate highlights for only visible lines. I was hoping to avoid virtualised scrolling entirely but I could improve it using the inverse sticky technique. Or I can go back to contenteditable because the file sizes Iâd be editing donât hit the performance wall.
Anyway, looking good, right?
Looks like 90% of a text editor with 1% of the features. From here itâs pretty straight forward to draw the rest of the owl. Iâm tempted to keep drawing but then I think about all the little things like tab indentation. Right now I just hijack the tab key to insert two spacesâ¦
My demos above are unoptimised and far from perfectly accessible but at least Iâm not starting from a losing position. Rendering on <canvas> would be a nightmare.
Iâm filing this project away for a rainy day.
JavaScript strings and text ranges work with UTF-16 code units. Itâs easy to naively introduce bugs. Iâm sure my demos are full of them. Iâll leave with a code example to nerd snipe.
"ðâð©".length;//5
[..."ðâð©"].length;//3
constsegmenter=newIntl.Segmenter("en",{granularity:"grapheme"});
[...segmenter.segment("ðâð©")].length;//1
Source: hackernews