Sorting Zettels in Neovim

Published Mon Feb 19 2024 Updated Tue Feb 20 2024

I have been working on sorting my alphanumeric zettelkasten notes to make my system more usable. I wrote some about this in my other Neovim posts, but now I have something that works!

How it works

My notes are each prefixed with an id that I create by hand. I started at 1 and increment when needed, but most of the time a note is related to another so I branch out. Each id can be extend by alternating between numbers and letters. For example a note related to the topic of 1 can be added as 1a, then a note related to 1a can be 1a1 or if 1a1 was already created I can make 1a2.

Since my notes are in Neorg I can use the dirman module to get a list of my notes and open files. By combining this data with Neovim’s Lua APIs I can create a custom view of my files.

Getting the files

First I need to get a list of my files. Since they are in a dedicated Neorg workspace this is easy with dirman’s get_norg_files method.

  (var neorg (require :neorg))
  (var dirman (neorg.modules.get_module :core.dirman))
  (var files (dirman.get_norg_files :zettel))

From here I need to get the identifier from each file so that they can be compared. In my format every character before the first hyphen is part of the identifier. This can be selected with string:match:

(fn file-to-key [filename]
  (var key (filename:match "^(.-)%-"))
  ;; if there is no match, pull id from before "." instead of "-"
  (when (= key nil) (set key (filename:match "^(.-)%.")))

I discovered I had broken my own rule by creating index.norg in my directory. To handle this exception I added an additional when statement where we fall back to the file’s entire name before the extension when there is no hyphen.

With the list of identifiers I can begin sorting. To compare identifiers I decided to break each one into a list of segments by type: numbers or letters.

(fn key-to-list [path]
  (var key (path-to-filename path))
  (var result [(string.sub key 0 0)])
  (var curr_type :num)
  (for [i 1 (length key)]
    (var len (length result))
    (var k (string.sub key i i))
    (var k_type "")
    (if (= (tonumber k) nil)
      (set k_type :alpha)
      (set k_type :num))
    (when (= k_type curr_type)
      (let [item (. result len)]
        (tset result len (.. item k))))
    (when (not (= k_type curr_type))
      (table.insert result k)
      (set curr_type k_type)))

The above function will turn 13a3c14 into a list containing { "13", "a", "3", "c", "14" } for comparisons we do not want numbers to be strings. I could add this to the loop above, but I write inefficient code so here I iterate again to parse numbers.

  ; parse numbers for later comparisons
  (set result (icollect [k v (pairs result)]
    (let [to_num (tonumber v)]
    (if (= to_num nil)
      v))

From here I created a Lua sort comparison function that steps through lists of keys to compare segments.

(fn an-compare [a b]
  (case [a b]
    (where [x y] (string.find x "index.norg")) (lua "return false")
    (where [x y] (string.find y "index.norg")) (lua "return true"))

  (var a-id (path-to-filename a))
  (var b-id (path-to-filename b))
  (var a-keys (key-to-list a))
  (var b-keys (key-to-list b))
  (var result nil)

  (each [k a-val (pairs a-keys) &until (not (= result nil))]
    (var b-val (. b-keys k))
    (case [a-val b-val]
      [x x] (set result nil)
      [x nil] (set result false)
      [nil y] (set result true)
      (where [x y] (> x y)) (set result false)
      (where [x y] (< x y)) (set result true)
    ))
  (if (= result nil) (set result false))
  result)

Connecting to Neovim

Now that I have my files and a way to sort them all I need is a way to see the files in my editor. Here I’ve called dirman and sorted the resulting files into a list. This list can be rendered to a new buffer using nvim_buf_set_lines. For now I am using vsplit to create the window for my new buffer.

(vim.cmd :vsplit)
(vim.api.nvim_buf_set_lines buf 0 -1 true sorted_files)
(vim.api.nvim_buf_set_name buf :zettels)
(vim.api.nvim_win_set_buf (vim.api.nvim_get_current_win) buf)

The resulting commands create a buffer with all my files sorted which is awesome! This is an interface though, I need a way to interact with the file names. To do this I can set up a custom keymap just for the buffer I created with nvim_buf_set_keymap.

(vim.api.nvim_buf_set_keymap buf :n "<Cr>" "" { :callback open-zettel })

This function was a little difficult to figure out. The opts parameter can be used to provide a lua callback function that is run instead of a vim cmd. My open-zettel function opens the file by name using dirman.

(fn open-zettel []
  (var neorg (require :neorg))
  (var dirman (neorg.modules.get_module :core.dirman))
  (var line (vim.api.nvim_get_current_line))
  (dirman.open_file :zettel line))

dirman.open_file is helpful here to avoid worrying about file paths at all. Since each line is just the file’s name nvim_get_current_line returns the file name.

Heavy on the code in this one, but I plan to keep working on this code to simplify it while I work on my lisp skills. Really excited to have my files sorted in an easier to view order!