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 }