Neovim + Treesitter
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
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.
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 🐱💻