Neovim sign gutter, designed to be mostly VCS agnostic
at experimental_diffthis 168 lines 5.1 kB view raw
1local M = {} 2 3local util = require "vcsigns.util" 4local repo_common = require "vcsigns.repo_def.common" 5 6--- List of VCSs, in priority order. 7---@type VcsInterface[] 8M.vcs = { 9 require "vcsigns.repo_def.jj", 10 require "vcsigns.repo_def.git", 11 require "vcsigns.repo_def.hg", 12} 13 14--- Register a custom VCS implementation. 15--- The VCS will be added at the beginning of the detection priority list. 16---@param vcs Vcs The VCS implementation to register. 17function M.register_vcs(vcs) 18 table.insert(M.vcs, 1, vcs) 19end 20 21--- Get the absolute path of the file in the buffer. 22--- @param bufnr integer The buffer number. 23--- @return string The absolute path of the file. 24local function _get_path(bufnr) 25 return vim.fn.resolve( 26 vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ":p") 27 ) 28end 29 30local function _target_commit() 31 return vim.g.vcsigns_target_commit or 0 32end 33 34--- Get the target for the current buffer. 35---@param bufnr integer The buffer number. 36---@param vcs Vcs The version control system. 37---@return Target 38local function _get_target(bufnr, vcs) 39 local path = _get_path(bufnr) 40 assert(vcs.root, "VCS root must be set") 41 42 -- Make file path relative to repo root. 43 local root = vcs.root 44 local file 45 if path:sub(1, #root) == root then 46 file = path:sub(#root + 2) -- +2 to skip root and the path separator. 47 else 48 -- File is outside the repo root. 49 error(string.format("File %s is not under repo root %s", path, root)) 50 end 51 52 return { 53 commit = _target_commit(), 54 file = file, 55 path = path, 56 } 57end 58 59local function _is_available(vcs) 60 local programs = { 61 vcs.detect.cmd()[1], 62 } 63 for _, program in ipairs(programs) do 64 if vim.fn.executable(program) == 0 then 65 util.verbose("VCS command not executable: " .. program) 66 return false 67 end 68 end 69 return true 70end 71 72---@param bufnr integer The buffer number. 73---@param vcs Vcs The version control system to use. 74---@param target Target The target for the VCS command. 75---@param cb fun(lines: string[]|nil) Callback function to handle the output. 76local function _show_file_impl(bufnr, vcs, target, cb) 77 util.run_with_timeout(vcs.show.cmd(target), { cwd = vcs.root }, function(out) 78 -- If the buffer was deleted, bail. 79 if not vim.api.nvim_buf_is_valid(bufnr) then 80 util.verbose "Buffer no longer valid, skipping diff" 81 return nil 82 end 83 local old_contents = out.stdout 84 if not old_contents then 85 util.verbose "No output from command, skipping diff" 86 return nil 87 end 88 if not vcs.show.check(out) then 89 util.verbose "VCS decided to not produce a file, skipping diff" 90 return nil 91 end 92 93 -- Convert to lines as early as possible. 94 if old_contents == "" then 95 -- Non-existent file. 96 cb {} 97 end 98 if old_contents:sub(-1) == "\n" then 99 -- Trim trailing newline if present. 100 old_contents = old_contents:sub(1, -2) 101 end 102 local old_lines = vim.split(old_contents, "\n", { plain = true }) 103 cb(old_lines) 104 end) 105end 106 107--- Get the relevant file contents of the file according to the VCS. 108---@param bufnr integer The buffer number. 109---@param vcs Vcs The version control system to use. 110---@param cb fun(lines: string[]|nil) Callback function to handle the output. 111function M.show_file(bufnr, vcs, cb) 112 local target = _get_target(bufnr, vcs) 113 if vcs.resolve_rename then 114 util.verbose("Resolving rename for " .. target.file) 115 util.run_with_timeout( 116 vcs.resolve_rename.cmd(target), 117 { cwd = vcs.root }, 118 function(out) 119 -- If the buffer was deleted, bail. 120 if not vim.api.nvim_buf_is_valid(bufnr) then 121 util.verbose "Buffer no longer valid, skipping" 122 return 123 end 124 local resolved_file = vcs.resolve_rename.extract(out, target) 125 if resolved_file then 126 util.verbose( 127 "Rename found: " .. target.file .. " -> " .. resolved_file 128 ) 129 vim.b[bufnr].vcsigns_resolved_rename = 130 { to = target.file, from = resolved_file } 131 target.file = resolved_file 132 end 133 _show_file_impl(bufnr, vcs, target, cb) 134 end 135 ) 136 else 137 _show_file_impl(bufnr, vcs, target, cb) 138 end 139end 140 141--- Detect the VCS for the current buffer. 142---@param bufnr integer The buffer number. 143---@return Vcs|nil The detected VCS or nil if no VCS was detected. 144function M.detect_vcs(bufnr) 145 local file_dir = util.file_dir(bufnr) 146 -- If the file dir does not exist, things will end poorly. 147 if vim.fn.isdirectory(file_dir) == 0 then 148 util.verbose("File directory does not exist: " .. file_dir) 149 return nil 150 end 151 for _, vcs in ipairs(M.vcs) do 152 util.verbose("Trying to detect VCS " .. vcs.name) 153 if not _is_available(vcs) then 154 util.verbose("VCS " .. vcs.name .. " is not available") 155 goto continue 156 end 157 local detect_cmd = vcs.detect.cmd() 158 local res = util.run_with_timeout(detect_cmd, { cwd = file_dir }):wait() 159 local detection_result = vcs.detect.check(res) 160 if detection_result.detected then 161 return repo_common.vcs_with_root(vcs, detection_result.root) 162 end 163 ::continue:: 164 end 165 return nil 166end 167 168return M