Neovim sign gutter, designed to be mostly VCS agnostic
at experimental_diffthis 299 lines 8.5 kB view raw
1local M = {} 2 3M.actions = require "vcsigns.actions" 4local sign = require "vcsigns.sign" 5 6--- Decorator to wrap a function that takes no arguments. 7local function _no_args(fun) 8 local function wrap(bufnr, arg) 9 local args = vim.list_slice(arg.fargs, 2) 10 if #args > 0 then 11 error "This VCSigns command does not take any arguments" 12 end 13 fun(bufnr) 14 end 15 return wrap 16end 17 18local function _with_range(fun) 19 local function wrap(bufnr, arg) 20 local args = vim.list_slice(arg.fargs, 2) 21 if #args > 0 then 22 error "This VCSigns command does not take any arguments" 23 end 24 local range = { arg.line1, arg.line2 } 25 fun(bufnr, range) 26 end 27 return wrap 28end 29 30local function _with_count(fun) 31 local function wrap(bufnr, arg) 32 local args = vim.list_slice(arg.fargs, 2) 33 if #args > 1 then 34 error "This VCSigns command takes at most one argument" 35 end 36 local count = tonumber(args[1]) or 1 37 fun(bufnr, count) 38 end 39 return wrap 40end 41 42local command_map = { 43 start = _no_args(M.actions.start), 44 stop = _no_args(M.actions.stop), 45 newer = _with_count(M.actions.target_newer_commit), 46 older = _with_count(M.actions.target_older_commit), 47 fold = _no_args(M.actions.toggle_fold), 48 hunk_next = _with_count(M.actions.hunk_next), 49 hunk_prev = _with_count(M.actions.hunk_prev), 50 hunk_undo = _with_range(M.actions.hunk_undo), 51 hunk_diff = _no_args(M.actions.toggle_hunk_diff), 52 diffthis = _no_args(M.actions.diffthis), 53} 54 55local function _command(arg) 56 local bufnr = vim.api.nvim_get_current_buf() 57 local cmd = arg.fargs[1] 58 local fun = command_map[cmd] 59 if not fun then 60 error("Unknown VCSigns command: " .. cmd) 61 return 62 end 63 fun(bufnr, arg) 64end 65 66local default_config = { 67 -- Enable in all buffers by default. 68 auto_enable = true, 69 -- Initial target commit to show. 70 -- 0 means the current commit, 1 is one commit before that, etc. 71 target_commit = 0, 72 -- Shot the number of deleted lines in the sign column. 73 show_delete_count = true, 74 -- Highlight the number in the sign column. 75 highlight_number = false, 76 -- Signs to use for different types of changes. 77 signs = { 78 text = { 79 add = "", 80 change = "", 81 delete_below = "", 82 delete_above = "", 83 delete_above_below = "🮀", -- Reconsider this symbol. 84 combined = nil, -- If set, use this instead of multiple combined signs. 85 }, 86 hl = { 87 add = "SignAdd", 88 change = "SignChange", 89 delete = "SignDelete", 90 combined = "SignCombined", 91 }, 92 priority = 5, 93 }, 94 -- By default multiple signs on one line are avoided by shifting 95 -- delete_below into a delete_above on the next line. 96 -- This can optionally be skipped. 97 skip_sign_decongestion = false, 98 -- Sizes of context to add fold levels for (order doesn't matter). 99 -- E.g. { 1, 3 } would mean one fold level with a context of 1 line, 100 -- and one fold level with a context of 3 lines: 101 -- level 102 -- | 3 | Context 2 103 -- | 4 | Context 2 104 -- | 5 | Context 1 105 -- | 6 | Context 1 106 -- | 7 | Context 0 107 -- | + 8 | Added 0 108 -- | + 9 | More Added 0 109 -- | 10 | Context 0 110 -- | 11 | Context 1 111 -- | 12 | Context 1 112 -- | 13 | Context 2 113 -- | 14 | Context 2 114 fold_context_sizes = { 1 }, 115 -- Diff options to use. 116 -- See `:help vim.text.diff()` for available algorithms. 117 diff_opts = { 118 algorithm = "histogram", 119 linematch = 60, 120 }, 121 fine_diff_opts = {}, 122 123 -- Skip diffing files with more than this many lines. 124 diff_max_lines = 10000, 125 -- Whether to try respecting .gitignore files. 126 -- This relies on the `git` command being available. 127 -- Works for git repos and git backed jj repos. 128 respect_gitignore = true, 129} 130 131local function _move_deprecated(tbl, old_key, new_key) 132 local function extract_nested(t, key_path) 133 local current = t 134 local keys = {} 135 for key in string.gmatch(key_path, "[^%.]+") do 136 table.insert(keys, key) 137 end 138 for i = 1, #keys - 1 do 139 local key = keys[i] 140 if current[key] == nil then 141 return nil 142 end 143 current = current[key] 144 end 145 if current[keys[#keys]] == nil then 146 return nil 147 end 148 local result = current[keys[#keys]] 149 current[keys[#keys]] = nil 150 return result 151 end 152 153 local function set_nested(t, key_path, value) 154 local current = t 155 local keys = {} 156 for key in string.gmatch(key_path, "[^%.]+") do 157 table.insert(keys, key) 158 end 159 for i = 1, #keys - 1 do 160 local key = keys[i] 161 if current[key] == nil then 162 current[key] = {} 163 end 164 current = current[key] 165 end 166 current[keys[#keys]] = value 167 end 168 169 local value = extract_nested(tbl, old_key) 170 if value ~= nil then 171 -- Defer notification to avoid interfering with setup. 172 vim.defer_fn(function() 173 vim.notify( 174 "VCSigns config: '" 175 .. old_key 176 .. "' is deprecated. Use '" 177 .. new_key 178 .. "' instead.", 179 vim.log.levels.INFO, 180 { title = "VCSigns" } 181 ) 182 end, 0) 183 set_nested(tbl, new_key, value) 184 end 185end 186 187function M.setup(user_config) 188 -- Migrate deprecated config options. 189 user_config = vim.deepcopy(user_config) 190 _move_deprecated( 191 user_config, 192 "signs.text.change_delete", 193 "signs.text.combined" 194 ) 195 _move_deprecated(user_config, "signs.hl.change_delete", "signs.hl.combined") 196 197 local config = vim.tbl_deep_extend("force", default_config, user_config or {}) 198 vim.g.vcsigns_show_delete_count = config.show_delete_count 199 vim.g.vcsigns_fold_context_sizes = config.fold_context_sizes 200 vim.g.vcsigns_diff_opts = config.diff_opts 201 vim.g.vcsigns_fine_diff_opts = config.fine_diff_opts 202 vim.g.vcsigns_highlight_number = config.highlight_number 203 vim.g.vcsigns_skip_sign_decongestion = config.skip_sign_decongestion 204 vim.g.vcsigns_target_commit = config.target_commit 205 vim.g.vcsigns_respect_gitignore = config.respect_gitignore 206 vim.g.vcsigns_diff_max_lines = config.diff_max_lines 207 208 sign.signs = config.signs 209 210 vim.api.nvim_create_user_command("VCSigns", _command, { 211 desc = "VCSigns command", 212 nargs = "*", 213 bar = true, 214 range = true, 215 complete = function(_, line) 216 if line:match "^%s*VCSigns %w+ " then 217 return {} 218 end 219 local prefix = line:match "^%s*VCSigns (%w*)" or "" 220 return vim.tbl_filter(function(key) 221 return key:find(prefix) == 1 222 end, vim.tbl_keys(command_map)) 223 end, 224 }) 225 226 if config.auto_enable then 227 -- Enable VCSigns for all buffers. 228 vim.api.nvim_create_autocmd("BufEnter", { 229 pattern = "*", 230 callback = function(args) 231 local bufnr = args.buf 232 -- Defer setup to work around buftype not being set yet: 233 -- https://github.com/neovim/neovim/issues/29419 234 vim.defer_fn(function() 235 -- Buffer might have been unloaded in the meantime. 236 if not vim.api.nvim_buf_is_valid(bufnr) then 237 return 238 end 239 if vim.bo[bufnr].buftype == "" then 240 M.actions.start_if_needed(bufnr) 241 end 242 end, 100) 243 end, 244 desc = "Auto-enable VCSigns on buffer read", 245 }) 246 end 247 248 -- Set default highlights with fallbacks to common groups for signs. 249 local function hl_fallbacks(hl_group, fallbacks) 250 for _, fallback in ipairs(fallbacks) do 251 local hl = vim.api.nvim_get_hl(0, { name = fallback }) 252 if next(hl) then 253 vim.api.nvim_set_hl(0, hl_group, { link = fallback, default = true }) 254 return 255 end 256 end 257 end 258 259 hl_fallbacks("SignAdd", { 260 "GitSignsAdd", 261 "GitGutterAdd", 262 "SignifySignAdd", 263 "DiffAddedGutter", 264 "Added", 265 "DiffAdd", 266 }) 267 hl_fallbacks("SignDelete", { 268 "GitSignsDelete", 269 "GitGutterDelete", 270 "SignifySignDelete", 271 "DiffRemovedGutter", 272 "Removed", 273 "DiffDelete", 274 }) 275 hl_fallbacks("SignChange", { 276 "GitSignsChange", 277 "GitGutterChange", 278 "SignifySignChange", 279 "DiffModifiedGutter", 280 "Changed", 281 "DiffChange", 282 }) 283 hl_fallbacks("SignCombined", { 284 "SignChangeDelete", 285 "GitSignsChangeDelete", 286 "SignChange", 287 }) 288 hl_fallbacks("SignDeleteFirstLine", { 289 "GitSignsTopdelete", 290 "SignDelete", 291 }) 292 293 hl_fallbacks("VcsignsDiffAdd", { "DiffAdd" }) 294 hl_fallbacks("VcsignsDiffDelete", { "DiffDelete" }) 295 hl_fallbacks("VcsignsDiffTextAdd", { "DiffText" }) 296 hl_fallbacks("VcsignsDiffTextDelete", { "DiffText" }) 297end 298 299return M