Neovim sign gutter, designed to be mostly VCS agnostic
at experimental_diffthis 292 lines 7.9 kB view raw
1local M = {} 2 3local diff = require "vcsigns.diff" 4local fold = require "vcsigns.fold" 5local hunkops = require "vcsigns.hunkops" 6local repo = require "vcsigns.repo" 7local sign = require "vcsigns.sign" 8local state = require "vcsigns.state" 9local util = require "vcsigns.util" 10local updates = require "vcsigns.updates" 11 12local function _set_buflocal_autocmds(bufnr) 13 local group = vim.api.nvim_create_augroup("VCSigns", { clear = false }) 14 15 -- Clear existing autocommands in this buffer only. 16 vim.api.nvim_clear_autocmds { buffer = bufnr, group = group } 17 18 -- Expensive update on certain events. 19 local events = { 20 "BufEnter", 21 "WinEnter", 22 "BufWritePost", 23 "CursorHold", 24 "CursorHoldI", 25 "FocusGained", 26 "ShellCmdPost", 27 "VimResume", 28 } 29 vim.api.nvim_create_autocmd(events, { 30 group = group, 31 buffer = bufnr, 32 callback = function() 33 if not state.get(bufnr).vcs.detecting then 34 updates.deep_update(bufnr) 35 end 36 end, 37 desc = "VCSigns refresh and update hunks", 38 }) 39 40 -- Cheaper update on some frequent events. 41 local frequent_events = { 42 "TextChanged", 43 "TextChangedI", 44 "BufModifiedSet", 45 "InsertLeave", 46 } 47 vim.api.nvim_create_autocmd(frequent_events, { 48 group = group, 49 buffer = bufnr, 50 callback = function() 51 if not state.get(bufnr).vcs.detecting then 52 updates.shallow_update(bufnr) 53 end 54 end, 55 desc = "VCSigns refresh and update hunks", 56 }) 57end 58 59--- Start VCSigns for the given buffer, forcing a VCS detection. 60---@param bufnr integer The buffer number. 61function M.start(bufnr) 62 -- Clear existing state. 63 local s = state.get(bufnr) 64 s.vcs.detecting = nil 65 s.vcs.vcs = nil 66 67 local vcs = repo.detect_vcs(bufnr) 68 s.vcs.detecting = false 69 if not vcs then 70 util.verbose "No VCS detected" 71 return 72 end 73 util.verbose("Detected VCS " .. vcs.name) 74 s.vcs.vcs = vcs 75 76 _set_buflocal_autocmds(bufnr) 77 updates.deep_update(bufnr, true) 78end 79 80--- Start VCSigns for the given buffer, but skip if detection was already done. 81---@param bufnr integer The buffer number. 82function M.start_if_needed(bufnr) 83 if state.get(bufnr).vcs.vcs == nil then 84 M.start(bufnr) 85 end 86end 87 88---@param bufnr integer The buffer number. 89function M.stop(bufnr) 90 -- Clear autocommands. 91 local group = vim.api.nvim_create_augroup("VCSigns", { clear = false }) 92 vim.api.nvim_clear_autocmds { buffer = bufnr, group = group } 93 94 -- Clear signs. 95 sign.clear_signs(bufnr) 96 97 -- Clear state. 98 state.clear(bufnr) 99end 100 101local last_target_notification = nil 102 103local function _target_change_message() 104 local msg = 105 string.format("Now diffing against HEAD~%d", vim.g.vcsigns_target_commit) 106 last_target_notification = vim.notify( 107 msg, 108 vim.log.levels.INFO, 109 { title = "VCSigns", replace = last_target_notification } 110 ) 111end 112 113---@param bufnr integer The buffer number. 114---@param steps integer Number of steps to go back in time. 115function M.target_older_commit(bufnr, steps) 116 vim.g.vcsigns_target_commit = vim.g.vcsigns_target_commit + steps 117 _target_change_message() 118 -- Target has changed, trigger a full update. 119 updates.deep_update(bufnr, true) 120end 121 122---@param bufnr integer The buffer number. 123---@param steps integer Number of steps to go forward in time. 124function M.target_newer_commit(bufnr, steps) 125 local new_target = vim.g.vcsigns_target_commit - steps 126 if new_target >= 0 then 127 vim.g.vcsigns_target_commit = new_target 128 _target_change_message() 129 -- Target has changed, trigger a full update. 130 updates.deep_update(bufnr, true) 131 else 132 last_target_notification = vim.notify( 133 "No timetravel! Cannot diff against HEAD~" .. new_target, 134 vim.log.levels.WARN, 135 { 136 title = "VCSigns", 137 replace = last_target_notification, 138 } 139 ) 140 end 141end 142 143local function _hunk_navigation(bufnr, count, forward) 144 if vim.o.diff then 145 vim.cmd("normal! " .. (forward and "]c" or "[c")) 146 return 147 end 148 local lnum = vim.fn.line "." 149 local hunks = state.get(bufnr).diff.hunks 150 local hunk 151 if forward then 152 hunk = hunkops.next_hunk(lnum, hunks, count) 153 else 154 hunk = hunkops.prev_hunk(lnum, hunks, count) 155 end 156 if hunk then 157 vim.cmd "normal! m`" 158 vim.api.nvim_win_set_cursor(0, { hunk.plus_start, 0 }) 159 end 160end 161 162---@param bufnr integer The buffer number. 163---@param count integer The number of hunks ahead. 164function M.hunk_next(bufnr, count) 165 return _hunk_navigation(bufnr, count, true) 166end 167 168---@param bufnr integer The buffer number. 169---@param count integer The number of hunks ahead. 170function M.hunk_prev(bufnr, count) 171 return _hunk_navigation(bufnr, count, false) 172end 173 174---@param bufnr integer The buffer number. 175---@param range integer[] The range of lines to consider for the hunks. 176---@return Hunk[] Hunks in the specified range. 177local function _hunks_in_range(bufnr, range) 178 local hunks = state.get(bufnr).diff.hunks 179 ---@type Hunk[] 180 local hunks_in_range = {} 181 for _, hunk in ipairs(hunks) do 182 local l = hunkops.hunk_visual_start(hunk) 183 local r = l + hunkops.hunk_visual_size(hunk) - 1 184 if l <= range[2] and r >= range[1] then 185 table.insert(hunks_in_range, hunk) 186 end 187 end 188 return hunks_in_range 189end 190 191---@param bufnr integer The buffer number. 192---@param range integer[]|nil The range of lines to undo hunks in. 193function M.hunk_undo(bufnr, range) 194 if not range then 195 range = { vim.fn.line ".", vim.fn.line "v" } 196 end 197 table.sort(range) 198 local hunks_in_range = _hunks_in_range(bufnr, range) 199 200 if #hunks_in_range == 0 then 201 vim.notify( 202 "No hunks found in range " .. range[1] .. "-" .. range[2], 203 vim.log.levels.WARN, 204 { title = "VCSigns" } 205 ) 206 return 207 end 208 209 -- Undo the hunks in reverse order to make sure numbering is correct. 210 table.sort(hunks_in_range, function(a, b) 211 return a.plus_start > b.plus_start 212 end) 213 for _, hunk in ipairs(hunks_in_range) do 214 local start = hunk.plus_start - 1 215 if hunk.plus_count == 0 then 216 -- Special case of undoing a pure deletion. 217 -- To append after `start` we insert before `start + 1`. 218 start = start + 1 219 end 220 vim.api.nvim_buf_set_lines( 221 bufnr, 222 start, 223 start + hunk.plus_count, 224 true, 225 hunk.minus_lines 226 ) 227 end 228 updates.shallow_update(bufnr) 229end 230 231---@param bufnr integer The buffer number. 232function M.toggle_hunk_diff(bufnr) 233 vim.b[bufnr].vcsigns_show_hunk_diffs = 234 not vim.b[bufnr].vcsigns_show_hunk_diffs 235 updates.shallow_update(bufnr) 236end 237 238---@param bufnr integer The buffer number. 239function M.toggle_fold(bufnr) 240 fold.toggle(bufnr) 241end 242 243function M.diffthis(bufnr) 244 local diff_win = vim.b[bufnr].vcsigns_diff_win 245 if diff_win then 246 vim.api.nvim_win_close(diff_win, true) 247 vim.b[bufnr].vcsigns_diff_win = nil 248 vim.cmd "diffoff" 249 return 250 end 251 252 local s = state.get(bufnr) 253 local base_lines = s.diff.old_lines 254 255 -- Open a diff buffer with the base text. 256 if not base_lines or #base_lines == 0 then 257 vim.notify( 258 "Could not get base text from VCS", 259 vim.log.levels.ERROR, 260 { title = "VCSigns" } 261 ) 262 return 263 end 264 local diff_buf = vim.api.nvim_create_buf(false, true) 265 vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, base_lines) 266 267 -- Open the diff buffer in a new window. 268 local win = vim.api.nvim_open_win(diff_buf, false, { 269 split = "right", 270 win = 0, 271 }) 272 -- TODO(algmyr): Store this in the state module. 273 vim.b[bufnr].vcsigns_diff_win = win 274 275 -- Sync the filetype and syntax. 276 vim.bo[diff_buf].filetype = vim.bo[bufnr].filetype 277 278 vim.bo[diff_buf].buftype = "nofile" 279 vim.bo[diff_buf].bufhidden = "wipe" 280 vim.bo[diff_buf].swapfile = false 281 vim.bo[diff_buf].modifiable = false 282 vim.wo[win].foldcolumn = "0" 283 284 -- Run diffthis in both windows. 285 local current = vim.api.nvim_get_current_win() 286 vim.api.nvim_set_current_win(win) 287 vim.cmd "diffthis" 288 vim.api.nvim_set_current_win(current) 289 vim.cmd "diffthis" 290end 291 292return M