nix-configs

Personal NixOS and home-manager configuration files
Log | Files | Refs

neovim-sortblock.nix (5381B)


      1 { lib, vimUtils, writeText, runCommand }:
      2 
      3 let
      4   sortblockLua = writeText "sortblock.lua" ''
      5     -- sortblock.lua
      6     -- Sort + dedupe marked blocks of text on save.
      7     --
      8     -- Wrap lines in markers:
      9     --   # sort:start
     10     --   dog
     11     --   cat
     12     --   cat
     13     --   # sort:end
     14     --
     15     --
     16     -- Also supports flags, eh: sort:start reverse nodedupe case
     17     local M = {}
     18     M.config = {
     19       start_marker = "sort:start",
     20       end_marker = "sort:end",
     21       pattern = "*",
     22       ignorecase = true,
     23       dedupe = true,
     24       reverse = false,
     25       strip_blanks = true,
     26     }
     27 
     28     -- Flags accepted on the start-marker line, mapped to config overrides.
     29     local FLAGS = {
     30       case = { ignorecase = false },
     31       nocase = { ignorecase = true },
     32       reverse = { reverse = true },
     33       nodedupe = { dedupe = false },
     34       dedupe = { dedupe = true },
     35       keepblanks = { strip_blanks = false },
     36     }
     37 
     38     local function opts_for_block(base, tail)
     39       local opts = vim.tbl_extend("force", {}, base)
     40       for word in (tail or ""):gmatch("%S+") do
     41         local override = FLAGS[word:lower()]
     42         if override then opts = vim.tbl_extend("force", opts, override) end
     43       end
     44       return opts
     45     end
     46 
     47     local function key_of(line, opts)
     48       local k = vim.trim(line)
     49       if opts.ignorecase then k = k:lower() end
     50       return k
     51     end
     52 
     53     -- Take the raw lines of a block, return the sorted/deduped version.
     54     local function transform(lines, opts)
     55       local out, seen = {}, {}
     56       for _, line in ipairs(lines) do
     57         local trimmed = vim.trim(line)
     58         if not (opts.strip_blanks and trimmed == "") then
     59           local k = key_of(line, opts)
     60           if not (opts.dedupe and seen[k]) then
     61             seen[k] = true
     62             out[#out + 1] = line
     63           end
     64         end
     65       end
     66 
     67       table.sort(out, function(a, b)
     68         local ka, kb = key_of(a, opts), key_of(b, opts)
     69         if ka == kb then return vim.trim(a) < vim.trim(b) end -- stable-ish tiebreak
     70         if opts.reverse then return ka > kb end
     71         return ka < kb
     72       end)
     73 
     74       return out
     75     end
     76 
     77     local function same(a, b)
     78       if #a ~= #b then return false end
     79       for i = 1, #a do
     80         if a[i] ~= b[i] then return false end
     81       end
     82       return true
     83     end
     84 
     85     -- Find all blocks, returns list
     86     local function find_blocks(lines, cfg)
     87       local blocks, open, open_opts = {}, nil, nil
     88       for i, line in ipairs(lines) do
     89         local s, e = line:find(cfg.start_marker, 1, true)
     90         if s and not open then
     91           open = i
     92           open_opts = opts_for_block(cfg, line:sub(e + 1))
     93         elseif line:find(cfg.end_marker, 1, true) and open then
     94           if i > open + 1 then
     95             blocks[#blocks + 1] = { first = open, last = i - 2, opts = open_opts }
     96           end
     97           open, open_opts = nil, nil
     98         end
     99       end
    100       return blocks
    101     end
    102 
    103     function M.sort_buffer(bufnr)
    104       bufnr = (bufnr == nil or bufnr == 0) and vim.api.nvim_get_current_buf() or bufnr
    105       if not vim.api.nvim_buf_is_loaded(bufnr) then return end
    106 
    107       local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
    108       local blocks = find_blocks(lines, M.config)
    109       if #blocks == 0 then return end
    110 
    111       -- Remember cursor positions
    112       local views = {}
    113       for _, win in ipairs(vim.api.nvim_list_wins()) do
    114         if vim.api.nvim_win_get_buf(win) == bufnr then
    115           views[win] = vim.api.nvim_win_get_cursor(win)
    116         end
    117       end
    118 
    119       -- Apply backwards so line numbers are valid
    120       for i = #blocks, 1, -1 do
    121         local b = blocks[i]
    122         local original = vim.list_slice(lines, b.first + 1, b.last + 1)
    123         local sorted = transform(original, b.opts)
    124         if not same(original, sorted) then
    125           vim.api.nvim_buf_set_lines(bufnr, b.first, b.last + 1, false, sorted)
    126         end
    127       end
    128 
    129       local count = vim.api.nvim_buf_line_count(bufnr)
    130       for win, pos in pairs(views) do
    131         if vim.api.nvim_win_is_valid(win) then
    132           vim.api.nvim_win_set_cursor(win, { math.min(pos[1], count), pos[2] })
    133         end
    134       end
    135     end
    136 
    137     function M.setup(user_config)
    138       M.config = vim.tbl_extend("force", M.config, user_config or {})
    139 
    140       local group = vim.api.nvim_create_augroup("SortBlockOnSave", { clear = true })
    141       vim.api.nvim_create_autocmd("BufWritePre", {
    142         group = group,
    143         pattern = M.config.pattern,
    144         callback = function(args) M.sort_buffer(args.buf) end,
    145       })
    146 
    147       vim.api.nvim_create_user_command("SortBlocks", function()
    148         M.sort_buffer(0)
    149       end, { desc = "Sort and dedupe all marked blocks in this buffer" })
    150     end
    151 
    152     return M
    153   '';
    154 in
    155 vimUtils.buildVimPlugin {
    156   pname = "sortblock";
    157   version = "1.0.0";
    158 
    159   # buildVimPlugin expects a source tree laid out the way Neovim's
    160   # runtimepath wants it, so wrap the embedded lua string in lua/.
    161   src = runCommand "sortblock-src" { } ''
    162     mkdir -p $out/lua
    163     cp ${sortblockLua} $out/lua/sortblock.lua
    164   '';
    165 
    166   # Fails the build if `require("sortblock")` errors — catches syntax
    167   # mistakes at rebuild time instead of at startup.
    168   # Remove this if your nixpkgs is older than 24.05.
    169   nvimRequireCheck = "sortblock";
    170 
    171   meta = {
    172     description = "Sort and dedupe marked blocks of text on save";
    173     license = lib.licenses.mit;
    174     platforms = lib.platforms.all;
    175   };
    176 }