Neovim sign gutter, designed to be mostly VCS agnostic
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