Two years ago, I came across this excellent tutorial written by snaptoken. It walks through the process of building a small text editor in C. I don’t really enjoy following along with code tutorials verbatim, so I thought it would be a fun challenge to use snaptoken’s tutorial as a general reference and implement the text editor in Rust instead. I effectively treated the tutorial like a product manager and came away from each chapter with a new list of features to implement:
Chapter 2#
- Enable raw mode.
Write each
stdin
char tostdout
.Ctrl
indicates a carriage return.q
indicates that the user wants to quit.Ctrl-#
combinations (e.g.,Ctrl-Z
) should be ignored.
Chapter 3#
- Clear the screen on startup, shutdown, and on tick.
- Draw a tilde border on the left-hand side of the screen.
- Render a cursor in the first position of the first line.
- Replace
println!()
withwrite!()
s to aBufWriter
. - Display a welcome message at the bottom of the screen.
- Allow the user to move their cursor using arrow keys. Cursor info should be stored in global state. Don’t allow the cursor to move offscreen.
Page Up
andPage Down
can snap the cursor to the top and bottom of the screen.Home
andEnd
can snap the cursor to the beginning and end of the current line.
Chapter 4#
- If run with a filename argument, read the file into memory.
- Enable vertical scrolling through a long text file. Enable horizontal scrolling on long lines. (Basically, allow users to scroll offscreen if the text allows it.)
- Snap to the next line if the user scrolls past the end of a line. Snap to the previous line if the user scrolls left at the beginning of a line.
- Update
Page Up
andPage Down
to scroll up and down the entire page. - Display a status bar that includes the filename, # of lines in the file, and current line number.
If the file has no name, display
[No Name]
. - Render key binding hints in the status bar (e.g.,
Ctrl-Q = quit
). Display on startup and hide 5 seconds after the user presses a key.
Chapter 5#
- Allow saving to disk via
Ctrl-S
. - Track “dirtiness” in global state. The buffer is “dirty” if it has been modified since opening or saving the file.
- Require
Ctrl-Q
3x to confirm quitting with unsaved changes. - Implement
Backspace
. Backspacing at the beginning of a line should wrap the line onto the previous line. If there is no previous line, do nothing. - Implement
Delete
. Deleting at the end of a line should wrap onto the next line. If there is no next line, do nothing. - Implement
Enter
. This can insert new lines or split a line into two lines. - Prompt the user to input a filename when saving a new file.
- Allow
Backspace
in prompt input.
Chapter 6#
- Implement search with
Ctrl-F
.- The user inputs their query into the prompt bar. On each keypress, the cursor snaps to a string that contains their query substring.
- When search is cancelled with
Escape
, restore the cursor to its original position. - Users can browse through substring matches using arrow keys.
Left
/Up
to move to the previous match,Right
/Down
to move to the next match.
Chapter 7#
- Implement basic C syntax highlighting.
- Digits are red. Search results are blue. Strings are magenta. Single-line and multi-line comments are cyan. Types are green. Keywords are yellow.
- Syntax highlighting should only appear if the open filename ends with
.c
. - Search result highlighting should overwrite other highlighting. Original highlighting should be restored when search is cancelled.
Crates I used#
- termion: This looked like the simplest way to enable raw mode without C interop. (Avoiding C interop was my only self-imposed rule for this project; after all, I wanted to have fun 😋) Cursor rendering and text rendering also met all my requirements and was simple enough to use.
- termsize: I used this to get terminal window size on startup. (I never actually updated my implementation to handle mid-session window resizes…)
- unicode_segmentation: I used this to split strings on grapheme cluster boundaries instead of character boundaries.
This ensures that a character with a diacritic like
Γ€
is kept together as a single element instead of split apart (also allows the text editor to support other exotic Unicode input like emoji, Cyrillic, etc.)
My experience#
I had a heck of a time with this project overall. I dropped it twice: once in 2022 because I was intimidated by Chapter 6, and another time in mid 2023 because I was intimidated by Chapter 7 and sick of looking at my years old over-engineered code.
It didn’t take long for my code to diverge significantly from snaptoken’s code. It started with, “oh, well I can’t really do that in Rust, so I have to do it this other way instead,” and ended with, “yeahhh, my design is nothing like that, I have to do this differently”.
The final structure that I ended up with is mind-boggling, to be honest:
πsrc
β£ πbackend
β β£ cursor.rs
β β£ mod.rs
β β£ operations.rs
β β prompt.rs
β£ πdata
β β£ enums.rs
β β£ mod.rs
β β£ payload.rs
β β textrow.rs
β£ πgfx
β β£ controller.rs
β β£ mod.rs
β β render.rs
β£ input.rs
β£ main.rs
β utils.rs
The RenderController
owns a CursorHandler
and an OperationsHandler
.
The OperationsHandler
is a middle layer between the RenderController
, RenderDriver
, and PromptProcessor
. Lol?
This object-oriented approach must have been veryyy satisfying for me to build out in 2022, but it quickly became a rat king as I had to wrestle with issues like: “how do I pass data from the PromptProcessor to its parent’s parent RenderController!?!?” If implementing a text editor is truly a gateway drug into kernel development, then I overdosed.
Kilo is considered a simple project because it can be implemented with ~1000 lines of code. Running tokei on my repo tells a grim tale:
===================================================================
Language Files Lines Code Comments
===================================================================
C 1 28 18 4
Markdown 1 13 0 10
Rust 14 1897 1410 299
TOML 1 12 9 1
===================================================================
Total 17 1950 1437 314
===================================================================
You’re invited to gawk at my repo for yourself:
Rust kilo implementation
Over-abstraction aside, I had a lot of fun implementing all of these features. It’s easy to dream up a bunch of potential extensions to Kilo. I’ve had enough of text editor development for the time being 🙂 but I was tempted to implement automated testing. It was tedious to get through Chapter 6 + 7 when large code changes required me to manually check for regressions in all previously implemented features. Someday, I might try compiling my code to WASM or rebuilding with xterm-js-rs so I can get the app web-embeddable.