Neovim + Treesitter
2023-01-04
I've been watching some of TJ's recent YouTube videos about neovim and customization with treesitter and lua scripting. I really love the idea of a PDE (personalized development environment). It might not be for everybody, but if you like to tinker with things, the current neovim ecosystem is a dream come true. I want to share the details of a small customization that I hacked together.
I use a markdown style TODO lists regularly. The simplicity and portability of plain text is great, and for small projects and personal lists, I find it to be the best option for me. A TODO list in markdown looks something like this:
# TODO
- [ ] Unit test for message encoding
- [ ] Cleanup logging feature
- [ ] Test behaviour with multiple instances of plugin running.
- [ ] When connecting send the server some info about which track the plugin is
attached to (if possible)
- [x] Test on Windows
- works in Waveform
- maybe does not work on Audacity
- [x] Get audio sample rate from API and send over socket after initial
connection.
- [x] Attempt reconnection any time the socket is not open
- [x] Define a more strict binary "wire" format for the streaming data
- [x] Implement some error handling strategy.
- [x] Research if there is a safe way to do the [f32] -> [u8] conversion. It
also seems like we're currently leaving byte order up to chance.
At work, I keep a big TODO file and make a new heading for each day where I can keep track of what I've worked on and what I want to work on.
I've had a couple of past brushes with treesitter (and treesitter playground), and with TJ's video's on my mind I suddenly decided that it was preposterous that my todo items didn't grey out and get a strike once I marked them off! I knew this should be possible to achieve, but it took some exploring to get everything working.
The first step, is to create treesitter query captures for a whole checked or
unchecked list item. The builtin rules for markdown do provide a capture for the
checkbox, but it doesn't include the rest of the text in the list item. The
:TSHighlightCapturesUnderCursor
command from treesitter playground great for
finding the names of existing captures.
Using :TSPlayground we find that the AST for a checkbox list looks like this:
list [1, 0] - [18, 0]
list_item [1, 0] - [2, 0]
list_marker_minus [1, 0] - [1, 2]
task_list_marker_unchecked [1, 2] - [1, 5]
paragraph [1, 6] - [2, 0]
inline [1, 6] - [1, 36]
list_item [2, 0] - [3, 0]
list_marker_minus [2, 0] - [2, 2]
task_list_marker_unchecked [2, 2] - [2, 5]
paragraph [2, 6] - [3, 0]
inline [2, 6] - [2, 29]
We want to make a capture for a @checked_list_item and an @unchecked_list_item. Luckily this is super easy to do with a tree sitter query:
;; extends
(list_item (task_list_marker_unchecked)) @unchecked_list_item
(list_item (task_list_marker_checked)) @checked_list_item
Since our capture name is outside of the list_item group, it means we capture the whole list item, instead of just the checkbox.
To make these captures available for use in highlighting, we can put them into a
file ~/.config/nvim/after/queries/markdown/highlights.scm
. The ;; extends
comment is necessary to merge the custom rules into the builtin rules.
Now, we can do custom syntax highlighting with a vim script command like:
highlight @unchecked_list_item guifg=#F8F8F2
highlight @checked_list_item guifg=#375749 gui=strikethrough
highlight @text.todo.unchecked guifg=#F8F8F2
highlight @text.todo.checked guifg=#375749
I put this into ~/.config/after/syntax/markdown.vim
. There's some goofy
builtin highlighting rules for @text.todo.checked and @text.todo.uncheked, so I
override them so that they're the same as the rest of line.
And voila!
This is a pretty small thing, but it's immensely more satisfying to check off items now, and even more so, knowing that I hacked it together 🐱💻