Portrait of David Findley

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!

A markdown todo list, the checked-off items are greyed out and styled with strikethrough.

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 🐱‍💻