Regenerate nvim config
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,196 @@
|
||||
local M = {}
|
||||
|
||||
--- @class Gitsigns.Async_T
|
||||
--- @field _current Gitsigns.Async_T
|
||||
local Async_T = {}
|
||||
|
||||
-- Handle for an object currently running on the event loop.
|
||||
-- The coroutine is paused while this is active.
|
||||
-- Must provide methods cancel() and is_cancelled()
|
||||
--
|
||||
-- Handle gets updated on each call to a wrapped functions, so provide access
|
||||
-- to it via a proxy
|
||||
|
||||
-- Coroutine.running() was changed between Lua 5.1 and 5.2:
|
||||
-- - 5.1: Returns the running coroutine, or nil when called by the main thread.
|
||||
-- - 5.2: Returns the running coroutine plus a boolean, true when the running
|
||||
-- coroutine is the main one.
|
||||
--
|
||||
-- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
|
||||
--
|
||||
-- We need to handle both.
|
||||
|
||||
--- Store all the async threads in a weak table so we don't prevent them from
|
||||
--- being garbage collected
|
||||
--- @type table<thread,Gitsigns.Async_T>
|
||||
local handles = setmetatable({}, { __mode = 'k' })
|
||||
|
||||
--- Returns whether the current execution context is async.
|
||||
function M.running()
|
||||
local current = coroutine.running()
|
||||
if current and handles[current] then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function is_Async_T(handle)
|
||||
if
|
||||
handle
|
||||
and type(handle) == 'table'
|
||||
and vim.is_callable(handle.cancel)
|
||||
and vim.is_callable(handle.is_cancelled)
|
||||
then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--- Analogous to uv.close
|
||||
--- @param cb function
|
||||
function Async_T:cancel(cb)
|
||||
-- Cancel anything running on the event loop
|
||||
if self._current and not self._current:is_cancelled() then
|
||||
self._current:cancel(cb)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param co thread
|
||||
--- @return Gitsigns.Async_T
|
||||
function Async_T.new(co)
|
||||
local handle = setmetatable({}, { __index = Async_T })
|
||||
handles[co] = handle
|
||||
return handle
|
||||
end
|
||||
|
||||
--- Analogous to uv.is_closing
|
||||
--- @return boolean
|
||||
function Async_T:is_cancelled()
|
||||
return self._current and self._current:is_cancelled()
|
||||
end
|
||||
|
||||
--- @param func function
|
||||
--- @param callback? fun(...: any)
|
||||
--- @param ... any
|
||||
--- @return Gitsigns.Async_T
|
||||
local function run(func, callback, ...)
|
||||
local co = coroutine.create(func)
|
||||
local handle = Async_T.new(co)
|
||||
|
||||
local function step(...)
|
||||
local ret = { coroutine.resume(co, ...) }
|
||||
local stat = ret[1]
|
||||
|
||||
if not stat then
|
||||
local err = ret[2] --[[@as string]]
|
||||
error(
|
||||
string.format('The coroutine failed with this message: %s\n%s', err, debug.traceback(co))
|
||||
)
|
||||
end
|
||||
|
||||
if coroutine.status(co) == 'dead' then
|
||||
if callback then
|
||||
callback(unpack(ret, 2, table.maxn(ret)))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
--- @type integer, fun(...: any): any
|
||||
local nargs, fn = ret[2], ret[3]
|
||||
|
||||
assert(type(fn) == 'function', 'type error :: expected func')
|
||||
|
||||
local args = { select(4, unpack(ret)) }
|
||||
args[nargs] = step
|
||||
|
||||
local r = fn(unpack(args, 1, nargs))
|
||||
if is_Async_T(r) then
|
||||
--- @cast r Gitsigns.Async_T
|
||||
handle._current = r
|
||||
end
|
||||
end
|
||||
|
||||
step(...)
|
||||
return handle
|
||||
end
|
||||
|
||||
--- @param argc integer
|
||||
--- @param func function
|
||||
--- @param ... any
|
||||
--- @return any ...
|
||||
function M.wait(argc, func, ...)
|
||||
-- Always run the wrapped functions in xpcall and re-raise the error in the
|
||||
-- coroutine. This makes pcall work as normal.
|
||||
local function pfunc(...)
|
||||
local args = { ... } --- @type any[]
|
||||
local cb = args[argc]
|
||||
args[argc] = function(...)
|
||||
cb(true, ...)
|
||||
end
|
||||
xpcall(func, function(err)
|
||||
cb(false, err, debug.traceback())
|
||||
end, unpack(args, 1, argc))
|
||||
end
|
||||
|
||||
local ret = { coroutine.yield(argc, pfunc, ...) }
|
||||
|
||||
local ok = ret[1]
|
||||
if not ok then
|
||||
--- @type string, string
|
||||
local err, traceback = ret[2], ret[3]
|
||||
error(string.format('Wrapped function failed: %s\n%s', err, traceback))
|
||||
end
|
||||
|
||||
return unpack(ret, 2, table.maxn(ret))
|
||||
end
|
||||
|
||||
--- Creates an async function with a callback style function.
|
||||
--- @param argc number The number of arguments of func. Must be included.
|
||||
--- @param func function A callback style function to be converted. The last argument must be the callback.
|
||||
--- @return function: Returns an async function
|
||||
function M.wrap(argc, func)
|
||||
assert(type(func) == 'function')
|
||||
assert(type(argc) == 'number')
|
||||
return function(...)
|
||||
return M.wait(argc, func, ...)
|
||||
end
|
||||
end
|
||||
|
||||
--- create([argc, ] func)
|
||||
---
|
||||
--- Use this to create a function which executes in an async context but
|
||||
--- called from a non-async context. Inherently this cannot return anything
|
||||
--- since it is non-blocking
|
||||
---
|
||||
--- If argc is not provided, then the created async function cannot be continued
|
||||
---
|
||||
--- @generic F: function
|
||||
--- @param argc_or_func F|integer
|
||||
--- @param func? F
|
||||
--- @return F
|
||||
function M.create(argc_or_func, func)
|
||||
local argc --- @type integer
|
||||
if type(argc_or_func) == 'function' then
|
||||
assert(not func)
|
||||
func = argc_or_func
|
||||
elseif type(argc_or_func) == 'number' then
|
||||
assert(type(func) == 'function')
|
||||
argc = argc_or_func
|
||||
end
|
||||
|
||||
--- @cast func function
|
||||
|
||||
return function(...)
|
||||
local callback = argc and select(argc + 1, ...) or nil
|
||||
return run(func, callback, unpack({ ... }, 1, argc))
|
||||
end
|
||||
end
|
||||
|
||||
--- An async function that when called will yield to the Neovim scheduler to be
|
||||
--- able to call the API.
|
||||
M.scheduler = M.wrap(1, vim.schedule)
|
||||
|
||||
function M.run(func, ...)
|
||||
return run(func, nil, ...)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,423 @@
|
||||
local async = require('gitsigns.async')
|
||||
local git = require('gitsigns.git')
|
||||
|
||||
local manager = require('gitsigns.manager')
|
||||
|
||||
local log = require('gitsigns.debug.log')
|
||||
local dprintf = log.dprintf
|
||||
local dprint = log.dprint
|
||||
|
||||
local gs_cache = require('gitsigns.cache')
|
||||
local cache = gs_cache.cache
|
||||
local Status = require('gitsigns.status')
|
||||
|
||||
local config = require('gitsigns.config').config
|
||||
|
||||
local util = require('gitsigns.util')
|
||||
|
||||
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
|
||||
|
||||
local api = vim.api
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @param name string
|
||||
--- @return string? buffer
|
||||
--- @return string? commit
|
||||
local function parse_fugitive_uri(name)
|
||||
if vim.fn.exists('*FugitiveReal') == 0 then
|
||||
dprint('Fugitive not installed')
|
||||
return
|
||||
end
|
||||
|
||||
---@type string
|
||||
local path = vim.fn.FugitiveReal(name)
|
||||
---@type string?
|
||||
local commit = vim.fn.FugitiveParse(name)[1]:match('([^:]+):.*')
|
||||
if commit == '0' then
|
||||
-- '0' means the index so clear commit so we attach normally
|
||||
commit = nil
|
||||
end
|
||||
return path, commit
|
||||
end
|
||||
|
||||
--- @param name string
|
||||
--- @return string buffer
|
||||
--- @return string? commit
|
||||
local function parse_gitsigns_uri(name)
|
||||
-- TODO(lewis6991): Support submodules
|
||||
--- @type any, any, string?, string?, string
|
||||
local _, _, root_path, commit, rel_path = name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]])
|
||||
commit = util.norm_base(commit)
|
||||
if root_path then
|
||||
name = root_path .. '/' .. rel_path
|
||||
end
|
||||
return name, commit
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @return string buffer
|
||||
--- @return string? commit
|
||||
--- @return boolean? force_attach
|
||||
local function get_buf_path(bufnr)
|
||||
local file = uv.fs_realpath(api.nvim_buf_get_name(bufnr))
|
||||
or api.nvim_buf_call(bufnr, function()
|
||||
return vim.fn.expand('%:p')
|
||||
end)
|
||||
|
||||
if vim.startswith(file, 'fugitive://') then
|
||||
local path, commit = parse_fugitive_uri(file)
|
||||
dprintf("Fugitive buffer for file '%s' from path '%s'", path, file)
|
||||
if path then
|
||||
local realpath = uv.fs_realpath(path)
|
||||
if realpath then
|
||||
return realpath, commit, true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if vim.startswith(file, 'gitsigns://') then
|
||||
local path, commit = parse_gitsigns_uri(file)
|
||||
dprintf("Gitsigns buffer for file '%s' from path '%s' on commit '%s'", path, file, commit)
|
||||
local realpath = uv.fs_realpath(path)
|
||||
if realpath then
|
||||
return realpath, commit, true
|
||||
end
|
||||
end
|
||||
|
||||
return file
|
||||
end
|
||||
|
||||
local function on_lines(_, bufnr, _, first, last_orig, last_new, byte_count)
|
||||
if first == last_orig and last_orig == last_new and byte_count == 0 then
|
||||
-- on_lines can be called twice for undo events; ignore the second
|
||||
-- call which indicates no changes.
|
||||
return
|
||||
end
|
||||
return manager.on_lines(bufnr, first, last_orig, last_new)
|
||||
end
|
||||
|
||||
--- @param _ 'reload'
|
||||
--- @param bufnr integer
|
||||
local function on_reload(_, bufnr)
|
||||
local __FUNC__ = 'on_reload'
|
||||
cache[bufnr]:invalidate()
|
||||
dprint('Reload')
|
||||
manager.update_debounced(bufnr)
|
||||
end
|
||||
|
||||
--- @param _ 'detach'
|
||||
--- @param bufnr integer
|
||||
local function on_detach(_, bufnr)
|
||||
api.nvim_clear_autocmds({ group = 'gitsigns', buffer = bufnr })
|
||||
M.detach(bufnr, true)
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @return string?
|
||||
--- @return string?
|
||||
local function on_attach_pre(bufnr)
|
||||
--- @type string?, string?
|
||||
local gitdir, toplevel
|
||||
if config._on_attach_pre then
|
||||
--- @type {gitdir: string?, toplevel: string?}
|
||||
local res = async.wait(2, config._on_attach_pre, bufnr)
|
||||
dprintf('ran on_attach_pre with result %s', vim.inspect(res))
|
||||
if type(res) == 'table' then
|
||||
if type(res.gitdir) == 'string' then
|
||||
gitdir = res.gitdir
|
||||
end
|
||||
if type(res.toplevel) == 'string' then
|
||||
toplevel = res.toplevel
|
||||
end
|
||||
end
|
||||
end
|
||||
return gitdir, toplevel
|
||||
end
|
||||
|
||||
--- @param _bufnr integer
|
||||
--- @param file string
|
||||
--- @param encoding string
|
||||
--- @return Gitsigns.GitObj?
|
||||
local function try_worktrees(_bufnr, file, encoding)
|
||||
if not config.worktrees then
|
||||
return
|
||||
end
|
||||
|
||||
for _, wt in ipairs(config.worktrees) do
|
||||
local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel)
|
||||
if git_obj and git_obj.object_name then
|
||||
dprintf('Using worktree %s', vim.inspect(wt))
|
||||
return git_obj
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local setup = util.once(function()
|
||||
manager.setup()
|
||||
|
||||
api.nvim_create_autocmd('OptionSet', {
|
||||
group = 'gitsigns',
|
||||
pattern = { 'fileformat', 'bomb', 'eol' },
|
||||
callback = function()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local bcache = cache[buf]
|
||||
if not bcache then
|
||||
return
|
||||
end
|
||||
bcache:invalidate(true)
|
||||
manager.update(buf)
|
||||
end,
|
||||
})
|
||||
|
||||
require('gitsigns.current_line_blame').setup()
|
||||
|
||||
api.nvim_create_autocmd('VimLeavePre', {
|
||||
group = 'gitsigns',
|
||||
callback = M.detach_all,
|
||||
})
|
||||
end)
|
||||
|
||||
--- @class Gitsigns.GitContext
|
||||
--- @field file string
|
||||
--- @field toplevel? string
|
||||
--- @field gitdir? string
|
||||
--- @field base? string
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @return Gitsigns.GitContext? ctx
|
||||
--- @return string? err
|
||||
local function get_buf_context(bufnr)
|
||||
if api.nvim_buf_line_count(bufnr) > config.max_file_length then
|
||||
return nil, 'Exceeds max_file_length'
|
||||
end
|
||||
|
||||
local file, commit, force_attach = get_buf_path(bufnr)
|
||||
|
||||
if vim.bo[bufnr].buftype ~= '' and not force_attach then
|
||||
return nil, 'Non-normal buffer'
|
||||
end
|
||||
|
||||
local file_dir = util.dirname(file)
|
||||
|
||||
if not file_dir or not util.path_exists(file_dir) then
|
||||
return nil, 'Not a path'
|
||||
end
|
||||
|
||||
local gitdir, toplevel = on_attach_pre(bufnr)
|
||||
|
||||
return {
|
||||
file = file,
|
||||
gitdir = gitdir,
|
||||
toplevel = toplevel,
|
||||
-- Commit buffers have there base set back one revision with '^'
|
||||
-- Stage buffers always compare against the common ancestor (':1')
|
||||
-- :0: index
|
||||
-- :1: common ancestor
|
||||
-- :2: target commit (HEAD)
|
||||
-- :3: commit which is being merged
|
||||
base = commit and (commit:match('^:[1-3]') and ':1' or commit .. '^') or nil,
|
||||
}
|
||||
end
|
||||
|
||||
--- Ensure attaches cannot be interleaved for the same buffer.
|
||||
--- Since attaches are asynchronous we need to make sure an attach isn't
|
||||
--- performed whilst another one is in progress.
|
||||
--- @param cbuf integer
|
||||
--- @param ctx? Gitsigns.GitContext
|
||||
--- @param aucmd? string
|
||||
local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd)
|
||||
local __FUNC__ = 'attach'
|
||||
local passed_ctx = ctx ~= nil
|
||||
|
||||
setup()
|
||||
|
||||
if cache[cbuf] then
|
||||
dprint('Already attached')
|
||||
return
|
||||
end
|
||||
|
||||
if aucmd then
|
||||
dprintf('Attaching (trigger=%s)', aucmd)
|
||||
else
|
||||
dprint('Attaching')
|
||||
end
|
||||
|
||||
if not api.nvim_buf_is_loaded(cbuf) then
|
||||
dprint('Non-loaded buffer')
|
||||
return
|
||||
end
|
||||
|
||||
if not ctx then
|
||||
local err
|
||||
ctx, err = get_buf_context(cbuf)
|
||||
if err then
|
||||
dprint(err)
|
||||
return
|
||||
end
|
||||
assert(ctx)
|
||||
end
|
||||
|
||||
local encoding = vim.bo[cbuf].fileencoding
|
||||
if encoding == '' then
|
||||
encoding = 'utf-8'
|
||||
end
|
||||
|
||||
local file = ctx.file
|
||||
if not vim.startswith(file, '/') and ctx.toplevel then
|
||||
file = ctx.toplevel .. util.path_sep .. file
|
||||
end
|
||||
|
||||
local revision = ctx.base or config.base
|
||||
local git_obj = git.Obj.new(file, revision, encoding, ctx.gitdir, ctx.toplevel)
|
||||
|
||||
if not git_obj and not passed_ctx then
|
||||
git_obj = try_worktrees(cbuf, file, encoding)
|
||||
async.scheduler()
|
||||
if not api.nvim_buf_is_valid(cbuf) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if not git_obj then
|
||||
dprint('Empty git obj')
|
||||
return
|
||||
end
|
||||
|
||||
async.scheduler()
|
||||
if not api.nvim_buf_is_valid(cbuf) then
|
||||
return
|
||||
end
|
||||
|
||||
Status:update(cbuf, {
|
||||
head = git_obj.repo.abbrev_head,
|
||||
root = git_obj.repo.toplevel,
|
||||
gitdir = git_obj.repo.gitdir,
|
||||
})
|
||||
|
||||
if not passed_ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then
|
||||
dprint('Not a file')
|
||||
return
|
||||
end
|
||||
|
||||
if not git_obj.relpath then
|
||||
dprint('Cannot resolve file in repo')
|
||||
return
|
||||
end
|
||||
|
||||
if not config.attach_to_untracked and git_obj.object_name == nil then
|
||||
dprint('File is untracked')
|
||||
return
|
||||
end
|
||||
|
||||
-- On windows os.tmpname() crashes in callback threads so initialise this
|
||||
-- variable on the main thread.
|
||||
async.scheduler()
|
||||
if not api.nvim_buf_is_valid(cbuf) then
|
||||
return
|
||||
end
|
||||
|
||||
if config.on_attach and config.on_attach(cbuf) == false then
|
||||
dprint('User on_attach() returned false')
|
||||
return
|
||||
end
|
||||
|
||||
cache[cbuf] = gs_cache.new({
|
||||
bufnr = cbuf,
|
||||
file = file,
|
||||
git_obj = git_obj,
|
||||
})
|
||||
|
||||
if config.watch_gitdir.enable then
|
||||
local watcher = require('gitsigns.watcher')
|
||||
cache[cbuf].gitdir_watcher = watcher.watch_gitdir(cbuf, git_obj.repo.gitdir)
|
||||
end
|
||||
|
||||
if not api.nvim_buf_is_loaded(cbuf) then
|
||||
dprint('Un-loaded buffer')
|
||||
return
|
||||
end
|
||||
|
||||
-- Make sure to attach before the first update (which is async) so we pick up
|
||||
-- changes from BufReadCmd.
|
||||
api.nvim_buf_attach(cbuf, false, {
|
||||
on_lines = on_lines,
|
||||
on_reload = on_reload,
|
||||
on_detach = on_detach,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd('BufWrite', {
|
||||
group = 'gitsigns',
|
||||
buffer = cbuf,
|
||||
callback = function()
|
||||
manager.update_debounced(cbuf)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Initial update
|
||||
manager.update(cbuf)
|
||||
end)
|
||||
|
||||
--- Detach Gitsigns from all buffers it is attached to.
|
||||
function M.detach_all()
|
||||
for k, _ in pairs(cache) do
|
||||
M.detach(k)
|
||||
end
|
||||
end
|
||||
|
||||
--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not
|
||||
--- provided then the current buffer is used.
|
||||
---
|
||||
--- @param bufnr integer Buffer number
|
||||
--- @param _keep_signs? boolean
|
||||
function M.detach(bufnr, _keep_signs)
|
||||
-- When this is called interactively (with no arguments) we want to remove all
|
||||
-- the signs, however if called via a detach event (due to nvim_buf_attach)
|
||||
-- then we don't want to clear the signs in case the buffer is just being
|
||||
-- updated due to the file externally changing. When this happens a detach and
|
||||
-- attach event happen in sequence and so we keep the old signs to stop the
|
||||
-- sign column width moving about between updates.
|
||||
bufnr = bufnr or api.nvim_get_current_buf()
|
||||
dprint('Detached')
|
||||
local bcache = cache[bufnr]
|
||||
if not bcache then
|
||||
dprint('Cache was nil')
|
||||
return
|
||||
end
|
||||
|
||||
manager.detach(bufnr, _keep_signs)
|
||||
|
||||
-- Clear status variables
|
||||
Status:clear(bufnr)
|
||||
|
||||
gs_cache.destroy(bufnr)
|
||||
end
|
||||
|
||||
--- Attach Gitsigns to the buffer.
|
||||
---
|
||||
--- Attributes: ~
|
||||
--- {async}
|
||||
---
|
||||
--- @param bufnr integer Buffer number
|
||||
--- @param ctx Gitsigns.GitContext|nil
|
||||
--- Git context data that may optionally be used to attach to any
|
||||
--- buffer that represents a real git object.
|
||||
--- • {file}: (string)
|
||||
--- Path to the file represented by the buffer, relative to the
|
||||
--- top-level.
|
||||
--- • {toplevel}: (string?)
|
||||
--- Path to the top-level of the parent git repository.
|
||||
--- • {gitdir}: (string?)
|
||||
--- Path to the git directory of the parent git repository
|
||||
--- (typically the ".git/" directory).
|
||||
--- • {commit}: (string?)
|
||||
--- The git revision that the file belongs to.
|
||||
--- • {base}: (string?)
|
||||
--- The git revision that the file should be compared to.
|
||||
--- @param _trigger? string
|
||||
M.attach = async.create(3, function(bufnr, ctx, _trigger)
|
||||
attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger)
|
||||
end)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,151 @@
|
||||
local async = require('gitsigns.async')
|
||||
local util = require('gitsigns.util')
|
||||
|
||||
local M = {
|
||||
CacheEntry = {},
|
||||
}
|
||||
|
||||
--- @class (exact) Gitsigns.CacheEntry
|
||||
--- @field bufnr integer
|
||||
--- @field file string
|
||||
--- @field compare_text? string[]
|
||||
--- @field hunks? Gitsigns.Hunk.Hunk[]
|
||||
--- @field force_next_update? boolean
|
||||
--- @field file_mode? boolean
|
||||
---
|
||||
--- @field compare_text_head? string[]
|
||||
--- @field hunks_staged? Gitsigns.Hunk.Hunk[]
|
||||
---
|
||||
--- @field staged_diffs? Gitsigns.Hunk.Hunk[]
|
||||
--- @field gitdir_watcher? uv.uv_fs_event_t
|
||||
--- @field git_obj Gitsigns.GitObj
|
||||
--- @field blame? table<integer,Gitsigns.BlameInfo?>
|
||||
local CacheEntry = M.CacheEntry
|
||||
|
||||
function CacheEntry:get_rev_bufname(rev)
|
||||
rev = rev or self.git_obj.revision or ':0'
|
||||
return string.format('gitsigns://%s/%s:%s', self.git_obj.repo.gitdir, rev, self.git_obj.relpath)
|
||||
end
|
||||
|
||||
--- Invalidate any state dependent on the buffer content.
|
||||
--- If 'all' is passed, then invalidate everything.
|
||||
--- @param all? boolean
|
||||
function CacheEntry:invalidate(all)
|
||||
self.hunks = nil
|
||||
self.hunks_staged = nil
|
||||
self.blame = nil
|
||||
if all then
|
||||
-- The below doesn't need to be invalidated
|
||||
-- if the buffer changes
|
||||
self.compare_text = nil
|
||||
self.compare_text_head = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- @param o Gitsigns.CacheEntry
|
||||
--- @return Gitsigns.CacheEntry
|
||||
function M.new(o)
|
||||
o.staged_diffs = o.staged_diffs or {}
|
||||
return setmetatable(o, { __index = CacheEntry })
|
||||
end
|
||||
|
||||
local sleep = async.wrap(2, function(duration, cb)
|
||||
vim.defer_fn(cb, duration)
|
||||
end)
|
||||
|
||||
--- @private
|
||||
function CacheEntry:wait_for_hunks()
|
||||
local loop_protect = 0
|
||||
while not self.hunks and loop_protect < 10 do
|
||||
loop_protect = loop_protect + 1
|
||||
sleep(100)
|
||||
end
|
||||
end
|
||||
|
||||
-- If a file contains has up to this amount of lines, then
|
||||
-- always blame the whole file, otherwise only blame one line
|
||||
-- at a time.
|
||||
local BLAME_THRESHOLD_LEN = 1000000
|
||||
|
||||
--- @private
|
||||
--- @param lnum integer
|
||||
--- @param opts Gitsigns.BlameOpts
|
||||
--- @return table<integer,Gitsigns.BlameInfo?>?
|
||||
function CacheEntry:run_blame(lnum, opts)
|
||||
local bufnr = self.bufnr
|
||||
local blame_cache --- @type table<integer,Gitsigns.BlameInfo?>?
|
||||
repeat
|
||||
local buftext = util.buf_lines(bufnr)
|
||||
local tick = vim.b[bufnr].changedtick
|
||||
local lnum0 = #buftext > BLAME_THRESHOLD_LEN and lnum or nil
|
||||
-- TODO(lewis6991): Cancel blame on changedtick
|
||||
blame_cache = self.git_obj:run_blame(buftext, lnum0, opts)
|
||||
async.scheduler()
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
until vim.b[bufnr].changedtick == tick
|
||||
return blame_cache
|
||||
end
|
||||
|
||||
--- @param file string
|
||||
--- @param lnum integer
|
||||
--- @return Gitsigns.BlameInfo
|
||||
local function get_blame_nc(file, lnum)
|
||||
local Git = require('gitsigns.git')
|
||||
|
||||
return {
|
||||
orig_lnum = 0,
|
||||
final_lnum = lnum,
|
||||
commit = Git.not_commited(file),
|
||||
filename = file,
|
||||
}
|
||||
end
|
||||
|
||||
--- @param lnum integer
|
||||
--- @param opts Gitsigns.BlameOpts
|
||||
--- @return Gitsigns.BlameInfo?
|
||||
function CacheEntry:get_blame(lnum, opts)
|
||||
if opts.rev then
|
||||
local buftext = util.buf_lines(self.bufnr)
|
||||
return self.git_obj:run_blame(buftext, lnum, opts)[lnum]
|
||||
end
|
||||
|
||||
local blame_cache = self.blame
|
||||
|
||||
if not blame_cache or not blame_cache[lnum] then
|
||||
self:wait_for_hunks()
|
||||
local Hunks = require('gitsigns.hunks')
|
||||
if Hunks.find_hunk(lnum, self.hunks) then
|
||||
--- Bypass running blame (which can be expensive) if we know lnum is in a hunk
|
||||
blame_cache = blame_cache or {}
|
||||
blame_cache[lnum] = get_blame_nc(self.git_obj.relpath, lnum)
|
||||
else
|
||||
-- Refresh cache
|
||||
blame_cache = self:run_blame(lnum, opts)
|
||||
end
|
||||
self.blame = blame_cache
|
||||
end
|
||||
|
||||
if blame_cache then
|
||||
return blame_cache[lnum]
|
||||
end
|
||||
end
|
||||
|
||||
function CacheEntry:destroy()
|
||||
local w = self.gitdir_watcher
|
||||
if w and not w:is_closing() then
|
||||
w:close()
|
||||
end
|
||||
end
|
||||
|
||||
---@type table<integer,Gitsigns.CacheEntry>
|
||||
M.cache = {}
|
||||
|
||||
--- @param bufnr integer
|
||||
function M.destroy(bufnr)
|
||||
M.cache[bufnr]:destroy()
|
||||
M.cache[bufnr] = nil
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,108 @@
|
||||
local async = require('gitsigns.async')
|
||||
|
||||
local log = require('gitsigns.debug.log')
|
||||
local dprintf = log.dprintf
|
||||
local message = require('gitsigns.message')
|
||||
|
||||
local parse_args = require('gitsigns.cli.argparse').parse_args
|
||||
|
||||
local actions = require('gitsigns.actions')
|
||||
local attach = require('gitsigns.attach')
|
||||
local gs_debug = require('gitsigns.debug')
|
||||
|
||||
--- @type table<table<string,function>,boolean>
|
||||
local sources = {
|
||||
[actions] = true,
|
||||
[attach] = false,
|
||||
[gs_debug] = false,
|
||||
}
|
||||
|
||||
-- try to parse each argument as a lua boolean, nil or number, if fails then
|
||||
-- keep argument as a string:
|
||||
--
|
||||
-- 'false' -> false
|
||||
-- 'nil' -> nil
|
||||
-- '100' -> 100
|
||||
-- 'HEAD~300' -> 'HEAD~300'
|
||||
local function parse_to_lua(a)
|
||||
if tonumber(a) then
|
||||
return tonumber(a)
|
||||
elseif a == 'false' or a == 'true' then
|
||||
return a == 'true'
|
||||
elseif a == 'nil' then
|
||||
return nil
|
||||
end
|
||||
return a
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.complete(arglead, line)
|
||||
local words = vim.split(line, '%s+')
|
||||
local n = #words
|
||||
|
||||
local matches = {}
|
||||
if n == 2 then
|
||||
for m, _ in pairs(sources) do
|
||||
for func, _ in pairs(m) do
|
||||
if not func:match('^[a-z]') then
|
||||
-- exclude
|
||||
elseif vim.startswith(func, arglead) then
|
||||
table.insert(matches, func)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif n > 2 then
|
||||
-- Subcommand completion
|
||||
local cmp_func = actions._get_cmp_func(words[2])
|
||||
if cmp_func then
|
||||
return cmp_func(arglead)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
M.run = async.create(1, function(params)
|
||||
local __FUNC__ = 'cli.run'
|
||||
local pos_args_raw, named_args_raw = parse_args(params.args)
|
||||
|
||||
local func = pos_args_raw[1]
|
||||
|
||||
if not func then
|
||||
func = async.wait(3, vim.ui.select, M.complete('', 'Gitsigns '), {}) --[[@as string]]
|
||||
if not func then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2))
|
||||
local named_args = vim.tbl_map(parse_to_lua, named_args_raw)
|
||||
local args = vim.tbl_extend('error', pos_args, named_args)
|
||||
|
||||
dprintf(
|
||||
"Running action '%s' with arguments %s",
|
||||
func,
|
||||
vim.inspect(args, { newline = ' ', indent = '' })
|
||||
)
|
||||
|
||||
local cmd_func = actions._get_cmd_func(func)
|
||||
if cmd_func then
|
||||
-- Action has a specialised mapping function from command form to lua
|
||||
-- function
|
||||
cmd_func(args, params)
|
||||
return
|
||||
end
|
||||
|
||||
for m, has_named in pairs(sources) do
|
||||
local f = m[func]
|
||||
if type(f) == 'function' then
|
||||
-- Note functions here do not have named arguments
|
||||
f(unpack(pos_args), has_named and named_args or nil)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
message.error('%s is not a valid function or action', func)
|
||||
end)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,108 @@
|
||||
local M = {}
|
||||
|
||||
local function is_char(x)
|
||||
return x:match('[^=\'"%s]') ~= nil
|
||||
end
|
||||
|
||||
-- Return positional arguments and named arguments
|
||||
--- @param x string
|
||||
--- @return string[], table<string,string|boolean>
|
||||
function M.parse_args(x)
|
||||
--- @type string[], table<string,string|boolean>
|
||||
local pos_args, named_args = {}, {}
|
||||
|
||||
local state = 'in_arg'
|
||||
local cur_arg = ''
|
||||
local cur_val = ''
|
||||
local cur_quote = ''
|
||||
|
||||
local function peek(idx)
|
||||
return x:sub(idx + 1, idx + 1)
|
||||
end
|
||||
|
||||
local i = 1
|
||||
while i <= #x do
|
||||
local ch = x:sub(i, i)
|
||||
-- dprintf('L(%d)(%s): cur_arg="%s" ch="%s"', i, state, cur_arg, ch)
|
||||
|
||||
if state == 'in_arg' then
|
||||
if is_char(ch) then
|
||||
if ch == '-' and peek(i) == '-' then
|
||||
state = 'in_flag'
|
||||
cur_arg = ''
|
||||
i = i + 1
|
||||
else
|
||||
cur_arg = cur_arg .. ch
|
||||
end
|
||||
elseif ch:match('%s') then
|
||||
pos_args[#pos_args + 1] = cur_arg
|
||||
state = 'in_ws'
|
||||
elseif ch == '=' then
|
||||
cur_val = ''
|
||||
local next_ch = peek(i)
|
||||
if next_ch == "'" or next_ch == '"' then
|
||||
cur_quote = next_ch
|
||||
i = i + 1
|
||||
state = 'in_quote'
|
||||
else
|
||||
state = 'in_value'
|
||||
end
|
||||
end
|
||||
elseif state == 'in_flag' then
|
||||
if ch:match('%s') then
|
||||
named_args[cur_arg] = true
|
||||
state = 'in_ws'
|
||||
else
|
||||
cur_arg = cur_arg .. ch
|
||||
end
|
||||
elseif state == 'in_ws' then
|
||||
if is_char(ch) then
|
||||
if ch == '-' and peek(i) == '-' then
|
||||
state = 'in_flag'
|
||||
cur_arg = ''
|
||||
i = i + 1
|
||||
else
|
||||
state = 'in_arg'
|
||||
cur_arg = ch
|
||||
end
|
||||
end
|
||||
elseif state == 'in_value' then
|
||||
if is_char(ch) then
|
||||
cur_val = cur_val .. ch
|
||||
elseif ch:match('%s') then
|
||||
named_args[cur_arg] = cur_val
|
||||
cur_arg = ''
|
||||
state = 'in_ws'
|
||||
end
|
||||
elseif state == 'in_quote' then
|
||||
local next_ch = peek(i)
|
||||
if ch == '\\' and next_ch == cur_quote then
|
||||
cur_val = cur_val .. next_ch
|
||||
i = i + 1
|
||||
elseif ch == cur_quote then
|
||||
named_args[cur_arg] = cur_val
|
||||
state = 'in_ws'
|
||||
if next_ch ~= '' and not next_ch:match('%s') then
|
||||
error('malformed argument: ' .. next_ch)
|
||||
end
|
||||
else
|
||||
cur_val = cur_val .. ch
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
if #cur_arg > 0 then
|
||||
if state == 'in_arg' then
|
||||
pos_args[#pos_args + 1] = cur_arg
|
||||
elseif state == 'in_flag' then
|
||||
named_args[cur_arg] = true
|
||||
elseif state == 'in_value' then
|
||||
named_args[cur_arg] = cur_val
|
||||
end
|
||||
end
|
||||
|
||||
return pos_args, named_args
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,952 @@
|
||||
--- @class (exact) Gitsigns.SchemaElem.Deprecated
|
||||
---
|
||||
--- Used for renaming fields.
|
||||
--- @field new_field? string
|
||||
---
|
||||
--- Documentation for deprecation. Will be added to the help file and used in
|
||||
--- the notification if `hard = true`.
|
||||
--- @field message? string
|
||||
---
|
||||
--- Emit a message via vim.notify
|
||||
--- @field hard? boolean
|
||||
|
||||
--- @class (exact) Gitsigns.SchemaElem
|
||||
--- @field type string|string[]
|
||||
--- @field refresh? fun(cb: fun()) Function to refresh the config value
|
||||
--- @field deep_extend? boolean
|
||||
--- @field default any
|
||||
--- @field deprecated? boolean|Gitsigns.SchemaElem.Deprecated
|
||||
--- @field default_help? string
|
||||
--- @field description string
|
||||
|
||||
--- @class (exact) Gitsigns.DiffOpts
|
||||
--- @field algorithm string
|
||||
--- @field internal boolean
|
||||
--- @field indent_heuristic boolean
|
||||
--- @field vertical boolean
|
||||
--- @field linematch? integer
|
||||
--- @field ignore_whitespace_change? true
|
||||
--- @field ignore_whitespace? true
|
||||
--- @field ignore_whitespace_change_at_eol? true
|
||||
--- @field ignore_blank_lines? true
|
||||
|
||||
--- @class (exact) Gitsigns.SignConfig
|
||||
--- @field show_count boolean
|
||||
--- @field hl string
|
||||
--- @field text string
|
||||
--- @field numhl string
|
||||
--- @field linehl string
|
||||
|
||||
--- @alias Gitsigns.SignType
|
||||
--- | 'add'
|
||||
--- | 'change'
|
||||
--- | 'delete'
|
||||
--- | 'topdelete'
|
||||
--- | 'changedelete'
|
||||
--- | 'untracked'
|
||||
|
||||
--- @class (exact) Gitsigns.CurrentLineBlameFmtOpts
|
||||
--- @field relative_time boolean
|
||||
|
||||
--- @alias Gitsigns.CurrentLineBlameFmtFun fun(user: string, info: table<string,any>, opts: Gitsigns.CurrentLineBlameFmtOpts): {[1]:string,[2]:string}[]
|
||||
|
||||
--- @class (exact) Gitsigns.CurrentLineBlameOpts : Gitsigns.BlameOpts
|
||||
--- @field virt_text? boolean
|
||||
--- @field virt_text_pos? 'eol'|'overlay'|'right_align'
|
||||
--- @field delay? integer
|
||||
--- @field virt_text_priority? integer
|
||||
|
||||
--- @class (exact) Gitsigns.BlameOpts
|
||||
--- @field ignore_whitespace? boolean
|
||||
--- @field rev? string
|
||||
--- @field extra_opts? string[]
|
||||
|
||||
--- @class (exact) Gitsigns.LineBlameOpts : Gitsigns.BlameOpts
|
||||
--- @field full? boolean
|
||||
|
||||
--- @class (exact) Gitsigns.Config
|
||||
--- @field debug_mode boolean
|
||||
--- @field diff_opts Gitsigns.DiffOpts
|
||||
--- @field base? string
|
||||
--- @field signs table<Gitsigns.SignType,Gitsigns.SignConfig>
|
||||
--- @field _signs_staged table<Gitsigns.SignType,Gitsigns.SignConfig>
|
||||
--- @field _signs_staged_enable boolean
|
||||
--- @field count_chars table<string|integer,string>
|
||||
--- @field signcolumn boolean
|
||||
--- @field numhl boolean
|
||||
--- @field linehl boolean
|
||||
--- @field show_deleted boolean
|
||||
--- @field sign_priority integer
|
||||
--- @field _on_attach_pre fun(bufnr: integer, callback: fun(_: table))
|
||||
--- @field on_attach fun(bufnr: integer)
|
||||
--- @field watch_gitdir { enable: boolean, follow_files: boolean }
|
||||
--- @field max_file_length integer
|
||||
--- @field update_debounce integer
|
||||
--- @field status_formatter fun(_: table<string,any>): string
|
||||
--- @field current_line_blame boolean
|
||||
--- @field current_line_blame_formatter_opts { relative_time: boolean }
|
||||
--- @field current_line_blame_formatter string|Gitsigns.CurrentLineBlameFmtFun
|
||||
--- @field current_line_blame_formatter_nc string|Gitsigns.CurrentLineBlameFmtFun
|
||||
--- @field current_line_blame_opts Gitsigns.CurrentLineBlameOpts
|
||||
--- @field preview_config table<string,any>
|
||||
--- @field auto_attach boolean
|
||||
--- @field attach_to_untracked boolean
|
||||
--- @field yadm { enable: boolean }
|
||||
--- @field worktrees {toplevel: string, gitdir: string}[]
|
||||
--- @field word_diff boolean
|
||||
--- @field trouble boolean
|
||||
--- -- Undocumented
|
||||
--- @field _refresh_staged_on_update boolean
|
||||
--- @field _threaded_diff boolean
|
||||
--- @field _inline2 boolean
|
||||
--- @field _git_version string
|
||||
--- @field _verbose boolean
|
||||
--- @field _test_mode boolean
|
||||
|
||||
local M = {
|
||||
Config = {
|
||||
DiffOpts = {},
|
||||
SignConfig = {},
|
||||
watch_gitdir = {},
|
||||
current_line_blame_formatter_opts = {},
|
||||
current_line_blame_opts = {},
|
||||
yadm = {},
|
||||
Worktree = {},
|
||||
},
|
||||
}
|
||||
|
||||
--- @param v Gitsigns.SchemaElem
|
||||
--- @return any
|
||||
local function resolve_default(v)
|
||||
if type(v.default) == 'function' and v.type ~= 'function' then
|
||||
return v.default()
|
||||
else
|
||||
return v.default
|
||||
end
|
||||
end
|
||||
|
||||
--- @return Gitsigns.DiffOpts
|
||||
local function parse_diffopt()
|
||||
--- @type Gitsigns.DiffOpts
|
||||
local r = {
|
||||
algorithm = 'myers',
|
||||
internal = false,
|
||||
indent_heuristic = false,
|
||||
vertical = true,
|
||||
}
|
||||
|
||||
local optmap = {
|
||||
['indent-heuristic'] = 'indent_heuristic',
|
||||
internal = 'internal',
|
||||
iwhite = 'ignore_whitespace_change',
|
||||
iblank = 'ignore_blank_lines',
|
||||
iwhiteeol = 'ignore_whitespace_change_at_eol',
|
||||
iwhiteall = 'ignore_whitespace',
|
||||
}
|
||||
|
||||
local diffopt = vim.opt.diffopt:get() --[[@as string[] ]]
|
||||
for _, o in ipairs(diffopt) do
|
||||
if optmap[o] then
|
||||
r[optmap[o]] = true
|
||||
elseif o == 'horizontal' then
|
||||
r.vertical = false
|
||||
elseif vim.startswith(o, 'algorithm:') then
|
||||
r.algorithm = string.sub(o, ('algorithm:'):len() + 1)
|
||||
elseif vim.startswith(o, 'linematch:') then
|
||||
r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1))
|
||||
end
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
--- @type Gitsigns.Config
|
||||
M.config = setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
if rawget(t, k) == nil then
|
||||
local field = M.schema[k]
|
||||
if not field then
|
||||
return
|
||||
end
|
||||
|
||||
rawset(t, k, resolve_default(field))
|
||||
|
||||
if field.refresh then
|
||||
field.refresh(function()
|
||||
rawset(t, k, resolve_default(field))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
return rawget(t, k)
|
||||
end,
|
||||
})
|
||||
|
||||
--- @type table<string,Gitsigns.SchemaElem>
|
||||
M.schema = {
|
||||
signs = {
|
||||
type = 'table',
|
||||
deep_extend = true,
|
||||
default = {
|
||||
add = { hl = 'GitSignsAdd', text = '┃', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn' },
|
||||
change = {
|
||||
hl = 'GitSignsChange',
|
||||
text = '┃',
|
||||
numhl = 'GitSignsChangeNr',
|
||||
linehl = 'GitSignsChangeLn',
|
||||
},
|
||||
delete = {
|
||||
hl = 'GitSignsDelete',
|
||||
text = '▁',
|
||||
numhl = 'GitSignsDeleteNr',
|
||||
linehl = 'GitSignsDeleteLn',
|
||||
},
|
||||
topdelete = {
|
||||
hl = 'GitSignsTopdelete',
|
||||
text = '▔',
|
||||
numhl = 'GitSignsTopdeleteNr',
|
||||
linehl = 'GitSignsTopdeleteLn',
|
||||
},
|
||||
changedelete = {
|
||||
hl = 'GitSignsChangedelete',
|
||||
text = '~',
|
||||
numhl = 'GitSignsChangedeleteNr',
|
||||
linehl = 'GitSignsChangedeleteLn',
|
||||
},
|
||||
untracked = {
|
||||
hl = 'GitSignsUntracked',
|
||||
text = '┆',
|
||||
numhl = 'GitSignsUntrackedNr',
|
||||
linehl = 'GitSignsUntrackedLn',
|
||||
},
|
||||
},
|
||||
default_help = [[{
|
||||
add = { text = '┃' },
|
||||
change = { text = '┃' },
|
||||
delete = { text = '▁' },
|
||||
topdelete = { text = '▔' },
|
||||
changedelete = { text = '~' },
|
||||
untracked = { text = '┆' },
|
||||
}]],
|
||||
description = [[
|
||||
Configuration for signs:
|
||||
• `text` specifies the character to use for the sign.
|
||||
• `show_count` to enable showing count of hunk, e.g. number of deleted
|
||||
lines.
|
||||
|
||||
The highlights `GitSigns[kind][type]` is used for each kind of sign. E.g.
|
||||
'add' signs uses the highlights:
|
||||
• `GitSignsAdd` (for normal text signs)
|
||||
• `GitSignsAddNr` (for signs when `config.numhl == true`)
|
||||
• `GitSignsAddLn `(for signs when `config.linehl == true`)
|
||||
|
||||
See |gitsigns-highlight-groups|.
|
||||
]],
|
||||
},
|
||||
|
||||
_signs_staged = {
|
||||
type = 'table',
|
||||
deep_extend = true,
|
||||
default = {
|
||||
add = {
|
||||
hl = 'GitSignsStagedAdd',
|
||||
text = '┃',
|
||||
numhl = 'GitSignsStagedAddNr',
|
||||
linehl = 'GitSignsStagedAddLn',
|
||||
},
|
||||
change = {
|
||||
hl = 'GitSignsStagedChange',
|
||||
text = '┃',
|
||||
numhl = 'GitSignsStagedChangeNr',
|
||||
linehl = 'GitSignsStagedChangeLn',
|
||||
},
|
||||
delete = {
|
||||
hl = 'GitSignsStagedDelete',
|
||||
text = '▁',
|
||||
numhl = 'GitSignsStagedDeleteNr',
|
||||
linehl = 'GitSignsStagedDeleteLn',
|
||||
},
|
||||
topdelete = {
|
||||
hl = 'GitSignsStagedTopdelete',
|
||||
text = '▔',
|
||||
numhl = 'GitSignsStagedTopdeleteNr',
|
||||
linehl = 'GitSignsStagedTopdeleteLn',
|
||||
},
|
||||
changedelete = {
|
||||
hl = 'GitSignsStagedChangedelete',
|
||||
text = '~',
|
||||
numhl = 'GitSignsStagedChangedeleteNr',
|
||||
linehl = 'GitSignsStagedChangedeleteLn',
|
||||
},
|
||||
},
|
||||
default_help = [[{
|
||||
add = { text = '┃' },
|
||||
change = { text = '┃' },
|
||||
delete = { text = '▁' },
|
||||
topdelete = { text = '▔' },
|
||||
changedelete = { text = '~' },
|
||||
}]],
|
||||
description = [[
|
||||
Configuration for signs of staged hunks.
|
||||
|
||||
See |gitsigns-config-signs|.
|
||||
]],
|
||||
},
|
||||
|
||||
_signs_staged_enable = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Show signs for staged hunks.
|
||||
|
||||
When enabled the signs defined in |git-config-signs_staged|` are used.
|
||||
]],
|
||||
},
|
||||
|
||||
worktrees = {
|
||||
type = 'table',
|
||||
default = nil,
|
||||
description = [[
|
||||
Detached working trees.
|
||||
|
||||
Array of tables with the keys `gitdir` and `toplevel`.
|
||||
|
||||
If normal attaching fails, then each entry in the table is attempted
|
||||
with the work tree details set.
|
||||
|
||||
Example: >lua
|
||||
worktrees = {
|
||||
{
|
||||
toplevel = vim.env.HOME,
|
||||
gitdir = vim.env.HOME .. '/projects/dotfiles/.git'
|
||||
}
|
||||
}
|
||||
]],
|
||||
},
|
||||
|
||||
_on_attach_pre = {
|
||||
type = 'function',
|
||||
default = nil,
|
||||
description = [[
|
||||
Asynchronous hook called before attaching to a buffer. Mainly used to
|
||||
configure detached worktrees.
|
||||
|
||||
This callback must call its callback argument. The callback argument can
|
||||
accept an optional table argument with the keys: 'gitdir' and 'toplevel'.
|
||||
|
||||
Example: >lua
|
||||
on_attach_pre = function(bufnr, callback)
|
||||
...
|
||||
callback {
|
||||
gitdir = ...,
|
||||
toplevel = ...
|
||||
}
|
||||
end
|
||||
<
|
||||
]],
|
||||
},
|
||||
|
||||
on_attach = {
|
||||
type = 'function',
|
||||
default = nil,
|
||||
description = [[
|
||||
Callback called when attaching to a buffer. Mainly used to setup keymaps.
|
||||
The buffer number is passed as the first argument.
|
||||
|
||||
This callback can return `false` to prevent attaching to the buffer.
|
||||
|
||||
Example: >lua
|
||||
on_attach = function(bufnr)
|
||||
if vim.api.nvim_buf_get_name(bufnr):match(<PATTERN>) then
|
||||
-- Don't attach to specific buffers whose name matches a pattern
|
||||
return false
|
||||
end
|
||||
|
||||
-- Setup keymaps
|
||||
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', '<cmd>lua require"gitsigns".stage_hunk()<CR>', {})
|
||||
... -- More keymaps
|
||||
end
|
||||
<
|
||||
]],
|
||||
},
|
||||
|
||||
watch_gitdir = {
|
||||
type = 'table',
|
||||
deep_extend = true,
|
||||
default = {
|
||||
enable = true,
|
||||
follow_files = true,
|
||||
},
|
||||
description = [[
|
||||
When opening a file, a libuv watcher is placed on the respective
|
||||
`.git` directory to detect when changes happen to use as a trigger to
|
||||
update signs.
|
||||
|
||||
Fields: ~
|
||||
• `enable`:
|
||||
Whether the watcher is enabled.
|
||||
|
||||
• `follow_files`:
|
||||
If a file is moved with `git mv`, switch the buffer to the new location.
|
||||
]],
|
||||
},
|
||||
|
||||
sign_priority = {
|
||||
type = 'number',
|
||||
default = 6,
|
||||
description = [[
|
||||
Priority to use for signs.
|
||||
]],
|
||||
},
|
||||
|
||||
signcolumn = {
|
||||
type = 'boolean',
|
||||
default = true,
|
||||
description = [[
|
||||
Enable/disable symbols in the sign column.
|
||||
|
||||
When enabled the highlights defined in `signs.*.hl` and symbols defined
|
||||
in `signs.*.text` are used.
|
||||
]],
|
||||
},
|
||||
|
||||
numhl = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Enable/disable line number highlights.
|
||||
|
||||
When enabled the highlights defined in `signs.*.numhl` are used. If
|
||||
the highlight group does not exist, then it is automatically defined
|
||||
and linked to the corresponding highlight group in `signs.*.hl`.
|
||||
]],
|
||||
},
|
||||
|
||||
linehl = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Enable/disable line highlights.
|
||||
|
||||
When enabled the highlights defined in `signs.*.linehl` are used. If
|
||||
the highlight group does not exist, then it is automatically defined
|
||||
and linked to the corresponding highlight group in `signs.*.hl`.
|
||||
]],
|
||||
},
|
||||
|
||||
show_deleted = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Show the old version of hunks inline in the buffer (via virtual lines).
|
||||
|
||||
Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`.
|
||||
]],
|
||||
},
|
||||
|
||||
diff_opts = {
|
||||
type = 'table',
|
||||
deep_extend = true,
|
||||
refresh = function(callback)
|
||||
vim.api.nvim_create_autocmd('OptionSet', {
|
||||
group = vim.api.nvim_create_augroup('gitsigns.config.diff_opts', {}),
|
||||
pattern = 'diffopt',
|
||||
callback = callback,
|
||||
})
|
||||
end,
|
||||
default = parse_diffopt,
|
||||
default_help = "derived from 'diffopt'",
|
||||
description = [[
|
||||
Diff options. If the default value is used, then changes to 'diffopt' are
|
||||
automatically applied.
|
||||
|
||||
Fields: ~
|
||||
• algorithm: string
|
||||
Diff algorithm to use. Values:
|
||||
• "myers" the default algorithm
|
||||
• "minimal" spend extra time to generate the
|
||||
smallest possible diff
|
||||
• "patience" patience diff algorithm
|
||||
• "histogram" histogram diff algorithm
|
||||
• internal: boolean
|
||||
Use Neovim's built in xdiff library for running diffs.
|
||||
• indent_heuristic: boolean
|
||||
Use the indent heuristic for the internal
|
||||
diff library.
|
||||
• vertical: boolean
|
||||
Start diff mode with vertical splits.
|
||||
• linematch: integer
|
||||
Enable second-stage diff on hunks to align lines.
|
||||
Requires `internal=true`.
|
||||
• ignore_blank_lines: boolean
|
||||
Ignore changes where lines are blank.
|
||||
• ignore_whitespace_change: boolean
|
||||
Ignore changes in amount of white space.
|
||||
It should ignore adding trailing white space,
|
||||
but not leading white space.
|
||||
• ignore_whitespace: boolean
|
||||
Ignore all white space changes.
|
||||
• ignore_whitespace_change_at_eol: boolean
|
||||
Ignore white space changes at end of line.
|
||||
]],
|
||||
},
|
||||
|
||||
base = {
|
||||
type = 'string',
|
||||
default = nil,
|
||||
default_help = 'index',
|
||||
description = [[
|
||||
The object/revision to diff against.
|
||||
See |gitsigns-revision|.
|
||||
]],
|
||||
},
|
||||
|
||||
count_chars = {
|
||||
type = 'table',
|
||||
default = {
|
||||
[1] = '1', -- '₁',
|
||||
[2] = '2', -- '₂',
|
||||
[3] = '3', -- '₃',
|
||||
[4] = '4', -- '₄',
|
||||
[5] = '5', -- '₅',
|
||||
[6] = '6', -- '₆',
|
||||
[7] = '7', -- '₇',
|
||||
[8] = '8', -- '₈',
|
||||
[9] = '9', -- '₉',
|
||||
['+'] = '>', -- '₊',
|
||||
},
|
||||
description = [[
|
||||
The count characters used when `signs.*.show_count` is enabled. The
|
||||
`+` entry is used as a fallback. With the default, any count outside
|
||||
of 1-9 uses the `>` character in the sign.
|
||||
|
||||
Possible use cases for this field:
|
||||
• to specify unicode characters for the counts instead of 1-9.
|
||||
• to define characters to be used for counts greater than 9.
|
||||
]],
|
||||
},
|
||||
|
||||
status_formatter = {
|
||||
type = 'function',
|
||||
--- @param status Gitsigns.StatusObj
|
||||
--- @return string
|
||||
default = function(status)
|
||||
local added, changed, removed = status.added, status.changed, status.removed
|
||||
local status_txt = {}
|
||||
if added and added > 0 then
|
||||
table.insert(status_txt, '+' .. added)
|
||||
end
|
||||
if changed and changed > 0 then
|
||||
table.insert(status_txt, '~' .. changed)
|
||||
end
|
||||
if removed and removed > 0 then
|
||||
table.insert(status_txt, '-' .. removed)
|
||||
end
|
||||
return table.concat(status_txt, ' ')
|
||||
end,
|
||||
default_help = [[function(status)
|
||||
local added, changed, removed = status.added, status.changed, status.removed
|
||||
local status_txt = {}
|
||||
if added and added > 0 then table.insert(status_txt, '+'..added ) end
|
||||
if changed and changed > 0 then table.insert(status_txt, '~'..changed) end
|
||||
if removed and removed > 0 then table.insert(status_txt, '-'..removed) end
|
||||
return table.concat(status_txt, ' ')
|
||||
end]],
|
||||
description = [[
|
||||
Function used to format `b:gitsigns_status`.
|
||||
]],
|
||||
},
|
||||
|
||||
max_file_length = {
|
||||
type = 'number',
|
||||
default = 40000,
|
||||
description = [[
|
||||
Max file length (in lines) to attach to.
|
||||
]],
|
||||
},
|
||||
|
||||
preview_config = {
|
||||
type = 'table',
|
||||
deep_extend = true,
|
||||
default = {
|
||||
border = 'single',
|
||||
style = 'minimal',
|
||||
relative = 'cursor',
|
||||
row = 0,
|
||||
col = 1,
|
||||
},
|
||||
description = [[
|
||||
Option overrides for the Gitsigns preview window. Table is passed directly
|
||||
to `nvim_open_win`.
|
||||
]],
|
||||
},
|
||||
|
||||
auto_attach = {
|
||||
type = 'boolean',
|
||||
default = true,
|
||||
description = [[
|
||||
Automatically attach to files.
|
||||
]],
|
||||
},
|
||||
|
||||
attach_to_untracked = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Attach to untracked files.
|
||||
]],
|
||||
},
|
||||
|
||||
update_debounce = {
|
||||
type = 'number',
|
||||
default = 100,
|
||||
description = [[
|
||||
Debounce time for updates (in milliseconds).
|
||||
]],
|
||||
},
|
||||
|
||||
current_line_blame = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Adds an unobtrusive and customisable blame annotation at the end of
|
||||
the current line.
|
||||
|
||||
The highlight group used for the text is `GitSignsCurrentLineBlame`.
|
||||
]],
|
||||
},
|
||||
|
||||
current_line_blame_opts = {
|
||||
type = 'table',
|
||||
deep_extend = true,
|
||||
default = {
|
||||
virt_text = true,
|
||||
virt_text_pos = 'eol',
|
||||
virt_text_priority = 100,
|
||||
delay = 1000,
|
||||
},
|
||||
description = [[
|
||||
Options for the current line blame annotation.
|
||||
|
||||
Fields: ~
|
||||
• virt_text: boolean
|
||||
Whether to show a virtual text blame annotation.
|
||||
• virt_text_pos: string
|
||||
Blame annotation position. Available values:
|
||||
`eol` Right after eol character.
|
||||
`overlay` Display over the specified column, without
|
||||
shifting the underlying text.
|
||||
`right_align` Display right aligned in the window.
|
||||
• delay: integer
|
||||
Sets the delay (in milliseconds) before blame virtual text is
|
||||
displayed.
|
||||
• ignore_whitespace: boolean
|
||||
Ignore whitespace when running blame.
|
||||
• virt_text_priority: integer
|
||||
Priority of virtual text.
|
||||
• extra_opts: string[]
|
||||
Extra options passed to `git-blame`.
|
||||
]],
|
||||
},
|
||||
|
||||
current_line_blame_formatter_opts = {
|
||||
type = 'table',
|
||||
deep_extend = true,
|
||||
deprecated = true,
|
||||
default = {
|
||||
relative_time = false,
|
||||
},
|
||||
description = [[
|
||||
Options for the current line blame annotation formatter.
|
||||
|
||||
Fields: ~
|
||||
• relative_time: boolean
|
||||
]],
|
||||
},
|
||||
|
||||
current_line_blame_formatter = {
|
||||
type = { 'string', 'function' },
|
||||
default = ' <author>, <author_time> - <summary> ',
|
||||
description = [[
|
||||
String or function used to format the virtual text of
|
||||
|gitsigns-config-current_line_blame|.
|
||||
|
||||
When a string, accepts the following format specifiers:
|
||||
|
||||
• `<abbrev_sha>`
|
||||
• `<orig_lnum>`
|
||||
• `<final_lnum>`
|
||||
• `<author>`
|
||||
• `<author_mail>`
|
||||
• `<author_time>` or `<author_time:FORMAT>`
|
||||
• `<author_tz>`
|
||||
• `<committer>`
|
||||
• `<committer_mail>`
|
||||
• `<committer_time>` or `<committer_time:FORMAT>`
|
||||
• `<committer_tz>`
|
||||
• `<summary>`
|
||||
• `<previous>`
|
||||
• `<filename>`
|
||||
|
||||
For `<author_time:FORMAT>` and `<committer_time:FORMAT>`, `FORMAT` can
|
||||
be any valid date format that is accepted by `os.date()` with the
|
||||
addition of `%R` (defaults to `%Y-%m-%d`):
|
||||
|
||||
• `%a` abbreviated weekday name (e.g., Wed)
|
||||
• `%A` full weekday name (e.g., Wednesday)
|
||||
• `%b` abbreviated month name (e.g., Sep)
|
||||
• `%B` full month name (e.g., September)
|
||||
• `%c` date and time (e.g., 09/16/98 23:48:10)
|
||||
• `%d` day of the month (16) [01-31]
|
||||
• `%H` hour, using a 24-hour clock (23) [00-23]
|
||||
• `%I` hour, using a 12-hour clock (11) [01-12]
|
||||
• `%M` minute (48) [00-59]
|
||||
• `%m` month (09) [01-12]
|
||||
• `%p` either "am" or "pm" (pm)
|
||||
• `%S` second (10) [00-61]
|
||||
• `%w` weekday (3) [0-6 = Sunday-Saturday]
|
||||
• `%x` date (e.g., 09/16/98)
|
||||
• `%X` time (e.g., 23:48:10)
|
||||
• `%Y` full year (1998)
|
||||
• `%y` two-digit year (98) [00-99]
|
||||
• `%%` the character `%´
|
||||
• `%R` relative (e.g., 4 months ago)
|
||||
|
||||
When a function:
|
||||
Parameters: ~
|
||||
{name} Git user name returned from `git config user.name` .
|
||||
{blame_info} Table with the following keys:
|
||||
• `abbrev_sha`: string
|
||||
• `orig_lnum`: integer
|
||||
• `final_lnum`: integer
|
||||
• `author`: string
|
||||
• `author_mail`: string
|
||||
• `author_time`: integer
|
||||
• `author_tz`: string
|
||||
• `committer`: string
|
||||
• `committer_mail`: string
|
||||
• `committer_time`: integer
|
||||
• `committer_tz`: string
|
||||
• `summary`: string
|
||||
• `previous`: string
|
||||
• `filename`: string
|
||||
• `boundary`: true?
|
||||
|
||||
Note that the keys map onto the output of:
|
||||
`git blame --line-porcelain`
|
||||
|
||||
{opts} Passed directly from
|
||||
|gitsigns-config-current_line_blame_formatter_opts|.
|
||||
|
||||
Return: ~
|
||||
The result of this function is passed directly to the `opts.virt_text`
|
||||
field of |nvim_buf_set_extmark| and thus must be a list of
|
||||
[text, highlight] tuples.
|
||||
]],
|
||||
},
|
||||
|
||||
current_line_blame_formatter_nc = {
|
||||
type = { 'string', 'function' },
|
||||
default = ' <author>',
|
||||
description = [[
|
||||
String or function used to format the virtual text of
|
||||
|gitsigns-config-current_line_blame| for lines that aren't committed.
|
||||
|
||||
See |gitsigns-config-current_line_blame_formatter| for more information.
|
||||
]],
|
||||
},
|
||||
|
||||
trouble = {
|
||||
type = 'boolean',
|
||||
default = function()
|
||||
local has_trouble = pcall(require, 'trouble')
|
||||
return has_trouble
|
||||
end,
|
||||
default_help = 'true if installed',
|
||||
description = [[
|
||||
When using setqflist() or setloclist(), open Trouble instead of the
|
||||
quickfix/location list window.
|
||||
]],
|
||||
},
|
||||
|
||||
yadm = {
|
||||
type = 'table',
|
||||
deprecated = {
|
||||
message = 'Please use |gitsigns-config-on_attach_pre| instead',
|
||||
},
|
||||
default = { enable = false },
|
||||
description = [[
|
||||
yadm configuration.
|
||||
]],
|
||||
},
|
||||
|
||||
_git_version = {
|
||||
type = 'string',
|
||||
default = 'auto',
|
||||
description = [[
|
||||
Version of git available. Set to 'auto' to automatically detect.
|
||||
]],
|
||||
},
|
||||
|
||||
_verbose = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
More verbose debug message. Requires debug_mode=true.
|
||||
]],
|
||||
},
|
||||
|
||||
_test_mode = {
|
||||
description = 'Enable test mode',
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
},
|
||||
|
||||
word_diff = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Highlight intra-line word differences in the buffer.
|
||||
Requires `config.diff_opts.internal = true` .
|
||||
|
||||
Uses the highlights:
|
||||
• For word diff in previews:
|
||||
• `GitSignsAddInline`
|
||||
• `GitSignsChangeInline`
|
||||
• `GitSignsDeleteInline`
|
||||
• For word diff in buffer:
|
||||
• `GitSignsAddLnInline`
|
||||
• `GitSignsChangeLnInline`
|
||||
• `GitSignsDeleteLnInline`
|
||||
• For word diff in virtual lines (e.g. show_deleted):
|
||||
• `GitSignsAddVirtLnInline`
|
||||
• `GitSignsChangeVirtLnInline`
|
||||
• `GitSignsDeleteVirtLnInline`
|
||||
]],
|
||||
},
|
||||
|
||||
_refresh_staged_on_update = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Always refresh the staged file on each update. Disabling this will cause
|
||||
the staged file to only be refreshed when an update to the index is
|
||||
detected.
|
||||
]],
|
||||
},
|
||||
|
||||
_threaded_diff = {
|
||||
type = 'boolean',
|
||||
default = true,
|
||||
description = [[
|
||||
Run diffs on a separate thread
|
||||
]],
|
||||
},
|
||||
|
||||
_inline2 = {
|
||||
type = 'boolean',
|
||||
default = true,
|
||||
description = [[
|
||||
Enable enhanced version of preview_hunk_inline()
|
||||
]],
|
||||
},
|
||||
|
||||
debug_mode = {
|
||||
type = 'boolean',
|
||||
default = false,
|
||||
description = [[
|
||||
Enables debug logging and makes the following functions
|
||||
available: `dump_cache`, `debug_messages`, `clear_debug`.
|
||||
]],
|
||||
},
|
||||
}
|
||||
|
||||
local function warn(s, ...)
|
||||
vim.notify(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' })
|
||||
end
|
||||
|
||||
--- @param config Gitsigns.Config
|
||||
local function validate_config(config)
|
||||
--- @diagnostic disable-next-line:no-unknown
|
||||
for k, v in pairs(config) do
|
||||
local kschema = M.schema[k]
|
||||
if kschema == nil then
|
||||
warn("gitsigns: Ignoring invalid configuration field '%s'", k)
|
||||
elseif kschema.type then
|
||||
if type(kschema.type) == 'string' then
|
||||
vim.validate({
|
||||
[k] = { v, kschema.type },
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param cfg table<any, any>
|
||||
local function handle_deprecated(cfg)
|
||||
for k, v in pairs(M.schema) do
|
||||
local dep = v.deprecated
|
||||
if dep and cfg[k] ~= nil then
|
||||
if type(dep) == 'table' then
|
||||
if dep.new_field then
|
||||
local opts_key, field = dep.new_field:match('(.*)%.(.*)')
|
||||
if opts_key and field then
|
||||
-- Field moved to an options table
|
||||
local opts = (cfg[opts_key] or {}) --[[@as table<any,any>]]
|
||||
opts[field] = cfg[k]
|
||||
cfg[opts_key] = opts
|
||||
else
|
||||
-- Field renamed
|
||||
cfg[dep.new_field] = cfg[k]
|
||||
end
|
||||
end
|
||||
|
||||
if dep.hard then
|
||||
if dep.message then
|
||||
warn(dep.message)
|
||||
elseif dep.new_field then
|
||||
warn('%s is now deprecated, please use %s', k, dep.new_field)
|
||||
else
|
||||
warn('%s is now deprecated; ignoring', k)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param k string
|
||||
--- @param v Gitsigns.SchemaElem
|
||||
--- @param user_val any
|
||||
local function build_field(k, v, user_val)
|
||||
local config = M.config --[[@as table<string,any>]]
|
||||
|
||||
if v.deep_extend then
|
||||
local d = resolve_default(v)
|
||||
config[k] = vim.tbl_deep_extend('force', d, user_val)
|
||||
else
|
||||
config[k] = user_val
|
||||
end
|
||||
end
|
||||
|
||||
--- @param user_config Gitsigns.Config|nil
|
||||
function M.build(user_config)
|
||||
user_config = user_config or {}
|
||||
|
||||
handle_deprecated(user_config)
|
||||
|
||||
validate_config(user_config)
|
||||
|
||||
for k, v in pairs(M.schema) do
|
||||
if user_config[k] ~= nil then
|
||||
build_field(k, v, user_config[k])
|
||||
if v.refresh then
|
||||
v.refresh(function()
|
||||
build_field(k, v, user_config[k])
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,242 @@
|
||||
local async = require('gitsigns.async')
|
||||
local cache = require('gitsigns.cache').cache
|
||||
local config = require('gitsigns.config').config
|
||||
local util = require('gitsigns.util')
|
||||
|
||||
local api = vim.api
|
||||
|
||||
local debounce = require('gitsigns.debounce')
|
||||
|
||||
local namespace = api.nvim_create_namespace('gitsigns_blame')
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @param bufnr integer
|
||||
local function reset(bufnr)
|
||||
if not api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
api.nvim_buf_del_extmark(bufnr, namespace, 1)
|
||||
vim.b[bufnr].gitsigns_blame_line_dict = nil
|
||||
end
|
||||
|
||||
--- @param fmt string
|
||||
--- @param name string
|
||||
--- @param info Gitsigns.BlameInfoPublic
|
||||
--- @return string
|
||||
local function expand_blame_format(fmt, name, info)
|
||||
if info.author == name then
|
||||
info.author = 'You'
|
||||
end
|
||||
return util.expand_format(fmt, info, config.current_line_blame_formatter_opts.relative_time)
|
||||
end
|
||||
|
||||
--- @param virt_text {[1]: string, [2]: string}[]
|
||||
--- @return string
|
||||
local function flatten_virt_text(virt_text)
|
||||
local res = {} ---@type string[]
|
||||
for _, part in ipairs(virt_text) do
|
||||
res[#res + 1] = part[1]
|
||||
end
|
||||
return table.concat(res)
|
||||
end
|
||||
|
||||
--- @param winid integer
|
||||
--- @return integer
|
||||
local function win_width()
|
||||
local winid = api.nvim_get_current_win()
|
||||
local wininfo = vim.fn.getwininfo(winid)[1]
|
||||
local textoff = wininfo and wininfo.textoff or 0
|
||||
return api.nvim_win_get_width(winid) - textoff
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param lnum integer
|
||||
--- @return integer
|
||||
local function line_len(bufnr, lnum)
|
||||
local line = api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1]
|
||||
return api.nvim_strwidth(line)
|
||||
end
|
||||
|
||||
--- @param fmt string
|
||||
--- @return Gitsigns.CurrentLineBlameFmtFun
|
||||
local function default_formatter(fmt)
|
||||
return function(username, blame_info, _opts)
|
||||
return {
|
||||
{
|
||||
expand_blame_format(fmt, username, blame_info),
|
||||
'GitSignsCurrentLineBlame',
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param blame_info Gitsigns.BlameInfoPublic
|
||||
---@return {[1]: string, [2]:string}[]
|
||||
local function get_blame_virt_text(bufnr, blame_info)
|
||||
local git_obj = assert(cache[bufnr]).git_obj
|
||||
|
||||
local clb_formatter = blame_info.author == 'Not Committed Yet'
|
||||
and config.current_line_blame_formatter_nc
|
||||
or config.current_line_blame_formatter
|
||||
|
||||
if type(clb_formatter) == 'string' then
|
||||
clb_formatter = default_formatter(clb_formatter)
|
||||
end
|
||||
|
||||
return clb_formatter(git_obj.repo.username, blame_info, config.current_line_blame_formatter_opts)
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param lnum integer
|
||||
--- @param blame_info Gitsigns.BlameInfo
|
||||
--- @param opts Gitsigns.CurrentLineBlameOpts
|
||||
local function handle_blame_info(bufnr, lnum, blame_info, opts)
|
||||
blame_info = util.convert_blame_info(blame_info)
|
||||
|
||||
local virt_text = get_blame_virt_text(bufnr, blame_info)
|
||||
local virt_text_str = flatten_virt_text(virt_text)
|
||||
|
||||
vim.b[bufnr].gitsigns_blame_line_dict = blame_info
|
||||
vim.b[bufnr].gitsigns_blame_line = virt_text_str
|
||||
|
||||
if opts.virt_text then
|
||||
local virt_text_pos = opts.virt_text_pos
|
||||
if virt_text_pos == 'right_align' then
|
||||
if api.nvim_strwidth(virt_text_str) > (win_width() - line_len(bufnr, lnum)) then
|
||||
virt_text_pos = 'eol'
|
||||
end
|
||||
end
|
||||
api.nvim_buf_set_extmark(bufnr, namespace, lnum - 1, 0, {
|
||||
id = 1,
|
||||
virt_text = virt_text,
|
||||
virt_text_pos = virt_text_pos,
|
||||
priority = opts.virt_text_priority,
|
||||
hl_mode = 'combine',
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
--- @param winid integer
|
||||
--- @return integer lnum
|
||||
local function get_lnum(winid)
|
||||
return api.nvim_win_get_cursor(winid)[1]
|
||||
end
|
||||
|
||||
--- @param winid integer
|
||||
--- @param lnum integer
|
||||
--- @return boolean
|
||||
local function foldclosed(winid, lnum)
|
||||
---@return boolean
|
||||
return api.nvim_win_call(winid, function()
|
||||
return vim.fn.foldclosed(lnum) ~= -1
|
||||
end)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
local function insert_mode()
|
||||
return api.nvim_get_mode().mode == 'i'
|
||||
end
|
||||
|
||||
--- Update function, must be called in async context
|
||||
--- @param bufnr integer
|
||||
local function update0(bufnr)
|
||||
async.scheduler()
|
||||
if not api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
if insert_mode() then
|
||||
return
|
||||
end
|
||||
|
||||
local winid = api.nvim_get_current_win()
|
||||
|
||||
if bufnr ~= api.nvim_win_get_buf(winid) then
|
||||
return
|
||||
end
|
||||
|
||||
local lnum = get_lnum(winid)
|
||||
|
||||
-- Can't show extmarks on folded lines so skip
|
||||
if foldclosed(winid, lnum) then
|
||||
return
|
||||
end
|
||||
|
||||
local bcache = cache[bufnr]
|
||||
if not bcache or not bcache.git_obj.object_name then
|
||||
return
|
||||
end
|
||||
|
||||
local opts = config.current_line_blame_opts
|
||||
|
||||
local blame_info = bcache:get_blame(lnum, opts)
|
||||
|
||||
if not api.nvim_win_is_valid(winid) or bufnr ~= api.nvim_win_get_buf(winid) then
|
||||
return
|
||||
end
|
||||
|
||||
if not blame_info then
|
||||
return
|
||||
end
|
||||
|
||||
if lnum ~= get_lnum(winid) then
|
||||
-- Cursor has moved during events; abort and tr-trigger another update
|
||||
update0(bufnr)
|
||||
return
|
||||
end
|
||||
|
||||
handle_blame_info(bufnr, lnum, blame_info, opts)
|
||||
end
|
||||
|
||||
local update = async.create(1, debounce.throttle_by_id(update0))
|
||||
|
||||
--- @type fun(bufnr: integer)
|
||||
local update_debounced
|
||||
|
||||
function M.setup()
|
||||
local group = api.nvim_create_augroup('gitsigns_blame', {})
|
||||
|
||||
local opts = config.current_line_blame_opts
|
||||
update_debounced = debounce.debounce_trailing(opts.delay, update)
|
||||
|
||||
for k, _ in pairs(cache) do
|
||||
reset(k)
|
||||
end
|
||||
|
||||
if config.current_line_blame then
|
||||
local events = { 'FocusGained', 'BufEnter', 'CursorMoved', 'CursorMovedI' }
|
||||
if vim.fn.exists('#WinResized') == 1 then
|
||||
-- For nvim 0.9+
|
||||
events[#events + 1] = 'WinResized'
|
||||
end
|
||||
|
||||
api.nvim_create_autocmd(events, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
reset(args.buf)
|
||||
update_debounced(args.buf)
|
||||
end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd({ 'InsertEnter', 'FocusLost', 'BufLeave' }, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
reset(args.buf)
|
||||
end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd('OptionSet', {
|
||||
group = group,
|
||||
pattern = { 'fileformat', 'bomb', 'eol' },
|
||||
callback = function(args)
|
||||
reset(args.buf)
|
||||
end,
|
||||
})
|
||||
|
||||
update_debounced(api.nvim_get_current_buf())
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,74 @@
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Debounces a function on the trailing edge.
|
||||
---
|
||||
--- @generic F: function
|
||||
--- @param ms number Timeout in ms
|
||||
--- @param fn F Function to debounce
|
||||
--- @param hash? integer|fun(...): any Function that determines id from arguments to fn
|
||||
--- @return F Debounced function.
|
||||
function M.debounce_trailing(ms, fn, hash)
|
||||
local running = {} --- @type table<any,uv.uv_timer_t>
|
||||
if type(hash) == 'number' then
|
||||
local hash_i = hash
|
||||
hash = function(...)
|
||||
return select(hash_i, ...)
|
||||
end
|
||||
end
|
||||
return function(...)
|
||||
local id = hash and hash(...) or true
|
||||
if running[id] == nil then
|
||||
running[id] = assert(uv.new_timer())
|
||||
end
|
||||
local timer = running[id]
|
||||
local argv = { ... }
|
||||
timer:start(ms, 0, function()
|
||||
timer:stop()
|
||||
running[id] = nil
|
||||
fn(unpack(argv, 1, table.maxn(argv)))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- Throttles a function using the first argument as an ID
|
||||
---
|
||||
--- If function is already running then the function will be scheduled to run
|
||||
--- again once the running call has finished.
|
||||
---
|
||||
--- fn#1 _/‾\__/‾\_/‾\_____________________________
|
||||
--- throttled#1 _/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\/‾‾‾‾‾‾‾‾‾‾\____________
|
||||
--
|
||||
--- fn#2 ______/‾\___________/‾\___________________
|
||||
--- throttled#2 ______/‾‾‾‾‾‾‾‾‾‾\__/‾‾‾‾‾‾‾‾‾‾\__________
|
||||
---
|
||||
---
|
||||
--- @generic F: function
|
||||
--- @param fn F Function to throttle
|
||||
--- @param schedule? boolean
|
||||
--- @return F throttled function.
|
||||
function M.throttle_by_id(fn, schedule)
|
||||
local scheduled = {} --- @type table<any,boolean>
|
||||
local running = {} --- @type table<any,boolean>
|
||||
return function(id, ...)
|
||||
if scheduled[id] then
|
||||
-- If fn is already scheduled, then drop
|
||||
return
|
||||
end
|
||||
if not running[id] or schedule then
|
||||
scheduled[id] = true
|
||||
end
|
||||
if running[id] then
|
||||
return
|
||||
end
|
||||
while scheduled[id] do
|
||||
scheduled[id] = nil
|
||||
running[id] = true
|
||||
fn(id, ...)
|
||||
running[id] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,62 @@
|
||||
local log = require('gitsigns.debug.log')
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @param raw_item any
|
||||
--- @param path string[]
|
||||
--- @return any
|
||||
local function process(raw_item, path)
|
||||
--- @diagnostic disable-next-line:undefined-field
|
||||
if path[#path] == vim.inspect.METATABLE then
|
||||
return
|
||||
elseif type(raw_item) == 'function' then
|
||||
return
|
||||
elseif type(raw_item) ~= 'table' then
|
||||
return raw_item
|
||||
end
|
||||
--- @cast raw_item table<any,any>
|
||||
|
||||
local key = path[#path]
|
||||
if
|
||||
vim.tbl_contains({
|
||||
'compare_text',
|
||||
'compare_text_head',
|
||||
'hunks',
|
||||
'hunks_staged',
|
||||
'staged_diffs',
|
||||
}, key)
|
||||
then
|
||||
return { '...', length = #vim.tbl_keys(raw_item), head = raw_item[next(raw_item)] }
|
||||
elseif key == 'blame' then
|
||||
return { '...', length = #vim.tbl_keys(raw_item) }
|
||||
end
|
||||
|
||||
return raw_item
|
||||
end
|
||||
|
||||
--- @return any
|
||||
function M.dump_cache()
|
||||
-- TODO(lewis6991): hack: use package.loaded to avoid circular deps
|
||||
local cache = (require('gitsigns.cache')).cache
|
||||
--- @type string
|
||||
local text = vim.inspect(cache, { process = process })
|
||||
vim.api.nvim_echo({ { text } }, false, {})
|
||||
end
|
||||
|
||||
--- @param noecho boolean
|
||||
--- @return string[]?
|
||||
function M.debug_messages(noecho)
|
||||
if noecho then
|
||||
return log.messages
|
||||
else
|
||||
for _, m in ipairs(log.messages) do
|
||||
vim.api.nvim_echo({ { m } }, false, {})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.clear_debug()
|
||||
log.messages = {}
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,144 @@
|
||||
local M = {
|
||||
debug_mode = false,
|
||||
verbose = false,
|
||||
messages = {}, --- @type string[]
|
||||
}
|
||||
|
||||
--- @param name string
|
||||
--- @param lvl integer
|
||||
--- @return any
|
||||
local function getvarvalue(name, lvl)
|
||||
lvl = lvl + 1
|
||||
local value --- @type any?
|
||||
local found --- @type boolean?
|
||||
|
||||
-- try local variables
|
||||
local i = 1
|
||||
while true do
|
||||
local n, v = debug.getlocal(lvl, i)
|
||||
if not n then
|
||||
break
|
||||
end
|
||||
if n == name then
|
||||
value = v
|
||||
found = true
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
if found then
|
||||
return value
|
||||
end
|
||||
|
||||
-- try upvalues
|
||||
local func = debug.getinfo(lvl).func
|
||||
i = 1
|
||||
while true do
|
||||
local n, v = debug.getupvalue(func, i)
|
||||
if not n then
|
||||
break
|
||||
end
|
||||
if n == name then
|
||||
return v
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
-- not found; get global
|
||||
--- @diagnostic disable-next-line:deprecated
|
||||
return getfenv(func)[name]
|
||||
end
|
||||
|
||||
--- @param lvl integer
|
||||
--- @return {name:string, bufnr: integer}
|
||||
local function get_context(lvl)
|
||||
lvl = lvl + 1
|
||||
local ret = {} --- @type {name:string, bufnr: integer}
|
||||
ret.name = getvarvalue('__FUNC__', lvl)
|
||||
if not ret.name then
|
||||
local name0 = debug.getinfo(lvl, 'n').name or ''
|
||||
ret.name = name0:gsub('(.*)%d+$', '%1')
|
||||
end
|
||||
ret.bufnr = getvarvalue('bufnr', lvl)
|
||||
or getvarvalue('_bufnr', lvl)
|
||||
or getvarvalue('cbuf', lvl)
|
||||
or getvarvalue('buf', lvl)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
-- If called in a callback then make sure the callback defines a __FUNC__
|
||||
-- variable which can be used to identify the name of the function.
|
||||
--- @param obj any
|
||||
--- @param lvl integer
|
||||
local function cprint(obj, lvl)
|
||||
lvl = lvl + 1
|
||||
local msg = type(obj) == 'string' and obj or vim.inspect(obj)
|
||||
local ctx = get_context(lvl)
|
||||
local msg2 --- @type string
|
||||
if ctx.bufnr then
|
||||
msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg)
|
||||
else
|
||||
msg2 = string.format('%s: %s', ctx.name, msg)
|
||||
end
|
||||
table.insert(M.messages, msg2)
|
||||
end
|
||||
|
||||
function M.dprint(obj)
|
||||
if not M.debug_mode then
|
||||
return
|
||||
end
|
||||
cprint(obj, 2)
|
||||
end
|
||||
|
||||
function M.dprintf(obj, ...)
|
||||
if not M.debug_mode then
|
||||
return
|
||||
end
|
||||
cprint(obj:format(...), 2)
|
||||
end
|
||||
|
||||
function M.vprint(obj)
|
||||
if not (M.debug_mode and M.verbose) then
|
||||
return
|
||||
end
|
||||
cprint(obj, 2)
|
||||
end
|
||||
|
||||
function M.vprintf(obj, ...)
|
||||
if not (M.debug_mode and M.verbose) then
|
||||
return
|
||||
end
|
||||
cprint(obj:format(...), 2)
|
||||
end
|
||||
|
||||
local function eprint(msg, level)
|
||||
local info = debug.getinfo(level + 2, 'Sl')
|
||||
if info then
|
||||
msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg)
|
||||
end
|
||||
M.messages[#M.messages + 1] = debug.traceback(msg)
|
||||
if M.debug_mode then
|
||||
error(msg, 3)
|
||||
end
|
||||
end
|
||||
|
||||
function M.eprint(msg)
|
||||
eprint(msg, 1)
|
||||
end
|
||||
|
||||
function M.eprintf(fmt, ...)
|
||||
eprint(fmt:format(...), 1)
|
||||
end
|
||||
|
||||
--- @param cond boolean
|
||||
--- @param msg string
|
||||
--- @return boolean
|
||||
function M.assert(cond, msg)
|
||||
if not cond then
|
||||
eprint(msg, 1)
|
||||
end
|
||||
|
||||
return not cond
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,32 @@
|
||||
local config = require('gitsigns.config').config
|
||||
|
||||
--- @alias Gitsigns.Difffn fun(fa: string[], fb: string[], linematch?: integer): Gitsigns.Hunk.Hunk[]
|
||||
|
||||
--- @param a string[]
|
||||
--- @param b string[]
|
||||
--- @param linematch? boolean
|
||||
--- @return Gitsigns.Hunk.Hunk[] hunks
|
||||
return function(a, b, linematch)
|
||||
-- -- Short circuit optimization
|
||||
-- if not a or #a == 0 then
|
||||
-- local Hunks = require('gitsigns.hunks')
|
||||
-- local hunk = Hunks.create_hunk(0, 0, 1, #b)
|
||||
-- hunk.added.lines = b
|
||||
-- return { hunk }
|
||||
-- end
|
||||
|
||||
local diff_opts = config.diff_opts
|
||||
local f --- @type Gitsigns.Difffn
|
||||
if diff_opts.internal then
|
||||
f = require('gitsigns.diff_int').run_diff
|
||||
else
|
||||
f = require('gitsigns.diff_ext').run_diff
|
||||
end
|
||||
|
||||
local linematch0 --- @type integer?
|
||||
if linematch ~= false then
|
||||
linematch0 = diff_opts.linematch
|
||||
end
|
||||
|
||||
return f(a, b, linematch0)
|
||||
end
|
||||
@ -0,0 +1,80 @@
|
||||
local config = require('gitsigns.config').config
|
||||
local git_diff = require('gitsigns.git').diff
|
||||
|
||||
local gs_hunks = require('gitsigns.hunks')
|
||||
local util = require('gitsigns.util')
|
||||
local scheduler = require('gitsigns.async').scheduler
|
||||
|
||||
local M = {}
|
||||
-- Async function
|
||||
|
||||
--- @param path string
|
||||
--- @param text string[]
|
||||
local function write_to_file(path, text)
|
||||
local f, err = io.open(path, 'wb')
|
||||
if f == nil then
|
||||
error(err)
|
||||
end
|
||||
for _, l in ipairs(text) do
|
||||
f:write(l)
|
||||
f:write('\n')
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
|
||||
--- @async
|
||||
--- @param text_cmp string[]
|
||||
--- @param text_buf string[]
|
||||
--- @return Gitsigns.Hunk.Hunk[]
|
||||
function M.run_diff(text_cmp, text_buf)
|
||||
local results = {} --- @type Gitsigns.Hunk.Hunk[]
|
||||
|
||||
-- tmpname must not be called in a callback
|
||||
if vim.in_fast_event() then
|
||||
scheduler()
|
||||
end
|
||||
|
||||
local file_buf = util.tmpname()
|
||||
local file_cmp = util.tmpname()
|
||||
|
||||
write_to_file(file_buf, text_buf)
|
||||
write_to_file(file_cmp, text_cmp)
|
||||
|
||||
-- Taken from gitgutter, diff.vim:
|
||||
--
|
||||
-- If a file has CRLF line endings and git's core.autocrlf is true, the file
|
||||
-- in git's object store will have LF line endings. Writing it out via
|
||||
-- git-show will produce a file with LF line endings.
|
||||
--
|
||||
-- If this last file is one of the files passed to git-diff, git-diff will
|
||||
-- convert its line endings to CRLF before diffing -- which is what we want
|
||||
-- but also by default outputs a warning on stderr.
|
||||
--
|
||||
-- warning: LF will be replace by CRLF in <temp file>.
|
||||
-- The file will have its original line endings in your working directory.
|
||||
--
|
||||
-- We can safely ignore the warning, we turn it off by passing the '-c
|
||||
-- "core.safecrlf=false"' argument to git-diff.
|
||||
|
||||
local opts = config.diff_opts
|
||||
local out = git_diff(file_cmp, file_buf, opts.indent_heuristic, opts.algorithm)
|
||||
|
||||
for _, line in ipairs(out) do
|
||||
if vim.startswith(line, '@@') then
|
||||
results[#results + 1] = gs_hunks.parse_diff_line(line)
|
||||
elseif #results > 0 then
|
||||
local r = results[#results]
|
||||
if line:sub(1, 1) == '-' then
|
||||
r.removed.lines[#r.removed.lines + 1] = line:sub(2)
|
||||
elseif line:sub(1, 1) == '+' then
|
||||
r.added.lines[#r.added.lines + 1] = line:sub(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
os.remove(file_buf)
|
||||
os.remove(file_cmp)
|
||||
return results
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,191 @@
|
||||
local create_hunk = require('gitsigns.hunks').create_hunk
|
||||
local config = require('gitsigns.config').config
|
||||
local async = require('gitsigns.async')
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @alias Gitsigns.Region {[1]:integer, [2]:string, [3]:integer, [4]:integer}
|
||||
|
||||
--- @alias Gitsigns.RawHunk {[1]:integer, [2]:integer, [3]:integer, [4]:integer}
|
||||
--- @alias Gitsigns.RawDifffn fun(a: string, b: string, linematch?: integer): Gitsigns.RawHunk[]
|
||||
|
||||
--- @type Gitsigns.RawDifffn
|
||||
local run_diff_xdl = function(a, b, linematch)
|
||||
local opts = config.diff_opts
|
||||
return vim.diff(a, b, {
|
||||
result_type = 'indices',
|
||||
algorithm = opts.algorithm,
|
||||
indent_heuristic = opts.indent_heuristic,
|
||||
ignore_whitespace = opts.ignore_whitespace,
|
||||
ignore_whitespace_change = opts.ignore_whitespace_change,
|
||||
ignore_whitespace_change_at_eol = opts.ignore_whitespace_change_at_eol,
|
||||
ignore_blank_lines = opts.ignore_blank_lines,
|
||||
linematch = linematch,
|
||||
}) --[[@as Gitsigns.RawHunk[] ]]
|
||||
end
|
||||
|
||||
--- @type Gitsigns.RawDifffn
|
||||
local run_diff_xdl_async = async.wrap(
|
||||
4,
|
||||
--- @param a string
|
||||
--- @param b string
|
||||
--- @param linematch? integer
|
||||
--- @param callback fun(hunks: Gitsigns.RawHunk[])
|
||||
function(a, b, linematch, callback)
|
||||
local opts = config.diff_opts
|
||||
local function toflag(f, pos)
|
||||
return f and bit.lshift(1, pos) or 0
|
||||
end
|
||||
|
||||
local flags = toflag(opts.indent_heuristic, 0)
|
||||
+ toflag(opts.ignore_whitespace, 1)
|
||||
+ toflag(opts.ignore_whitespace_change, 2)
|
||||
+ toflag(opts.ignore_whitespace_change_at_eol, 3)
|
||||
+ toflag(opts.ignore_blank_lines, 4)
|
||||
|
||||
vim.loop
|
||||
.new_work(
|
||||
--- @param a0 string
|
||||
--- @param b0 string
|
||||
--- @param algorithm string
|
||||
--- @param flags0 integer
|
||||
--- @param linematch0 integer
|
||||
--- @return string
|
||||
function(a0, b0, algorithm, flags0, linematch0)
|
||||
local function flagval(pos)
|
||||
return bit.band(flags0, bit.lshift(1, pos)) ~= 0
|
||||
end
|
||||
|
||||
--- @diagnostic disable-next-line:redundant-return-value
|
||||
return vim.mpack.encode(vim.diff(a0, b0, {
|
||||
result_type = 'indices',
|
||||
algorithm = algorithm,
|
||||
linematch = linematch0,
|
||||
indent_heuristic = flagval(0),
|
||||
ignore_whitespace = flagval(1),
|
||||
ignore_whitespace_change = flagval(2),
|
||||
ignore_whitespace_change_at_eol = flagval(3),
|
||||
ignore_blank_lines = flagval(4),
|
||||
}))
|
||||
end,
|
||||
--- @param r string
|
||||
function(r)
|
||||
callback(vim.mpack.decode(r) --[[@as Gitsigns.RawHunk[] ]])
|
||||
end
|
||||
)
|
||||
:queue(a, b, opts.algorithm, flags, linematch)
|
||||
end
|
||||
)
|
||||
|
||||
--- @param fa string[]
|
||||
--- @param fb string[]
|
||||
--- @param linematch? integer
|
||||
--- @return Gitsigns.Hunk.Hunk[]
|
||||
function M.run_diff(fa, fb, linematch)
|
||||
local run_diff0 --- @type Gitsigns.RawDifffn
|
||||
if config._threaded_diff and vim.is_thread then
|
||||
run_diff0 = run_diff_xdl_async
|
||||
else
|
||||
run_diff0 = run_diff_xdl
|
||||
end
|
||||
|
||||
local a = table.concat(fa, '\n')
|
||||
local b = table.concat(fb, '\n')
|
||||
|
||||
local results = run_diff0(a, b, linematch)
|
||||
|
||||
local hunks = {} --- @type Gitsigns.Hunk.Hunk[]
|
||||
for _, r in ipairs(results) do
|
||||
local rs, rc, as, ac = r[1], r[2], r[3], r[4]
|
||||
local hunk = create_hunk(rs, rc, as, ac)
|
||||
if rc > 0 then
|
||||
for i = rs, rs + rc - 1 do
|
||||
hunk.removed.lines[#hunk.removed.lines + 1] = fa[i] or ''
|
||||
end
|
||||
if rs + rc >= #fa and fa[#fa] ~= '' then
|
||||
hunk.removed.no_nl_at_eof = true
|
||||
end
|
||||
end
|
||||
if ac > 0 then
|
||||
for i = as, as + ac - 1 do
|
||||
hunk.added.lines[#hunk.added.lines + 1] = fb[i] or ''
|
||||
end
|
||||
if as + ac >= #fb and fb[#fb] ~= '' then
|
||||
hunk.added.no_nl_at_eof = true
|
||||
end
|
||||
end
|
||||
hunks[#hunks + 1] = hunk
|
||||
end
|
||||
|
||||
return hunks
|
||||
end
|
||||
|
||||
local gaps_between_regions = 5
|
||||
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @return Gitsigns.Hunk.Hunk[]
|
||||
local function denoise_hunks(hunks)
|
||||
-- Denoise the hunks
|
||||
local ret = { hunks[1] } --- @type Gitsigns.Hunk.Hunk[]
|
||||
for j = 2, #hunks do
|
||||
local h, n = ret[#ret], hunks[j]
|
||||
if not h or not n then
|
||||
break
|
||||
end
|
||||
if n.added.start - h.added.start - h.added.count < gaps_between_regions then
|
||||
h.added.count = n.added.start + n.added.count - h.added.start
|
||||
h.removed.count = n.removed.start + n.removed.count - h.removed.start
|
||||
|
||||
if h.added.count > 0 or h.removed.count > 0 then
|
||||
h.type = 'change'
|
||||
end
|
||||
else
|
||||
ret[#ret + 1] = n
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
--- @param removed string[]
|
||||
--- @param added string[]
|
||||
--- @return Gitsigns.Region[] removed
|
||||
--- @return Gitsigns.Region[] added
|
||||
function M.run_word_diff(removed, added)
|
||||
local adds = {} --- @type Gitsigns.Region[]
|
||||
local rems = {} --- @type Gitsigns.Region[]
|
||||
|
||||
if #removed ~= #added then
|
||||
return rems, adds
|
||||
end
|
||||
|
||||
for i = 1, #removed do
|
||||
-- pair lines by position
|
||||
local a = table.concat(vim.split(removed[i], ''), '\n')
|
||||
local b = table.concat(vim.split(added[i], ''), '\n')
|
||||
|
||||
local hunks = {} --- @type Gitsigns.Hunk.Hunk[]
|
||||
for _, r in ipairs(run_diff_xdl(a, b)) do
|
||||
local rs, rc, as, ac = r[1], r[2], r[3], r[4]
|
||||
|
||||
-- Balance of the unknown offset done in hunk_func
|
||||
if rc == 0 then
|
||||
rs = rs + 1
|
||||
end
|
||||
if ac == 0 then
|
||||
as = as + 1
|
||||
end
|
||||
|
||||
hunks[#hunks + 1] = create_hunk(rs, rc, as, ac)
|
||||
end
|
||||
|
||||
hunks = denoise_hunks(hunks)
|
||||
|
||||
for _, h in ipairs(hunks) do
|
||||
adds[#adds + 1] = { i, h.type, h.added.start, h.added.start + h.added.count }
|
||||
rems[#rems + 1] = { i, h.type, h.removed.start, h.removed.start + h.removed.count }
|
||||
end
|
||||
end
|
||||
return rems, adds
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,234 @@
|
||||
local api = vim.api
|
||||
|
||||
local async = require('gitsigns.async')
|
||||
local cache = require('gitsigns.cache').cache
|
||||
local util = require('gitsigns.util')
|
||||
local manager = require('gitsigns.manager')
|
||||
local message = require('gitsigns.message')
|
||||
local Status = require('gitsigns.status')
|
||||
|
||||
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @async
|
||||
--- @param bufnr integer
|
||||
--- @param dbufnr integer
|
||||
--- @param base string?
|
||||
local function bufread(bufnr, dbufnr, base)
|
||||
local bcache = cache[bufnr]
|
||||
base = util.norm_base(base)
|
||||
local text --- @type string[]
|
||||
if base == bcache.git_obj.revision then
|
||||
text = assert(bcache.compare_text)
|
||||
else
|
||||
local err
|
||||
text, err = bcache.git_obj:get_show_text(base)
|
||||
if err then
|
||||
error(err, 2)
|
||||
end
|
||||
async.scheduler()
|
||||
if not api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
vim.bo[dbufnr].fileformat = vim.bo[bufnr].fileformat
|
||||
vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype
|
||||
vim.bo[dbufnr].bufhidden = 'wipe'
|
||||
|
||||
local modifiable = vim.bo[dbufnr].modifiable
|
||||
vim.bo[dbufnr].modifiable = true
|
||||
Status:update(dbufnr, { head = base })
|
||||
|
||||
util.set_lines(dbufnr, 0, -1, text)
|
||||
|
||||
vim.bo[dbufnr].modifiable = modifiable
|
||||
vim.bo[dbufnr].modified = false
|
||||
require('gitsigns.attach').attach(dbufnr, nil, 'BufReadCmd')
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param dbufnr integer
|
||||
--- @param base string?
|
||||
local bufwrite = async.create(3, function(bufnr, dbufnr, base)
|
||||
local bcache = cache[bufnr]
|
||||
local buftext = util.buf_lines(dbufnr)
|
||||
base = util.norm_base(base)
|
||||
bcache.git_obj:stage_lines(buftext)
|
||||
async.scheduler()
|
||||
if not api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
vim.bo[dbufnr].modified = false
|
||||
-- If diff buffer base matches the git_obj revision then also update the
|
||||
-- signs.
|
||||
if base == bcache.git_obj.revision then
|
||||
bcache.compare_text = buftext
|
||||
manager.update(bufnr)
|
||||
end
|
||||
end)
|
||||
|
||||
--- @async
|
||||
--- Create a gitsigns buffer for a certain revision of a file
|
||||
--- @param bufnr integer
|
||||
--- @param base string?
|
||||
--- @return string? buf Buffer name
|
||||
local function create_show_buf(bufnr, base)
|
||||
local bcache = assert(cache[bufnr])
|
||||
base = util.norm_base(base)
|
||||
|
||||
local bufname = bcache:get_rev_bufname(base)
|
||||
|
||||
if util.bufexists(bufname) then
|
||||
return bufname
|
||||
end
|
||||
|
||||
local dbuf = api.nvim_create_buf(false, true)
|
||||
api.nvim_buf_set_name(dbuf, bufname)
|
||||
|
||||
local ok, err = pcall(bufread, bufnr, dbuf, base)
|
||||
if not ok then
|
||||
message.error(err --[[@as string]])
|
||||
async.scheduler()
|
||||
api.nvim_buf_delete(dbuf, { force = true })
|
||||
return
|
||||
end
|
||||
|
||||
-- allow editing the index revision
|
||||
if not bcache.git_obj.revision then
|
||||
vim.bo[dbuf].buftype = 'acwrite'
|
||||
|
||||
api.nvim_create_autocmd('BufReadCmd', {
|
||||
group = 'gitsigns',
|
||||
buffer = dbuf,
|
||||
callback = function()
|
||||
async.run(bufread, bufnr, dbuf, base)
|
||||
end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd('BufWriteCmd', {
|
||||
group = 'gitsigns',
|
||||
buffer = dbuf,
|
||||
callback = function()
|
||||
bufwrite(bufnr, dbuf, base)
|
||||
end,
|
||||
})
|
||||
else
|
||||
vim.bo[dbuf].buftype = 'nowrite'
|
||||
vim.bo[dbuf].modifiable = false
|
||||
end
|
||||
|
||||
return bufname
|
||||
end
|
||||
|
||||
--- @class Gitsigns.DiffthisOpts
|
||||
--- @field vertical boolean
|
||||
--- @field split string
|
||||
|
||||
--- @async
|
||||
--- @param base string?
|
||||
--- @param opts? Gitsigns.DiffthisOpts
|
||||
local function diffthis_rev(base, opts)
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
|
||||
local bufname = create_show_buf(bufnr, base)
|
||||
if not bufname then
|
||||
return
|
||||
end
|
||||
|
||||
opts = opts or {}
|
||||
|
||||
vim.cmd(table.concat({
|
||||
'keepalt',
|
||||
opts.split or 'aboveleft',
|
||||
opts.vertical and 'vertical' or '',
|
||||
'diffsplit',
|
||||
bufname,
|
||||
}, ' '))
|
||||
end
|
||||
|
||||
--- @param base string?
|
||||
--- @param opts Gitsigns.DiffthisOpts
|
||||
M.diffthis = async.create(2, function(base, opts)
|
||||
if vim.wo.diff then
|
||||
return
|
||||
end
|
||||
|
||||
local bufnr = api.nvim_get_current_buf()
|
||||
local bcache = cache[bufnr]
|
||||
if not bcache then
|
||||
return
|
||||
end
|
||||
|
||||
local cwin = api.nvim_get_current_win()
|
||||
if not base and bcache.git_obj.has_conflicts then
|
||||
diffthis_rev(':2', opts)
|
||||
api.nvim_set_current_win(cwin)
|
||||
opts.split = 'belowright'
|
||||
diffthis_rev(':3', opts)
|
||||
else
|
||||
diffthis_rev(base, opts)
|
||||
end
|
||||
api.nvim_set_current_win(cwin)
|
||||
end)
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param base string
|
||||
M.show = async.create(2, function(bufnr, base)
|
||||
local bufname = create_show_buf(bufnr, base)
|
||||
if not bufname then
|
||||
return
|
||||
end
|
||||
|
||||
vim.cmd.edit(bufname)
|
||||
end)
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @return boolean
|
||||
local function should_reload(bufnr)
|
||||
if not vim.bo[bufnr].modified then
|
||||
return true
|
||||
end
|
||||
local response --- @type string?
|
||||
while not vim.tbl_contains({ 'O', 'L' }, response) do
|
||||
response = async.wait(2, vim.ui.input, {
|
||||
prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:',
|
||||
})
|
||||
end
|
||||
return response == 'L'
|
||||
end
|
||||
|
||||
-- This function needs to be throttled as there is a call to vim.ui.input
|
||||
--- @param bufnr integer
|
||||
M.update = throttle_by_id(async.create(1, function(bufnr)
|
||||
if not vim.wo.diff then
|
||||
return
|
||||
end
|
||||
|
||||
local bcache = cache[bufnr]
|
||||
|
||||
-- Note this will be the bufname for the currently set base
|
||||
-- which are the only ones we want to update
|
||||
local bufname = bcache:get_rev_bufname()
|
||||
|
||||
for _, w in ipairs(api.nvim_list_wins()) do
|
||||
if api.nvim_win_is_valid(w) then
|
||||
local b = api.nvim_win_get_buf(w)
|
||||
local bname = api.nvim_buf_get_name(b)
|
||||
local is_fugitive_diff_window = vim.startswith(bname, 'fugitive://')
|
||||
and vim.fn.exists('*FugitiveParse')
|
||||
and vim.fn.FugitiveParse(bname)[1] ~= ':'
|
||||
if bname == bufname or is_fugitive_diff_window then
|
||||
if should_reload(b) then
|
||||
api.nvim_buf_call(b, function()
|
||||
vim.cmd.doautocmd('BufReadCmd')
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end))
|
||||
|
||||
return M
|
||||
@ -0,0 +1,905 @@
|
||||
local async = require('gitsigns.async')
|
||||
local scheduler = require('gitsigns.async').scheduler
|
||||
|
||||
local log = require('gitsigns.debug.log')
|
||||
local util = require('gitsigns.util')
|
||||
local system = require('gitsigns.system').system
|
||||
|
||||
local gs_config = require('gitsigns.config')
|
||||
local config = gs_config.config
|
||||
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
local dprint = log.dprint
|
||||
local dprintf = log.dprintf
|
||||
local error_once = require('gitsigns.message').error_once
|
||||
|
||||
local check_version = require('gitsigns.git.version').check
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted
|
||||
local asystem = async.wrap(3, system)
|
||||
|
||||
--- @param file string
|
||||
--- @return boolean
|
||||
local function in_git_dir(file)
|
||||
for _, p in ipairs(vim.split(file, util.path_sep)) do
|
||||
if p == '.git' then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- @class Gitsigns.GitObj
|
||||
--- @field file string
|
||||
--- @field encoding string
|
||||
--- @field i_crlf boolean Object has crlf
|
||||
--- @field w_crlf boolean Working copy has crlf
|
||||
--- @field mode_bits string
|
||||
--- @field revision? string Revision the object is tracking against. Nil for index
|
||||
--- @field object_name string The fixed object name to use.
|
||||
--- @field relpath string
|
||||
--- @field orig_relpath? string Use for tracking moved files
|
||||
--- @field repo Gitsigns.Repo
|
||||
--- @field has_conflicts? boolean
|
||||
local Obj = {}
|
||||
|
||||
M.Obj = Obj
|
||||
|
||||
--- @class Gitsigns.RepoInfo
|
||||
--- @field gitdir string
|
||||
--- @field toplevel string
|
||||
--- @field detached boolean
|
||||
--- @field abbrev_head string
|
||||
|
||||
--- @class Gitsigns.Repo : Gitsigns.RepoInfo
|
||||
--- @field username string
|
||||
local Repo = {}
|
||||
M.Repo = Repo
|
||||
|
||||
--- @class Gitsigns.Git.JobSpec : vim.SystemOpts
|
||||
--- @field command? string
|
||||
--- @field ignore_error? boolean
|
||||
|
||||
--- @async
|
||||
--- @param args string[]
|
||||
--- @param spec? Gitsigns.Git.JobSpec
|
||||
--- @return string[] stdout, string? stderr
|
||||
local function git_command(args, spec)
|
||||
spec = spec or {}
|
||||
|
||||
local cmd = {
|
||||
spec.command or 'git',
|
||||
'--no-pager',
|
||||
'--no-optional-locks',
|
||||
'--literal-pathspecs',
|
||||
'-c',
|
||||
'gc.auto=0', -- Disable auto-packing which emits messages to stderr
|
||||
unpack(args),
|
||||
}
|
||||
|
||||
if spec.text == nil then
|
||||
spec.text = true
|
||||
end
|
||||
|
||||
-- Fix #895. Only needed for Nvim 0.9 and older
|
||||
spec.clear_env = true
|
||||
|
||||
--- @type vim.SystemCompleted
|
||||
local obj = asystem(cmd, spec)
|
||||
local stdout = obj.stdout
|
||||
local stderr = obj.stderr
|
||||
|
||||
if not spec.ignore_error and obj.code > 0 then
|
||||
local cmd_str = table.concat(cmd, ' ')
|
||||
log.eprintf("Received exit code %d when running command\n'%s':\n%s", obj.code, cmd_str, stderr)
|
||||
end
|
||||
|
||||
local stdout_lines = vim.split(stdout or '', '\n')
|
||||
|
||||
if spec.text then
|
||||
-- If stdout ends with a newline, then remove the final empty string after
|
||||
-- the split
|
||||
if stdout_lines[#stdout_lines] == '' then
|
||||
stdout_lines[#stdout_lines] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if log.verbose then
|
||||
log.vprintf('%d lines:', #stdout_lines)
|
||||
for i = 1, math.min(10, #stdout_lines) do
|
||||
log.vprintf('\t%s', stdout_lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
if stderr == '' then
|
||||
stderr = nil
|
||||
end
|
||||
|
||||
return stdout_lines, stderr
|
||||
end
|
||||
|
||||
--- @async
|
||||
--- @param file_cmp string
|
||||
--- @param file_buf string
|
||||
--- @param indent_heuristic? boolean
|
||||
--- @param diff_algo string
|
||||
--- @return string[] stdout, string? stderr
|
||||
function M.diff(file_cmp, file_buf, indent_heuristic, diff_algo)
|
||||
return git_command({
|
||||
'-c',
|
||||
'core.safecrlf=false',
|
||||
'diff',
|
||||
'--color=never',
|
||||
'--' .. (indent_heuristic and '' or 'no-') .. 'indent-heuristic',
|
||||
'--diff-algorithm=' .. diff_algo,
|
||||
'--patch-with-raw',
|
||||
'--unified=0',
|
||||
file_cmp,
|
||||
file_buf,
|
||||
}, {
|
||||
-- git-diff implies --exit-code
|
||||
ignore_error = true,
|
||||
})
|
||||
end
|
||||
|
||||
--- @async
|
||||
--- @param gitdir string
|
||||
--- @param head_str string
|
||||
--- @param cwd string
|
||||
--- @param cmd? string
|
||||
--- @return string
|
||||
local function process_abbrev_head(gitdir, head_str, cwd, cmd)
|
||||
if not gitdir then
|
||||
return head_str
|
||||
end
|
||||
if head_str == 'HEAD' then
|
||||
local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, {
|
||||
command = cmd,
|
||||
ignore_error = true,
|
||||
cwd = cwd,
|
||||
})[1] or ''
|
||||
if log.debug_mode and short_sha ~= '' then
|
||||
short_sha = 'HEAD'
|
||||
end
|
||||
if
|
||||
util.path_exists(gitdir .. '/rebase-merge')
|
||||
or util.path_exists(gitdir .. '/rebase-apply')
|
||||
then
|
||||
return short_sha .. '(rebasing)'
|
||||
end
|
||||
return short_sha
|
||||
end
|
||||
return head_str
|
||||
end
|
||||
|
||||
local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1
|
||||
|
||||
local cygpath_convert ---@type fun(path: string): string
|
||||
|
||||
if has_cygpath then
|
||||
cygpath_convert = function(path)
|
||||
--- @type vim.SystemCompleted
|
||||
local obj = asystem({ 'cygpath', '-aw', path })
|
||||
return obj.stdout
|
||||
end
|
||||
end
|
||||
|
||||
--- @param path string
|
||||
--- @return string
|
||||
local function normalize_path(path)
|
||||
if path and has_cygpath and not uv.fs_stat(path) then
|
||||
-- If on windows and path isn't recognizable as a file, try passing it
|
||||
-- through cygpath
|
||||
path = cygpath_convert(path)
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
--- @async
|
||||
--- @param cwd string
|
||||
--- @param cmd? string
|
||||
--- @param gitdir? string
|
||||
--- @param toplevel? string
|
||||
--- @return Gitsigns.RepoInfo
|
||||
function M.get_repo_info(cwd, cmd, gitdir, toplevel)
|
||||
-- Does git rev-parse have --absolute-git-dir, added in 2.13:
|
||||
-- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/
|
||||
local has_abs_gd = check_version({ 2, 13 })
|
||||
local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir'
|
||||
|
||||
-- Wait for internal scheduler to settle before running command (#215)
|
||||
scheduler()
|
||||
|
||||
local args = {}
|
||||
|
||||
if gitdir then
|
||||
vim.list_extend(args, { '--git-dir', gitdir })
|
||||
end
|
||||
|
||||
if toplevel then
|
||||
vim.list_extend(args, { '--work-tree', toplevel })
|
||||
end
|
||||
|
||||
vim.list_extend(args, {
|
||||
'rev-parse',
|
||||
'--show-toplevel',
|
||||
git_dir_opt,
|
||||
'--abbrev-ref',
|
||||
'HEAD',
|
||||
})
|
||||
|
||||
local results = git_command(args, {
|
||||
command = cmd,
|
||||
ignore_error = true,
|
||||
cwd = toplevel or cwd,
|
||||
})
|
||||
|
||||
local toplevel_r = normalize_path(results[1])
|
||||
local gitdir_r = normalize_path(results[2])
|
||||
|
||||
if gitdir_r and not has_abs_gd then
|
||||
gitdir_r = assert(uv.fs_realpath(gitdir_r))
|
||||
end
|
||||
|
||||
return {
|
||||
toplevel = toplevel_r,
|
||||
gitdir = gitdir_r,
|
||||
abbrev_head = process_abbrev_head(gitdir_r, results[3], cwd, cmd),
|
||||
detached = toplevel_r and gitdir_r ~= toplevel_r .. '/.git',
|
||||
}
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Git repo object methods
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--- Run git command the with the objects gitdir and toplevel
|
||||
--- @async
|
||||
--- @param args string[]
|
||||
--- @param spec? Gitsigns.Git.JobSpec
|
||||
--- @return string[] stdout, string? stderr
|
||||
function Repo:command(args, spec)
|
||||
spec = spec or {}
|
||||
spec.cwd = self.toplevel
|
||||
|
||||
local args1 = {
|
||||
'--git-dir',
|
||||
self.gitdir,
|
||||
}
|
||||
|
||||
if self.detached then
|
||||
vim.list_extend(args1, { '--work-tree', self.toplevel })
|
||||
end
|
||||
|
||||
vim.list_extend(args1, args)
|
||||
|
||||
return git_command(args1, spec)
|
||||
end
|
||||
|
||||
--- @return string[]
|
||||
function Repo:files_changed()
|
||||
--- @type string[]
|
||||
local results = self:command({ 'status', '--porcelain', '--ignore-submodules' })
|
||||
|
||||
local ret = {} --- @type string[]
|
||||
for _, line in ipairs(results) do
|
||||
if line:sub(1, 2):match('^.M') then
|
||||
ret[#ret + 1] = line:sub(4, -1)
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
--- @param encoding string
|
||||
--- @return boolean
|
||||
local function iconv_supported(encoding)
|
||||
-- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924
|
||||
if vim.startswith(encoding, 'utf-16') then
|
||||
return false
|
||||
elseif vim.startswith(encoding, 'utf-32') then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Get version of file in the index, return array lines
|
||||
--- @param object string
|
||||
--- @param encoding? string
|
||||
--- @return string[] stdout, string? stderr
|
||||
function Repo:get_show_text(object, encoding)
|
||||
local stdout, stderr = self:command({ 'show', object }, { text = false, ignore_error = true })
|
||||
|
||||
if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then
|
||||
for i, l in ipairs(stdout) do
|
||||
--- @diagnostic disable-next-line:param-type-mismatch
|
||||
stdout[i] = vim.iconv(l, encoding, 'utf-8')
|
||||
end
|
||||
end
|
||||
|
||||
return stdout, stderr
|
||||
end
|
||||
|
||||
--- @async
|
||||
function Repo:update_abbrev_head()
|
||||
self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param dir string
|
||||
--- @param gitdir? string
|
||||
--- @param toplevel? string
|
||||
function Repo:try_yadm(dir, gitdir, toplevel)
|
||||
if not config.yadm.enable or self.gitdir then
|
||||
return
|
||||
end
|
||||
|
||||
local home = os.getenv('HOME')
|
||||
|
||||
if not home or not vim.startswith(dir, home) then
|
||||
return
|
||||
end
|
||||
|
||||
if #git_command({ 'ls-files', dir }, { command = 'yadm' }) == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
M.get_repo_info(dir, 'yadm', gitdir, toplevel)
|
||||
local yadm_info = M.get_repo_info(dir, 'yadm', gitdir, toplevel)
|
||||
for k, v in
|
||||
pairs(yadm_info --[[@as table<string,any>]])
|
||||
do
|
||||
---@diagnostic disable-next-line:no-unknown
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
--- @async
|
||||
--- @param dir string
|
||||
--- @param gitdir? string
|
||||
--- @param toplevel? string
|
||||
--- @return Gitsigns.Repo
|
||||
function Repo.new(dir, gitdir, toplevel)
|
||||
local self = setmetatable({}, { __index = Repo })
|
||||
|
||||
self.username = git_command({ 'config', 'user.name' }, { ignore_error = true })[1]
|
||||
local info = M.get_repo_info(dir, nil, gitdir, toplevel)
|
||||
for k, v in
|
||||
pairs(info --[[@as table<string,any>]])
|
||||
do
|
||||
---@diagnostic disable-next-line:no-unknown
|
||||
self[k] = v
|
||||
end
|
||||
|
||||
self:try_yadm(dir, gitdir, toplevel)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Git object methods
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--- Run git command the with the objects gitdir and toplevel
|
||||
--- @param args string[]
|
||||
--- @param spec? Gitsigns.Git.JobSpec
|
||||
--- @return string[] stdout, string? stderr
|
||||
function Obj:command(args, spec)
|
||||
return self.repo:command(args, spec)
|
||||
end
|
||||
|
||||
--- @param revision? string
|
||||
function Obj:update_revision(revision)
|
||||
revision = util.norm_base(revision)
|
||||
self.revision = revision
|
||||
self:update()
|
||||
end
|
||||
|
||||
--- @param update_relpath? boolean
|
||||
--- @param silent? boolean
|
||||
--- @return boolean
|
||||
function Obj:update(update_relpath, silent)
|
||||
local old_object_name = self.object_name
|
||||
local props = self:file_info(self.file, silent)
|
||||
|
||||
if update_relpath then
|
||||
self.relpath = props.relpath
|
||||
end
|
||||
self.object_name = props.object_name
|
||||
self.mode_bits = props.mode_bits
|
||||
self.has_conflicts = props.has_conflicts
|
||||
self.i_crlf = props.i_crlf
|
||||
self.w_crlf = props.w_crlf
|
||||
|
||||
return old_object_name ~= self.object_name
|
||||
end
|
||||
|
||||
--- @class (exact) Gitsigns.FileInfo
|
||||
--- @field relpath string
|
||||
--- @field i_crlf? boolean
|
||||
--- @field w_crlf? boolean
|
||||
--- @field mode_bits? string
|
||||
--- @field object_name? string
|
||||
--- @field has_conflicts? true
|
||||
|
||||
--- @param file? string
|
||||
--- @param silent? boolean
|
||||
--- @return Gitsigns.FileInfo
|
||||
function Obj:file_info(file, silent)
|
||||
if self.revision and not vim.startswith(self.revision, ':') then
|
||||
return self:file_info_tree(file, silent)
|
||||
else
|
||||
return self:file_info_index(file, silent)
|
||||
end
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param file? string
|
||||
--- @param silent? boolean
|
||||
--- @return Gitsigns.FileInfo
|
||||
function Obj:file_info_index(file, silent)
|
||||
local has_eol = check_version({ 2, 9 })
|
||||
|
||||
local cmd = {
|
||||
'-c',
|
||||
'core.quotepath=off',
|
||||
'ls-files',
|
||||
'--stage',
|
||||
'--others',
|
||||
'--exclude-standard',
|
||||
}
|
||||
|
||||
if has_eol then
|
||||
cmd[#cmd + 1] = '--eol'
|
||||
end
|
||||
|
||||
cmd[#cmd + 1] = file or self.file
|
||||
|
||||
local results, stderr = self:command(cmd, { ignore_error = true })
|
||||
|
||||
if stderr and not silent then
|
||||
-- ignore_error for the cases when we run:
|
||||
-- git ls-files --others exists/nonexist
|
||||
if not stderr:match('^warning: could not open directory .*: No such file or directory') then
|
||||
log.eprint(stderr)
|
||||
end
|
||||
end
|
||||
|
||||
local relpath_idx = has_eol and 2 or 1
|
||||
|
||||
local result = {}
|
||||
for _, line in ipairs(results) do
|
||||
local parts = vim.split(line, '\t')
|
||||
if #parts > relpath_idx then -- tracked file
|
||||
local attrs = vim.split(parts[1], '%s+')
|
||||
local stage = tonumber(attrs[3])
|
||||
if stage <= 1 then
|
||||
result.mode_bits = attrs[1]
|
||||
result.object_name = attrs[2]
|
||||
else
|
||||
result.has_conflicts = true
|
||||
end
|
||||
|
||||
if has_eol then
|
||||
result.relpath = parts[3]
|
||||
local eol = vim.split(parts[2], '%s+')
|
||||
result.i_crlf = eol[1] == 'i/crlf'
|
||||
result.w_crlf = eol[2] == 'w/crlf'
|
||||
else
|
||||
result.relpath = parts[2]
|
||||
end
|
||||
else -- untracked file
|
||||
result.relpath = parts[relpath_idx]
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param file? string
|
||||
--- @param silent? boolean
|
||||
--- @return Gitsigns.FileInfo
|
||||
function Obj:file_info_tree(file, silent)
|
||||
local results, stderr = self:command({
|
||||
'-c',
|
||||
'core.quotepath=off',
|
||||
'ls-tree',
|
||||
self.revision,
|
||||
file or self.file,
|
||||
}, { ignore_error = true })
|
||||
|
||||
if stderr then
|
||||
if not silent then
|
||||
log.eprint(stderr)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local info, relpath = unpack(vim.split(results[1], '\t'))
|
||||
local mode_bits, objtype, object_name = unpack(vim.split(info, '%s+'))
|
||||
assert(objtype == 'blob')
|
||||
|
||||
return {
|
||||
mode_bits = mode_bits,
|
||||
object_name = object_name,
|
||||
relpath = relpath,
|
||||
}
|
||||
end
|
||||
|
||||
--- @param revision? string
|
||||
--- @return string[] stdout, string? stderr
|
||||
function Obj:get_show_text(revision)
|
||||
if revision and not self.relpath then
|
||||
dprint('no relpath')
|
||||
return {}
|
||||
end
|
||||
|
||||
local object = revision and (revision .. ':' .. self.relpath) or self.object_name
|
||||
|
||||
if not object then
|
||||
dprint('no revision or object_name')
|
||||
return { '' }
|
||||
end
|
||||
|
||||
local stdout, stderr = self.repo:get_show_text(object, self.encoding)
|
||||
|
||||
if not self.i_crlf and self.w_crlf then
|
||||
-- Add cr
|
||||
-- Do not add cr to the newline at the end of file
|
||||
for i = 1, #stdout - 1 do
|
||||
stdout[i] = stdout[i] .. '\r'
|
||||
end
|
||||
end
|
||||
|
||||
return stdout, stderr
|
||||
end
|
||||
|
||||
local function autocmd_changed(file)
|
||||
vim.schedule(function()
|
||||
vim.api.nvim_exec_autocmds('User', {
|
||||
pattern = 'GitSignsChanged',
|
||||
modeline = false,
|
||||
data = { file = file },
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
function Obj:unstage_file()
|
||||
self:command({ 'reset', self.file })
|
||||
autocmd_changed(self.file)
|
||||
end
|
||||
|
||||
--- @class Gitsigns.CommitInfo
|
||||
--- @field author string
|
||||
--- @field author_mail string
|
||||
--- @field author_time integer
|
||||
--- @field author_tz string
|
||||
--- @field committer string
|
||||
--- @field committer_mail string
|
||||
--- @field committer_time integer
|
||||
--- @field committer_tz string
|
||||
--- @field summary string
|
||||
--- @field sha string
|
||||
--- @field abbrev_sha string
|
||||
--- @field boundary? true
|
||||
|
||||
--- @class Gitsigns.BlameInfoPublic: Gitsigns.BlameInfo, Gitsigns.CommitInfo
|
||||
--- @field body? string[]
|
||||
--- @field hunk_no? integer
|
||||
--- @field num_hunks? integer
|
||||
--- @field hunk? string[]
|
||||
--- @field hunk_head? string
|
||||
|
||||
--- @class Gitsigns.BlameInfo
|
||||
--- @field orig_lnum integer
|
||||
--- @field final_lnum integer
|
||||
--- @field commit Gitsigns.CommitInfo
|
||||
--- @field filename string
|
||||
--- @field previous_filename? string
|
||||
--- @field previous_sha? string
|
||||
|
||||
local NOT_COMMITTED = {
|
||||
author = 'Not Committed Yet',
|
||||
author_mail = '<not.committed.yet>',
|
||||
committer = 'Not Committed Yet',
|
||||
committer_mail = '<not.committed.yet>',
|
||||
}
|
||||
|
||||
--- @param file string
|
||||
--- @return Gitsigns.CommitInfo
|
||||
function M.not_commited(file)
|
||||
local time = os.time()
|
||||
return {
|
||||
sha = string.rep('0', 40),
|
||||
abbrev_sha = string.rep('0', 8),
|
||||
author = 'Not Committed Yet',
|
||||
author_mail = '<not.committed.yet>',
|
||||
author_tz = '+0000',
|
||||
author_time = time,
|
||||
committer = 'Not Committed Yet',
|
||||
committer_time = time,
|
||||
committer_mail = '<not.committed.yet>',
|
||||
committer_tz = '+0000',
|
||||
summary = 'Version of ' .. file,
|
||||
}
|
||||
end
|
||||
|
||||
---@param x any
|
||||
---@return integer
|
||||
local function asinteger(x)
|
||||
return assert(tonumber(x))
|
||||
end
|
||||
|
||||
--- @param lines string[]
|
||||
--- @param lnum? integer
|
||||
--- @param opts? Gitsigns.BlameOpts
|
||||
--- @return table<integer,Gitsigns.BlameInfo?>?
|
||||
function Obj:run_blame(lines, lnum, opts)
|
||||
local ret = {} --- @type table<integer,Gitsigns.BlameInfo>
|
||||
|
||||
if not self.object_name or self.repo.abbrev_head == '' then
|
||||
-- As we support attaching to untracked files we need to return something if
|
||||
-- the file isn't isn't tracked in git.
|
||||
-- If abbrev_head is empty, then assume the repo has no commits
|
||||
local commit = M.not_commited(self.file)
|
||||
for i in ipairs(lines) do
|
||||
ret[i] = {
|
||||
orig_lnum = 0,
|
||||
final_lnum = i,
|
||||
commit = commit,
|
||||
filename = self.file,
|
||||
}
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local args = { 'blame', '--contents', '-', '--incremental' }
|
||||
|
||||
opts = opts or {}
|
||||
|
||||
if opts.ignore_whitespace then
|
||||
args[#args + 1] = '-w'
|
||||
end
|
||||
|
||||
if lnum then
|
||||
vim.list_extend(args, { '-L', lnum .. ',+1' })
|
||||
end
|
||||
|
||||
if opts.extra_opts then
|
||||
vim.list_extend(args, opts.extra_opts)
|
||||
end
|
||||
|
||||
local ignore_file = self.repo.toplevel .. '/.git-blame-ignore-revs'
|
||||
if uv.fs_stat(ignore_file) then
|
||||
vim.list_extend(args, { '--ignore-revs-file', ignore_file })
|
||||
end
|
||||
|
||||
args[#args + 1] = opts.rev
|
||||
args[#args + 1] = '--'
|
||||
args[#args + 1] = self.file
|
||||
|
||||
local results, stderr = self:command(args, { stdin = lines, ignore_error = true })
|
||||
if stderr then
|
||||
error_once('Error running git-blame: ' .. stderr)
|
||||
return
|
||||
end
|
||||
|
||||
if #results == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local commits = {} --- @type table<string,Gitsigns.CommitInfo>
|
||||
local i = 1
|
||||
|
||||
while i <= #results do
|
||||
--- @param pat? string
|
||||
--- @return string
|
||||
local function get(pat)
|
||||
local l = assert(results[i])
|
||||
i = i + 1
|
||||
if pat then
|
||||
return l:match(pat)
|
||||
end
|
||||
return l
|
||||
end
|
||||
|
||||
local function peek(pat)
|
||||
local l = results[i]
|
||||
if l and pat then
|
||||
return l:match(pat)
|
||||
end
|
||||
return l
|
||||
end
|
||||
|
||||
local sha, orig_lnum_str, final_lnum_str, size_str = get('(%x+) (%d+) (%d+) (%d+)')
|
||||
local orig_lnum = asinteger(orig_lnum_str)
|
||||
local final_lnum = asinteger(final_lnum_str)
|
||||
local size = asinteger(size_str)
|
||||
|
||||
if peek():match('^author ') then
|
||||
--- @type table<string,string|true>
|
||||
local commit = {
|
||||
sha = sha,
|
||||
abbrev_sha = sha:sub(1, 8),
|
||||
}
|
||||
|
||||
-- filename terminates the entry
|
||||
while peek() and not (peek():match('^filename ') or peek():match('^previous ')) do
|
||||
local l = get()
|
||||
local key, value = l:match('^([^%s]+) (.*)')
|
||||
if key then
|
||||
if vim.endswith(key, '_time') then
|
||||
value = tonumber(value)
|
||||
end
|
||||
key = key:gsub('%-', '_') --- @type string
|
||||
commit[key] = value
|
||||
else
|
||||
commit[l] = true
|
||||
if l ~= 'boundary' then
|
||||
dprintf("Unknown tag: '%s'", l)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- New in git 2.41:
|
||||
-- The output given by "git blame" that attributes a line to contents
|
||||
-- taken from the file specified by the "--contents" option shows it
|
||||
-- differently from a line attributed to the working tree file.
|
||||
if
|
||||
commit.author_mail == '<external.file>'
|
||||
or commit.author_mail == 'External file (--contents)'
|
||||
then
|
||||
commit = vim.tbl_extend('force', commit, NOT_COMMITTED)
|
||||
end
|
||||
commits[sha] = commit
|
||||
end
|
||||
|
||||
local previous_sha, previous_filename = peek():match('^previous (%x+) (.*)')
|
||||
if previous_sha then
|
||||
get()
|
||||
end
|
||||
|
||||
local filename = assert(get():match('^filename (.*)'))
|
||||
|
||||
for j = 0, size - 1 do
|
||||
ret[final_lnum + j] = {
|
||||
final_lnum = final_lnum + j,
|
||||
orig_lnum = orig_lnum + j,
|
||||
commit = commits[sha],
|
||||
filename = filename,
|
||||
previous_filename = previous_filename,
|
||||
previous_sha = previous_sha,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
--- @param obj Gitsigns.GitObj
|
||||
local function ensure_file_in_index(obj)
|
||||
if obj.object_name and not obj.has_conflicts then
|
||||
return
|
||||
end
|
||||
|
||||
if not obj.object_name then
|
||||
-- If there is no object_name then it is not yet in the index so add it
|
||||
obj:command({ 'add', '--intent-to-add', obj.file })
|
||||
else
|
||||
-- Update the index with the common ancestor (stage 1) which is what bcache
|
||||
-- stores
|
||||
local info = string.format('%s,%s,%s', obj.mode_bits, obj.object_name, obj.relpath)
|
||||
obj:command({ 'update-index', '--add', '--cacheinfo', info })
|
||||
end
|
||||
|
||||
obj:update()
|
||||
end
|
||||
|
||||
--- Stage 'lines' as the entire contents of the file
|
||||
--- @param lines string[]
|
||||
function Obj:stage_lines(lines)
|
||||
local stdout = self:command({
|
||||
'hash-object',
|
||||
'-w',
|
||||
'--path',
|
||||
self.relpath,
|
||||
'--stdin',
|
||||
}, { stdin = lines })
|
||||
|
||||
local new_object = stdout[1]
|
||||
|
||||
self:command({
|
||||
'update-index',
|
||||
'--cacheinfo',
|
||||
string.format('%s,%s,%s', self.mode_bits, new_object, self.relpath),
|
||||
})
|
||||
|
||||
autocmd_changed(self.file)
|
||||
end
|
||||
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @param invert? boolean
|
||||
function Obj:stage_hunks(hunks, invert)
|
||||
ensure_file_in_index(self)
|
||||
|
||||
local gs_hunks = require('gitsigns.hunks')
|
||||
|
||||
local patch = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert)
|
||||
|
||||
if not self.i_crlf and self.w_crlf then
|
||||
-- Remove cr
|
||||
for i = 1, #patch do
|
||||
patch[i] = patch[i]:gsub('\r$', '')
|
||||
end
|
||||
end
|
||||
|
||||
self:command({
|
||||
'apply',
|
||||
'--whitespace=nowarn',
|
||||
'--cached',
|
||||
'--unidiff-zero',
|
||||
'-',
|
||||
}, {
|
||||
stdin = patch,
|
||||
})
|
||||
|
||||
autocmd_changed(self.file)
|
||||
end
|
||||
|
||||
--- @return string?
|
||||
function Obj:has_moved()
|
||||
local out = self:command({ 'diff', '--name-status', '-C', '--cached' })
|
||||
local orig_relpath = self.orig_relpath or self.relpath
|
||||
for _, l in ipairs(out) do
|
||||
local parts = vim.split(l, '%s+')
|
||||
if #parts == 3 then
|
||||
local orig, new = parts[2], parts[3]
|
||||
if orig_relpath == orig then
|
||||
self.orig_relpath = orig_relpath
|
||||
self.relpath = new
|
||||
self.file = self.repo.toplevel .. '/' .. new
|
||||
return new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param file string
|
||||
--- @param revision string?
|
||||
--- @param encoding string
|
||||
--- @param gitdir string?
|
||||
--- @param toplevel string?
|
||||
--- @return Gitsigns.GitObj?
|
||||
function Obj.new(file, revision, encoding, gitdir, toplevel)
|
||||
if in_git_dir(file) then
|
||||
dprint('In git dir')
|
||||
return nil
|
||||
end
|
||||
local self = setmetatable({}, { __index = Obj })
|
||||
|
||||
if not vim.startswith(file, '/') and toplevel then
|
||||
file = toplevel .. util.path_sep .. file
|
||||
end
|
||||
|
||||
self.file = file
|
||||
self.revision = util.norm_base(revision)
|
||||
self.encoding = encoding
|
||||
self.repo = Repo.new(util.dirname(file), gitdir, toplevel)
|
||||
|
||||
if not self.repo.gitdir then
|
||||
dprint('Not in git repo')
|
||||
return nil
|
||||
end
|
||||
|
||||
-- When passing gitdir and toplevel, suppress stderr when resolving the file
|
||||
local silent = gitdir ~= nil and toplevel ~= nil
|
||||
|
||||
self:update(true, silent)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,101 @@
|
||||
local async = require('gitsigns.async')
|
||||
local gs_config = require('gitsigns.config')
|
||||
|
||||
local log = require('gitsigns.debug.log')
|
||||
local err = require('gitsigns.message').error
|
||||
local system = require('gitsigns.system').system
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted
|
||||
local asystem = async.wrap(3, system)
|
||||
|
||||
--- @class (exact) Gitsigns.Version
|
||||
--- @field major integer
|
||||
--- @field minor integer
|
||||
--- @field patch integer
|
||||
|
||||
--- @param version string
|
||||
--- @return Gitsigns.Version
|
||||
local function parse_version(version)
|
||||
assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: ' .. version)
|
||||
local ret = {}
|
||||
local parts = vim.split(version, '%.')
|
||||
ret.major = assert(tonumber(parts[1]))
|
||||
ret.minor = assert(tonumber(parts[2]))
|
||||
|
||||
if parts[3] == 'GIT' then
|
||||
ret.patch = 0
|
||||
else
|
||||
local patch_ver = vim.split(parts[3], '-')
|
||||
ret.patch = assert(tonumber(patch_ver[1]))
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local function set_version()
|
||||
local version = gs_config.config._git_version
|
||||
if version ~= 'auto' then
|
||||
local ok, ret = pcall(parse_version, version)
|
||||
if ok then
|
||||
M.version = ret
|
||||
else
|
||||
err(ret --[[@as string]])
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
--- @type vim.SystemCompleted
|
||||
local obj = asystem({ 'git', '--version' })
|
||||
async.scheduler()
|
||||
|
||||
local line = vim.split(obj.stdout or '', '\n')[1]
|
||||
if not line then
|
||||
err("Unable to detect git version as 'git --version' failed to return anything")
|
||||
log.eprint(obj.stderr)
|
||||
return
|
||||
end
|
||||
|
||||
-- Sometime 'git --version' returns an empty string (#948)
|
||||
if log.assert(type(line) == 'string', 'Unexpected output: ' .. line) then
|
||||
return
|
||||
end
|
||||
|
||||
if log.assert(vim.startswith(line, 'git version'), 'Unexpected output: ' .. line) then
|
||||
return
|
||||
end
|
||||
|
||||
local parts = vim.split(line, '%s+')
|
||||
M.version = parse_version(parts[3])
|
||||
end
|
||||
|
||||
--- Usage: check_version{2,3}
|
||||
--- @param version {[1]: integer, [2]:integer, [3]:integer}?
|
||||
--- @return boolean
|
||||
function M.check(version)
|
||||
if not M.version then
|
||||
set_version()
|
||||
end
|
||||
|
||||
if not M.version then
|
||||
return false
|
||||
end
|
||||
|
||||
if not version then
|
||||
return false
|
||||
end
|
||||
|
||||
if M.version.major < version[1] then
|
||||
return false
|
||||
end
|
||||
if version[2] and M.version.minor < version[2] then
|
||||
return false
|
||||
end
|
||||
if version[3] and M.version.patch < version[3] then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,342 @@
|
||||
local api = vim.api
|
||||
|
||||
--- @class Gitsigns.Hldef
|
||||
--- @field [integer] string
|
||||
--- @field desc string
|
||||
--- @field hidden boolean
|
||||
--- @field fg_factor number
|
||||
|
||||
local nvim10 = vim.fn.has('nvim-0.10') > 0
|
||||
|
||||
local M = {}
|
||||
|
||||
--- Use array of dict so we can iterate deterministically
|
||||
--- Export for docgen
|
||||
--- @type table<string,Gitsigns.Hldef>[]
|
||||
M.hls = {
|
||||
{
|
||||
GitSignsAdd = {
|
||||
'GitGutterAdd',
|
||||
'SignifySignAdd',
|
||||
'DiffAddedGutter',
|
||||
nvim10 and 'Added' or 'diffAdded',
|
||||
'DiffAdd',
|
||||
desc = "Used for the text of 'add' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChange = {
|
||||
'GitGutterChange',
|
||||
'SignifySignChange',
|
||||
'DiffModifiedGutter',
|
||||
nvim10 and 'Changed' or 'diffChanged',
|
||||
'DiffChange',
|
||||
desc = "Used for the text of 'change' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsDelete = {
|
||||
'GitGutterDelete',
|
||||
'SignifySignDelete',
|
||||
'DiffRemovedGutter',
|
||||
nvim10 and 'Removed' or 'diffRemoved',
|
||||
'DiffDelete',
|
||||
desc = "Used for the text of 'delete' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChangedelete = {
|
||||
'GitSignsChange',
|
||||
desc = "Used for the text of 'changedelete' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{ GitSignsTopdelete = { 'GitSignsDelete', desc = "Used for the text of 'topdelete' signs." } },
|
||||
|
||||
{ GitSignsUntracked = { 'GitSignsAdd', desc = "Used for the text of 'untracked' signs." } },
|
||||
|
||||
{
|
||||
GitSignsAddNr = {
|
||||
'GitGutterAddLineNr',
|
||||
'GitSignsAdd',
|
||||
desc = "Used for number column (when `config.numhl == true`) of 'add' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChangeNr = {
|
||||
'GitGutterChangeLineNr',
|
||||
'GitSignsChange',
|
||||
desc = "Used for number column (when `config.numhl == true`) of 'change' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsDeleteNr = {
|
||||
'GitGutterDeleteLineNr',
|
||||
'GitSignsDelete',
|
||||
desc = "Used for number column (when `config.numhl == true`) of 'delete' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChangedeleteNr = {
|
||||
'GitSignsChangeNr',
|
||||
desc = "Used for number column (when `config.numhl == true`) of 'changedelete' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsTopdeleteNr = {
|
||||
'GitSignsDeleteNr',
|
||||
desc = "Used for number column (when `config.numhl == true`) of 'topdelete' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsUntrackedNr = {
|
||||
'GitSignsAddNr',
|
||||
desc = "Used for number column (when `config.numhl == true`) of 'untracked' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsAddLn = {
|
||||
'GitGutterAddLine',
|
||||
'SignifyLineAdd',
|
||||
'DiffAdd',
|
||||
desc = "Used for buffer line (when `config.linehl == true`) of 'add' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChangeLn = {
|
||||
'GitGutterChangeLine',
|
||||
'SignifyLineChange',
|
||||
'DiffChange',
|
||||
desc = "Used for buffer line (when `config.linehl == true`) of 'change' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChangedeleteLn = {
|
||||
'GitSignsChangeLn',
|
||||
desc = "Used for buffer line (when `config.linehl == true`) of 'changedelete' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsUntrackedLn = {
|
||||
'GitSignsAddLn',
|
||||
desc = "Used for buffer line (when `config.linehl == true`) of 'untracked' signs.",
|
||||
},
|
||||
},
|
||||
|
||||
-- Don't set GitSignsDeleteLn by default
|
||||
-- {GitSignsDeleteLn = {}},
|
||||
|
||||
{ GitSignsStagedAdd = { 'GitSignsAdd', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedChange = { 'GitSignsChange', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedDelete = { 'GitSignsDelete', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedChangedelete = { 'GitSignsChangedelete', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedTopdelete = { 'GitSignsTopdelete', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedAddNr = { 'GitSignsAddNr', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedChangeNr = { 'GitSignsChangeNr', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedDeleteNr = { 'GitSignsDeleteNr', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedChangedeleteNr = { 'GitSignsChangedeleteNr', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedTopdeleteNr = { 'GitSignsTopdeleteNr', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedAddLn = { 'GitSignsAddLn', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedChangeLn = { 'GitSignsChangeLn', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedDeleteLn = { 'GitSignsDeleteLn', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedChangedeleteLn = { 'GitSignsChangedeleteLn', fg_factor = 0.5, hidden = true } },
|
||||
{ GitSignsStagedTopdeleteLn = { 'GitSignsTopdeleteLn', fg_factor = 0.5, hidden = true } },
|
||||
|
||||
{
|
||||
GitSignsAddPreview = {
|
||||
'GitGutterAddLine',
|
||||
'SignifyLineAdd',
|
||||
'DiffAdd',
|
||||
desc = 'Used for added lines in previews.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsDeletePreview = {
|
||||
'GitGutterDeleteLine',
|
||||
'SignifyLineDelete',
|
||||
'DiffDelete',
|
||||
desc = 'Used for deleted lines in previews.',
|
||||
},
|
||||
},
|
||||
|
||||
{ GitSignsCurrentLineBlame = { 'NonText', desc = 'Used for current line blame.' } },
|
||||
|
||||
{
|
||||
GitSignsAddInline = {
|
||||
'TermCursor',
|
||||
desc = 'Used for added word diff regions in inline previews.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsDeleteInline = {
|
||||
'TermCursor',
|
||||
desc = 'Used for deleted word diff regions in inline previews.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChangeInline = {
|
||||
'TermCursor',
|
||||
desc = 'Used for changed word diff regions in inline previews.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsAddLnInline = {
|
||||
'GitSignsAddInline',
|
||||
desc = 'Used for added word diff regions when `config.word_diff == true`.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsChangeLnInline = {
|
||||
'GitSignsChangeInline',
|
||||
desc = 'Used for changed word diff regions when `config.word_diff == true`.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsDeleteLnInline = {
|
||||
'GitSignsDeleteInline',
|
||||
desc = 'Used for deleted word diff regions when `config.word_diff == true`.',
|
||||
},
|
||||
},
|
||||
|
||||
-- Currently unused
|
||||
-- {GitSignsAddLnVirtLn = {'GitSignsAddLn'}},
|
||||
-- {GitSignsChangeVirtLn = {'GitSignsChangeLn'}},
|
||||
-- {GitSignsAddLnVirtLnInLine = {'GitSignsAddLnInline', }},
|
||||
-- {GitSignsChangeVirtLnInLine = {'GitSignsChangeLnInline', }},
|
||||
|
||||
{
|
||||
GitSignsDeleteVirtLn = {
|
||||
'GitGutterDeleteLine',
|
||||
'SignifyLineDelete',
|
||||
'DiffDelete',
|
||||
desc = 'Used for deleted lines shown by inline `preview_hunk_inline()` or `show_deleted()`.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsDeleteVirtLnInLine = {
|
||||
'GitSignsDeleteLnInline',
|
||||
desc = 'Used for word diff regions in lines shown by inline `preview_hunk_inline()` or `show_deleted()`.',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
GitSignsVirtLnum = {
|
||||
'GitSignsDeleteVirtLn',
|
||||
desc = 'Used for line numbers in inline hunks previews.',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@param name string
|
||||
---@return table<string, any>
|
||||
local function get_hl(name)
|
||||
--- @diagnostic disable-next-line:deprecated
|
||||
return api.nvim_get_hl_by_name(name, true)
|
||||
end
|
||||
|
||||
--- @param hl_name string
|
||||
--- @return boolean
|
||||
local function is_hl_set(hl_name)
|
||||
-- TODO: this only works with `set termguicolors`
|
||||
local exists, hl = pcall(get_hl, hl_name)
|
||||
if not exists then
|
||||
return false
|
||||
end
|
||||
local color = hl.foreground or hl.background or hl.reverse
|
||||
return color ~= nil
|
||||
end
|
||||
|
||||
--- @param x? number
|
||||
--- @param factor number
|
||||
--- @return number?
|
||||
local function cmul(x, factor)
|
||||
if not x or factor == 1 then
|
||||
return x
|
||||
end
|
||||
|
||||
local r = math.floor(x / 2 ^ 16)
|
||||
local x1 = x - (r * 2 ^ 16)
|
||||
local g = math.floor(x1 / 2 ^ 8)
|
||||
local b = math.floor(x1 - (g * 2 ^ 8))
|
||||
return math.floor(
|
||||
math.floor(r * factor) * 2 ^ 16 + math.floor(g * factor) * 2 ^ 8 + math.floor(b * factor)
|
||||
)
|
||||
end
|
||||
|
||||
local function dprintf(fmt, ...)
|
||||
require('gitsigns.debug.log').dprintf(fmt, ...)
|
||||
end
|
||||
|
||||
--- @param hl string
|
||||
--- @param hldef Gitsigns.Hldef
|
||||
local function derive(hl, hldef)
|
||||
for _, d in ipairs(hldef) do
|
||||
if is_hl_set(d) then
|
||||
dprintf('Deriving %s from %s', hl, d)
|
||||
if hldef.fg_factor then
|
||||
hldef.fg_factor = hldef.fg_factor or 1
|
||||
local dh = get_hl(d)
|
||||
api.nvim_set_hl(0, hl, {
|
||||
default = true,
|
||||
fg = cmul(dh.foreground, hldef.fg_factor),
|
||||
bg = dh.background,
|
||||
})
|
||||
else
|
||||
api.nvim_set_hl(0, hl, { default = true, link = d })
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
if hldef[1] and not hldef.fg_factor then
|
||||
-- No fallback found which is set. Just link to the first fallback
|
||||
-- if there are no modifiers
|
||||
dprintf('Deriving %s from %s', hl, hldef[1])
|
||||
api.nvim_set_hl(0, hl, { default = true, link = hldef[1] })
|
||||
else
|
||||
dprintf('Could not derive %s', hl)
|
||||
end
|
||||
end
|
||||
|
||||
--- Setup a GitSign* highlight by deriving it from other potentially present
|
||||
--- highlights.
|
||||
function M.setup_highlights()
|
||||
for _, hlg in ipairs(M.hls) do
|
||||
for hl, hldef in pairs(hlg) do
|
||||
if is_hl_set(hl) then
|
||||
-- Already defined
|
||||
dprintf('Highlight %s is already defined', hl)
|
||||
else
|
||||
derive(hl, hldef)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
M.setup_highlights()
|
||||
api.nvim_create_autocmd('ColorScheme', {
|
||||
group = 'gitsigns',
|
||||
callback = M.setup_highlights,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,482 @@
|
||||
local util = require('gitsigns.util')
|
||||
|
||||
local min, max = math.min, math.max
|
||||
|
||||
--- @alias Gitsigns.Hunk.Type
|
||||
--- | "add"
|
||||
--- | "change"
|
||||
--- | "delete"
|
||||
|
||||
--- @class (exact) Gitsigns.Hunk.Node
|
||||
--- @field start integer
|
||||
--- @field count integer
|
||||
--- @field lines string[]
|
||||
--- @field no_nl_at_eof? true
|
||||
|
||||
--- @class (exact) Gitsigns.Hunk.Hunk
|
||||
--- @field type Gitsigns.Hunk.Type
|
||||
--- @field head string
|
||||
--- @field added Gitsigns.Hunk.Node
|
||||
--- @field removed Gitsigns.Hunk.Node
|
||||
--- @field vend integer
|
||||
|
||||
--- @class (exact) Gitsigns.Hunk.Hunk_Public
|
||||
--- @field type Gitsigns.Hunk.Type
|
||||
--- @field head string
|
||||
--- @field lines string[]
|
||||
--- @field added Gitsigns.Hunk.Node
|
||||
--- @field removed Gitsigns.Hunk.Node
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @param old_start integer
|
||||
--- @param old_count integer
|
||||
--- @param new_start integer
|
||||
--- @param new_count integer
|
||||
--- @return Gitsigns.Hunk.Hunk
|
||||
function M.create_hunk(old_start, old_count, new_start, new_count)
|
||||
return {
|
||||
removed = { start = old_start, count = old_count, lines = {} },
|
||||
added = { start = new_start, count = new_count, lines = {} },
|
||||
head = ('@@ -%d%s +%d%s @@'):format(
|
||||
old_start,
|
||||
old_count > 0 and ',' .. old_count or '',
|
||||
new_start,
|
||||
new_count > 0 and ',' .. new_count or ''
|
||||
),
|
||||
|
||||
vend = new_start + math.max(new_count - 1, 0),
|
||||
type = new_count == 0 and 'delete' or old_count == 0 and 'add' or 'change',
|
||||
}
|
||||
end
|
||||
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @param top integer
|
||||
--- @param bot integer
|
||||
--- @return Gitsigns.Hunk.Hunk
|
||||
function M.create_partial_hunk(hunks, top, bot)
|
||||
local pretop, precount = top, bot - top + 1
|
||||
for _, h in ipairs(hunks) do
|
||||
local added_in_hunk = h.added.count - h.removed.count
|
||||
|
||||
local added_in_range = 0
|
||||
if h.added.start >= top and h.vend <= bot then
|
||||
-- Range contains hunk
|
||||
added_in_range = added_in_hunk
|
||||
else
|
||||
local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count))
|
||||
local added_above_top = max(0, top - (h.added.start + h.removed.count))
|
||||
|
||||
if h.added.start >= top and h.added.start <= bot then
|
||||
-- Range top intersects hunk
|
||||
added_in_range = added_above_bot
|
||||
elseif h.vend >= top and h.vend <= bot then
|
||||
-- Range bottom intersects hunk
|
||||
added_in_range = added_in_hunk - added_above_top
|
||||
pretop = pretop - added_above_top
|
||||
elseif h.added.start <= top and h.vend >= bot then
|
||||
-- Range within hunk
|
||||
added_in_range = added_above_bot - added_above_top
|
||||
pretop = pretop - added_above_top
|
||||
end
|
||||
|
||||
if top > h.vend then
|
||||
pretop = pretop - added_in_hunk
|
||||
end
|
||||
end
|
||||
|
||||
precount = precount - added_in_range
|
||||
end
|
||||
|
||||
if precount == 0 then
|
||||
pretop = pretop - 1
|
||||
end
|
||||
|
||||
return M.create_hunk(pretop, precount, top, bot - top + 1)
|
||||
end
|
||||
|
||||
--- @param hunk Gitsigns.Hunk.Hunk
|
||||
--- @param fileformat string
|
||||
--- @return string[]
|
||||
function M.patch_lines(hunk, fileformat)
|
||||
local lines = {} --- @type string[]
|
||||
for _, l in ipairs(hunk.removed.lines) do
|
||||
lines[#lines + 1] = '-' .. l
|
||||
end
|
||||
for _, l in ipairs(hunk.added.lines) do
|
||||
lines[#lines + 1] = '+' .. l
|
||||
end
|
||||
|
||||
if fileformat == 'dos' then
|
||||
lines = util.strip_cr(lines)
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
local function tointeger(x)
|
||||
return tonumber(x) --[[@as integer]]
|
||||
end
|
||||
|
||||
--- @param line string
|
||||
--- @return Gitsigns.Hunk.Hunk
|
||||
function M.parse_diff_line(line)
|
||||
local diffkey = vim.trim(vim.split(line, '@@', { plain = true })[2])
|
||||
|
||||
-- diffKey: "-xx,n +yy"
|
||||
-- pre: {xx, n}, now: {yy}
|
||||
--- @type string[], string[]
|
||||
local pre, now = unpack(vim.tbl_map(
|
||||
--- @param s string
|
||||
--- @return string[]
|
||||
function(s)
|
||||
return vim.split(string.sub(s, 2), ',')
|
||||
end,
|
||||
vim.split(diffkey, ' ')
|
||||
))
|
||||
|
||||
local hunk = M.create_hunk(
|
||||
tointeger(pre[1]),
|
||||
(tointeger(pre[2]) or 1),
|
||||
tointeger(now[1]),
|
||||
(tointeger(now[2]) or 1)
|
||||
)
|
||||
|
||||
hunk.head = line
|
||||
|
||||
return hunk
|
||||
end
|
||||
|
||||
--- @param hunk Gitsigns.Hunk.Hunk
|
||||
--- @return integer
|
||||
local function change_end(hunk)
|
||||
if hunk.added.count == 0 then
|
||||
-- delete
|
||||
return hunk.added.start
|
||||
elseif hunk.removed.count == 0 then
|
||||
-- add
|
||||
return hunk.added.start + hunk.added.count - 1
|
||||
else
|
||||
-- change
|
||||
return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1
|
||||
end
|
||||
end
|
||||
|
||||
--- Calculate signs needed to be applied from a hunk for a specified line range.
|
||||
--- @param hunk Gitsigns.Hunk.Hunk
|
||||
--- @param next Gitsigns.Hunk.Hunk?
|
||||
--- @param min_lnum integer
|
||||
--- @param max_lnum integer
|
||||
--- @param untracked boolean
|
||||
--- @return Gitsigns.Sign[]
|
||||
function M.calc_signs(hunk, next, min_lnum, max_lnum, untracked)
|
||||
assert(
|
||||
not untracked or hunk.type == 'add',
|
||||
string.format('Invalid hunk with untracked=%s hunk="%s"', untracked, hunk.head)
|
||||
)
|
||||
min_lnum = min_lnum or 1
|
||||
max_lnum = max_lnum or math.huge
|
||||
local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count
|
||||
|
||||
if hunk.type == 'delete' and start == 0 then
|
||||
if min_lnum <= 1 then
|
||||
-- topdelete signs get placed one row lower
|
||||
return { { type = 'topdelete', count = removed, lnum = 1 } }
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
--- @type Gitsigns.Sign[]
|
||||
local signs = {}
|
||||
|
||||
local cend = change_end(hunk)
|
||||
|
||||
-- if this is a change hunk, mark changedelete if lines were removed or if the
|
||||
-- next hunk removes on this hunks last line
|
||||
local changedelete = false
|
||||
if hunk.type == 'change' then
|
||||
changedelete = removed > added
|
||||
if next ~= nil and next.type == 'delete' then
|
||||
changedelete = changedelete or hunk.added.start + hunk.added.count - 1 == next.added.start
|
||||
end
|
||||
end
|
||||
|
||||
for lnum = max(start, min_lnum), min(cend, max_lnum) do
|
||||
signs[#signs + 1] = {
|
||||
type = (changedelete and lnum == cend) and 'changedelete'
|
||||
or untracked and 'untracked'
|
||||
or hunk.type,
|
||||
count = lnum == start and (hunk.type == 'add' and added or removed) or nil,
|
||||
lnum = lnum,
|
||||
}
|
||||
end
|
||||
|
||||
if hunk.type == 'change' and added > removed and hunk.vend >= min_lnum and cend <= max_lnum then
|
||||
for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do
|
||||
signs[#signs + 1] = {
|
||||
type = 'add',
|
||||
count = lnum == hunk.vend and (added - removed) or nil,
|
||||
lnum = lnum,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return signs
|
||||
end
|
||||
|
||||
--- @param relpath string
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @param mode_bits string
|
||||
--- @param invert? boolean
|
||||
--- @return string[]
|
||||
function M.create_patch(relpath, hunks, mode_bits, invert)
|
||||
invert = invert or false
|
||||
|
||||
local results = {
|
||||
string.format('diff --git a/%s b/%s', relpath, relpath),
|
||||
'index 000000..000000 ' .. mode_bits,
|
||||
'--- a/' .. relpath,
|
||||
'+++ b/' .. relpath,
|
||||
}
|
||||
|
||||
local offset = 0
|
||||
|
||||
for _, process_hunk in ipairs(hunks) do
|
||||
local start, pre_count, now_count =
|
||||
process_hunk.removed.start, process_hunk.removed.count, process_hunk.added.count
|
||||
|
||||
if process_hunk.type == 'add' then
|
||||
start = start + 1
|
||||
end
|
||||
|
||||
local pre_lines = process_hunk.removed.lines
|
||||
local now_lines = process_hunk.added.lines
|
||||
|
||||
if invert then
|
||||
pre_count, now_count = now_count, pre_count --- @type integer, integer
|
||||
pre_lines, now_lines = now_lines, pre_lines --- @type string[], string[]
|
||||
end
|
||||
|
||||
table.insert(
|
||||
results,
|
||||
string.format('@@ -%s,%s +%s,%s @@', start, pre_count, start + offset, now_count)
|
||||
)
|
||||
for _, l in ipairs(pre_lines) do
|
||||
results[#results + 1] = '-' .. l
|
||||
end
|
||||
|
||||
if process_hunk.removed.no_nl_at_eof then
|
||||
results[#results + 1] = '\\ No newline at end of file'
|
||||
end
|
||||
|
||||
for _, l in ipairs(now_lines) do
|
||||
results[#results + 1] = '+' .. l
|
||||
end
|
||||
|
||||
if process_hunk.added.no_nl_at_eof then
|
||||
results[#results + 1] = '\\ No newline at end of file'
|
||||
end
|
||||
|
||||
process_hunk.removed.start = start + offset
|
||||
offset = offset + (now_count - pre_count)
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @return Gitsigns.StatusObj
|
||||
function M.get_summary(hunks)
|
||||
--- @type Gitsigns.StatusObj
|
||||
local status = { added = 0, changed = 0, removed = 0 }
|
||||
|
||||
for _, hunk in ipairs(hunks or {}) do
|
||||
if hunk.type == 'add' then
|
||||
status.added = status.added + hunk.added.count
|
||||
elseif hunk.type == 'delete' then
|
||||
status.removed = status.removed + hunk.removed.count
|
||||
elseif hunk.type == 'change' then
|
||||
local add, remove = hunk.added.count, hunk.removed.count
|
||||
local delta = min(add, remove)
|
||||
status.changed = status.changed + delta
|
||||
status.added = status.added + add - delta
|
||||
status.removed = status.removed + remove - delta
|
||||
end
|
||||
end
|
||||
|
||||
return status
|
||||
end
|
||||
|
||||
--- @param lnum integer
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @return Gitsigns.Hunk.Hunk?, integer?
|
||||
function M.find_hunk(lnum, hunks)
|
||||
for i, hunk in ipairs(hunks or {}) do
|
||||
if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then
|
||||
return hunk, i
|
||||
end
|
||||
|
||||
if hunk.added.start <= lnum and hunk.vend >= lnum then
|
||||
return hunk, i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param lnum integer
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @param direction 'first'|'last'|'next'|'prev'
|
||||
--- @param wrap boolean
|
||||
--- @return integer?
|
||||
function M.find_nearest_hunk(lnum, hunks, direction, wrap)
|
||||
if direction == 'first' then
|
||||
return 1
|
||||
elseif direction == 'last' then
|
||||
return #hunks
|
||||
elseif direction == 'next' then
|
||||
if hunks[1].added.start > lnum then
|
||||
return 1
|
||||
end
|
||||
for i = #hunks, 1, -1 do
|
||||
if hunks[i].added.start <= lnum then
|
||||
if i + 1 <= #hunks and hunks[i + 1].added.start > lnum then
|
||||
return i + 1
|
||||
elseif wrap then
|
||||
return 1
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif direction == 'prev' then
|
||||
if math.max(hunks[#hunks].vend) < lnum then
|
||||
return #hunks
|
||||
end
|
||||
for i = 1, #hunks do
|
||||
if lnum <= math.max(hunks[i].vend, 1) then
|
||||
if i > 1 and math.max(hunks[i - 1].vend, 1) < lnum then
|
||||
return i - 1
|
||||
elseif wrap then
|
||||
return #hunks
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param a Gitsigns.Hunk.Hunk[]?
|
||||
--- @param b Gitsigns.Hunk.Hunk[]?
|
||||
--- @return boolean
|
||||
function M.compare_heads(a, b)
|
||||
if (a == nil) ~= (b == nil) then
|
||||
return true
|
||||
elseif a and #a ~= #b then
|
||||
return true
|
||||
end
|
||||
for i, ah in ipairs(a or {}) do
|
||||
--- @diagnostic disable-next-line:need-check-nil
|
||||
if b[i].head ~= ah.head then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- @param a Gitsigns.Hunk.Hunk
|
||||
--- @param b Gitsigns.Hunk.Hunk
|
||||
--- @return boolean
|
||||
local function compare_new(a, b)
|
||||
if a.added.start ~= b.added.start then
|
||||
return false
|
||||
end
|
||||
|
||||
if a.added.count ~= b.added.count then
|
||||
return false
|
||||
end
|
||||
|
||||
for i = 1, a.added.count do
|
||||
if a.added.lines[i] ~= b.added.lines[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Return hunks in a using b's hunks as a filter. Only compare the 'new' section
|
||||
--- of the hunk.
|
||||
---
|
||||
--- Eg. Given:
|
||||
---
|
||||
--- a = {
|
||||
--- 1 = '@@ -24 +25,1 @@',
|
||||
--- 2 = '@@ -32 +34,1 @@',
|
||||
--- 3 = '@@ -37 +40,1 @@'
|
||||
--- }
|
||||
---
|
||||
--- b = {
|
||||
--- 1 = '@@ -26 +25,1 @@'
|
||||
--- }
|
||||
---
|
||||
--- Since a[1] and b[1] introduce the same changes to the buffer (both have
|
||||
--- +25,1), we exclude this hunk in the output so we return:
|
||||
---
|
||||
--- {
|
||||
--- 1 = '@@ -32 +34,1 @@',
|
||||
--- 2 = '@@ -37 +40,1 @@'
|
||||
--- }
|
||||
---
|
||||
--- @param a Gitsigns.Hunk.Hunk[]
|
||||
--- @param b Gitsigns.Hunk.Hunk[]
|
||||
--- @return Gitsigns.Hunk.Hunk[]?
|
||||
function M.filter_common(a, b)
|
||||
if not a and not b then
|
||||
return
|
||||
end
|
||||
|
||||
a, b = a or {}, b or {}
|
||||
local max_iter = math.max(#a, #b)
|
||||
|
||||
local a_i = 1
|
||||
local b_i = 1
|
||||
|
||||
--- @type Gitsigns.Hunk.Hunk[]
|
||||
local ret = {}
|
||||
|
||||
for _ = 1, max_iter do
|
||||
local a_h, b_h = a[a_i], b[b_i]
|
||||
|
||||
if not a_h then
|
||||
-- Reached the end of a
|
||||
break
|
||||
end
|
||||
|
||||
if not b_h then
|
||||
-- Reached the end of b, add remainder of a
|
||||
for i = a_i, #a do
|
||||
ret[#ret + 1] = a[i]
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
if a_h.added.start > b_h.added.start then
|
||||
-- a pointer is ahead of b; increment b pointer
|
||||
b_i = b_i + 1
|
||||
elseif a_h.added.start < b_h.added.start then
|
||||
-- b pointer is ahead of a; add a_h to ret and increment a pointer
|
||||
ret[#ret + 1] = a_h
|
||||
a_i = a_i + 1
|
||||
else -- a_h.start == b_h.start
|
||||
-- a_h and b_h start on the same line, if hunks have the same changes then
|
||||
-- skip (filtered) otherwise add a_h to ret. Increment both hunk
|
||||
-- pointers
|
||||
-- TODO(lewis6991): Be smarter; if bh intercepts then break down ah.
|
||||
if not compare_new(a_h, b_h) then
|
||||
ret[#ret + 1] = a_h
|
||||
end
|
||||
a_i = a_i + 1
|
||||
b_i = b_i + 1
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,591 @@
|
||||
local async = require('gitsigns.async')
|
||||
|
||||
local gs_cache = require('gitsigns.cache')
|
||||
local cache = gs_cache.cache
|
||||
|
||||
local Signs = require('gitsigns.signs')
|
||||
local Status = require('gitsigns.status')
|
||||
|
||||
local debounce_trailing = require('gitsigns.debounce').debounce_trailing
|
||||
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
|
||||
|
||||
local log = require('gitsigns.debug.log')
|
||||
local dprint = log.dprint
|
||||
|
||||
local util = require('gitsigns.util')
|
||||
local run_diff = require('gitsigns.diff')
|
||||
|
||||
local gs_hunks = require('gitsigns.hunks')
|
||||
|
||||
local config = require('gitsigns.config').config
|
||||
|
||||
local api = vim.api
|
||||
|
||||
local signs_normal --- @type Gitsigns.Signs
|
||||
local signs_staged --- @type Gitsigns.Signs
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param signs Gitsigns.Signs
|
||||
--- @param hunks Gitsigns.Hunk.Hunk[]
|
||||
--- @param top integer
|
||||
--- @param bot integer
|
||||
--- @param clear boolean
|
||||
--- @param untracked boolean
|
||||
local function apply_win_signs0(bufnr, signs, hunks, top, bot, clear, untracked)
|
||||
if clear then
|
||||
signs:remove(bufnr) -- Remove all signs
|
||||
end
|
||||
|
||||
for i, hunk in ipairs(hunks or {}) do
|
||||
--- @type Gitsigns.Hunk.Hunk?
|
||||
local next = hunks[i + 1]
|
||||
|
||||
-- To stop the sign column width changing too much, if there are signs to be
|
||||
-- added but none of them are visible in the window, then make sure to add at
|
||||
-- least one sign. Only do this on the first call after an update when we all
|
||||
-- the signs have been cleared.
|
||||
if clear and i == 1 then
|
||||
signs:add(
|
||||
bufnr,
|
||||
gs_hunks.calc_signs(hunk, next, hunk.added.start, hunk.added.start, untracked)
|
||||
)
|
||||
end
|
||||
|
||||
if top <= hunk.vend and bot >= hunk.added.start then
|
||||
signs:add(bufnr, gs_hunks.calc_signs(hunk, next, top, bot, untracked))
|
||||
end
|
||||
if hunk.added.start > bot then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param top integer
|
||||
--- @param bot integer
|
||||
--- @param clear boolean
|
||||
local function apply_win_signs(bufnr, top, bot, clear)
|
||||
local bcache = cache[bufnr]
|
||||
if not bcache then
|
||||
return
|
||||
end
|
||||
|
||||
local untracked = bcache.git_obj.object_name == nil
|
||||
apply_win_signs0(bufnr, signs_normal, bcache.hunks, top, bot, clear, untracked)
|
||||
if signs_staged then
|
||||
apply_win_signs0(bufnr, signs_staged, bcache.hunks_staged, top, bot, clear, false)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param blame table<integer,Gitsigns.BlameInfo?>?
|
||||
--- @param first integer
|
||||
--- @param last_orig integer
|
||||
--- @param last_new integer
|
||||
local function on_lines_blame(blame, first, last_orig, last_new)
|
||||
if not blame then
|
||||
return
|
||||
end
|
||||
|
||||
if last_new ~= last_orig then
|
||||
if last_new < last_orig then
|
||||
util.list_remove(blame, last_new, last_orig)
|
||||
else
|
||||
util.list_insert(blame, last_orig, last_new)
|
||||
end
|
||||
end
|
||||
|
||||
for i = math.min(first + 1, last_new), math.max(first + 1, last_new) do
|
||||
blame[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- @param buf integer
|
||||
--- @param first integer
|
||||
--- @param last_orig integer
|
||||
--- @param last_new integer
|
||||
--- @return true?
|
||||
function M.on_lines(buf, first, last_orig, last_new)
|
||||
local bcache = cache[buf]
|
||||
if not bcache then
|
||||
dprint('Cache for buffer was nil. Detaching')
|
||||
return true
|
||||
end
|
||||
|
||||
on_lines_blame(bcache.blame, first, last_orig, last_new)
|
||||
|
||||
signs_normal:on_lines(buf, first, last_orig, last_new)
|
||||
if signs_staged then
|
||||
signs_staged:on_lines(buf, first, last_orig, last_new)
|
||||
end
|
||||
|
||||
-- Signs in changed regions get invalidated so we need to force a redraw if
|
||||
-- any signs get removed.
|
||||
if bcache.hunks and signs_normal:contains(buf, first, last_new) then
|
||||
-- Force a sign redraw on the next update (fixes #521)
|
||||
bcache.force_next_update = true
|
||||
end
|
||||
|
||||
if signs_staged then
|
||||
if bcache.hunks_staged and signs_staged:contains(buf, first, last_new) then
|
||||
-- Force a sign redraw on the next update (fixes #521)
|
||||
bcache.force_next_update = true
|
||||
end
|
||||
end
|
||||
|
||||
M.update_debounced(buf)
|
||||
end
|
||||
|
||||
local ns = api.nvim_create_namespace('gitsigns')
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param row integer
|
||||
local function apply_word_diff(bufnr, row)
|
||||
-- Don't run on folded lines
|
||||
if vim.fn.foldclosed(row + 1) ~= -1 then
|
||||
return
|
||||
end
|
||||
|
||||
local bcache = cache[bufnr]
|
||||
|
||||
if not bcache or not bcache.hunks then
|
||||
return
|
||||
end
|
||||
|
||||
local line = api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1]
|
||||
if not line then
|
||||
-- Invalid line
|
||||
return
|
||||
end
|
||||
|
||||
local lnum = row + 1
|
||||
|
||||
local hunk = gs_hunks.find_hunk(lnum, bcache.hunks)
|
||||
if not hunk then
|
||||
-- No hunk at line
|
||||
return
|
||||
end
|
||||
|
||||
if hunk.added.count ~= hunk.removed.count then
|
||||
-- Only word diff if added count == removed
|
||||
return
|
||||
end
|
||||
|
||||
local pos = lnum - hunk.added.start + 1
|
||||
|
||||
local added_line = hunk.added.lines[pos]
|
||||
local removed_line = hunk.removed.lines[pos]
|
||||
|
||||
local _, added_regions = require('gitsigns.diff_int').run_word_diff(
|
||||
{ removed_line },
|
||||
{ added_line }
|
||||
)
|
||||
|
||||
local cols = #line
|
||||
|
||||
for _, region in ipairs(added_regions) do
|
||||
local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1
|
||||
if ecol == scol then
|
||||
-- Make sure region is at least 1 column wide so deletes can be shown
|
||||
ecol = scol + 1
|
||||
end
|
||||
|
||||
local hl_group = rtype == 'add' and 'GitSignsAddLnInline'
|
||||
or rtype == 'change' and 'GitSignsChangeLnInline'
|
||||
or 'GitSignsDeleteLnInline'
|
||||
|
||||
local opts = {
|
||||
ephemeral = true,
|
||||
priority = 1000,
|
||||
}
|
||||
|
||||
if ecol > cols and ecol == scol + 1 then
|
||||
-- delete on last column, use virtual text instead
|
||||
opts.virt_text = { { ' ', hl_group } }
|
||||
opts.virt_text_pos = 'overlay'
|
||||
else
|
||||
opts.end_col = ecol
|
||||
opts.hl_group = hl_group
|
||||
end
|
||||
|
||||
api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts)
|
||||
util.redraw({ buf = bufnr, range = { row, row + 1 } })
|
||||
end
|
||||
end
|
||||
|
||||
local ns_rm = api.nvim_create_namespace('gitsigns_removed')
|
||||
|
||||
local VIRT_LINE_LEN = 300
|
||||
|
||||
--- @param bufnr integer
|
||||
local function clear_deleted(bufnr)
|
||||
local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {})
|
||||
for _, mark in ipairs(marks) do
|
||||
api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1])
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param nsd integer
|
||||
--- @param hunk Gitsigns.Hunk.Hunk
|
||||
function M.show_deleted(bufnr, nsd, hunk)
|
||||
local virt_lines = {} --- @type {[1]: string, [2]: string}[][]
|
||||
|
||||
for i, line in ipairs(hunk.removed.lines) do
|
||||
local vline = {} --- @type {[1]: string, [2]: string}[]
|
||||
local last_ecol = 1
|
||||
|
||||
if config.word_diff then
|
||||
local regions = require('gitsigns.diff_int').run_word_diff(
|
||||
{ hunk.removed.lines[i] },
|
||||
{ hunk.added.lines[i] }
|
||||
)
|
||||
|
||||
for _, region in ipairs(regions) do
|
||||
local rline, scol, ecol = region[1], region[3], region[4]
|
||||
if rline > 1 then
|
||||
break
|
||||
end
|
||||
vline[#vline + 1] = { line:sub(last_ecol, scol - 1), 'GitSignsDeleteVirtLn' }
|
||||
vline[#vline + 1] = { line:sub(scol, ecol - 1), 'GitSignsDeleteVirtLnInline' }
|
||||
last_ecol = ecol
|
||||
end
|
||||
end
|
||||
|
||||
if #line > 0 then
|
||||
vline[#vline + 1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn' }
|
||||
end
|
||||
|
||||
-- Add extra padding so the entire line is highlighted
|
||||
local padding = string.rep(' ', VIRT_LINE_LEN - #line)
|
||||
vline[#vline + 1] = { padding, 'GitSignsDeleteVirtLn' }
|
||||
|
||||
virt_lines[i] = vline
|
||||
end
|
||||
|
||||
local topdelete = hunk.added.start == 0 and hunk.type == 'delete'
|
||||
|
||||
local row = topdelete and 0 or hunk.added.start - 1
|
||||
api.nvim_buf_set_extmark(bufnr, nsd, row, -1, {
|
||||
virt_lines = virt_lines,
|
||||
-- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166
|
||||
virt_lines_above = hunk.type ~= 'delete' or topdelete,
|
||||
})
|
||||
end
|
||||
|
||||
--- @param win integer
|
||||
--- @param lnum integer
|
||||
--- @param width integer
|
||||
--- @return string str
|
||||
--- @return {group:string, start:integer}[]? highlights
|
||||
local function build_lno_str(win, lnum, width)
|
||||
local has_col, statuscol =
|
||||
pcall(api.nvim_get_option_value, 'statuscolumn', { win = win, scope = 'local' })
|
||||
if has_col and statuscol and statuscol ~= '' then
|
||||
local ok, data = pcall(api.nvim_eval_statusline, statuscol, {
|
||||
winid = win,
|
||||
use_statuscol_lnum = lnum,
|
||||
highlights = true,
|
||||
})
|
||||
if ok then
|
||||
return data.str, data.highlights
|
||||
end
|
||||
end
|
||||
return string.format('%' .. width .. 'd', lnum)
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param nsd integer
|
||||
--- @param hunk Gitsigns.Hunk.Hunk
|
||||
--- @param staged boolean?
|
||||
--- @return integer winid
|
||||
function M.show_deleted_in_float(bufnr, nsd, hunk, staged)
|
||||
local cwin = api.nvim_get_current_win()
|
||||
local virt_lines = {} --- @type {[1]: string, [2]: string}[][]
|
||||
local textoff = vim.fn.getwininfo(cwin)[1].textoff --[[@as integer]]
|
||||
for i = 1, hunk.removed.count do
|
||||
local sc = build_lno_str(cwin, hunk.removed.start + i, textoff - 1)
|
||||
virt_lines[i] = { { sc, 'LineNr' } }
|
||||
end
|
||||
|
||||
local topdelete = hunk.added.start == 0 and hunk.type == 'delete'
|
||||
local virt_lines_above = hunk.type ~= 'delete' or topdelete
|
||||
|
||||
local row = topdelete and 0 or hunk.added.start - 1
|
||||
api.nvim_buf_set_extmark(bufnr, nsd, row, -1, {
|
||||
virt_lines = virt_lines,
|
||||
-- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166
|
||||
virt_lines_above = virt_lines_above,
|
||||
virt_lines_leftcol = true,
|
||||
})
|
||||
|
||||
local bcache = cache[bufnr]
|
||||
local pbufnr = api.nvim_create_buf(false, true)
|
||||
local text = staged and bcache.compare_text_head or bcache.compare_text
|
||||
api.nvim_buf_set_lines(pbufnr, 0, -1, false, assert(text))
|
||||
|
||||
local width = api.nvim_win_get_width(0)
|
||||
|
||||
local bufpos_offset = virt_lines_above and not topdelete and 1 or 0
|
||||
|
||||
local pwinid = api.nvim_open_win(pbufnr, false, {
|
||||
relative = 'win',
|
||||
win = cwin,
|
||||
width = width - textoff,
|
||||
height = hunk.removed.count,
|
||||
anchor = 'SW',
|
||||
bufpos = { hunk.added.start - bufpos_offset, 0 },
|
||||
style = 'minimal',
|
||||
})
|
||||
|
||||
vim.bo[pbufnr].filetype = vim.bo[bufnr].filetype
|
||||
vim.bo[pbufnr].bufhidden = 'wipe'
|
||||
vim.wo[pwinid].scrolloff = 0
|
||||
|
||||
api.nvim_win_call(pwinid, function()
|
||||
-- Expand folds
|
||||
vim.cmd('normal ' .. 'zR')
|
||||
|
||||
-- Navigate to hunk
|
||||
vim.cmd('normal ' .. tostring(hunk.removed.start) .. 'gg')
|
||||
vim.cmd('normal ' .. vim.api.nvim_replace_termcodes('z<CR>', true, false, true))
|
||||
end)
|
||||
|
||||
local last_lnum = api.nvim_buf_line_count(bufnr)
|
||||
|
||||
-- Apply highlights
|
||||
|
||||
for i = hunk.removed.start, hunk.removed.start + hunk.removed.count do
|
||||
api.nvim_buf_set_extmark(pbufnr, nsd, i - 1, 0, {
|
||||
hl_group = 'GitSignsDeleteVirtLn',
|
||||
hl_eol = true,
|
||||
end_row = i,
|
||||
strict = i == last_lnum,
|
||||
priority = 1000,
|
||||
})
|
||||
end
|
||||
|
||||
local removed_regions =
|
||||
require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines)
|
||||
|
||||
for _, region in ipairs(removed_regions) do
|
||||
local start_row = (hunk.removed.start - 1) + (region[1] - 1)
|
||||
local start_col = region[3] - 1
|
||||
local end_col = region[4] - 1
|
||||
api.nvim_buf_set_extmark(pbufnr, nsd, start_row, start_col, {
|
||||
hl_group = 'GitSignsDeleteVirtLnInline',
|
||||
end_col = end_col,
|
||||
end_row = start_row,
|
||||
priority = 1001,
|
||||
})
|
||||
end
|
||||
|
||||
return pwinid
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param nsw integer
|
||||
--- @param hunk Gitsigns.Hunk.Hunk
|
||||
function M.show_added(bufnr, nsw, hunk)
|
||||
local start_row = hunk.added.start - 1
|
||||
|
||||
for offset = 0, hunk.added.count - 1 do
|
||||
local row = start_row + offset
|
||||
api.nvim_buf_set_extmark(bufnr, nsw, row, 0, {
|
||||
end_row = row + 1,
|
||||
hl_group = 'GitSignsAddPreview',
|
||||
hl_eol = true,
|
||||
priority = 1000,
|
||||
})
|
||||
end
|
||||
|
||||
local _, added_regions =
|
||||
require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines)
|
||||
|
||||
for _, region in ipairs(added_regions) do
|
||||
local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1
|
||||
|
||||
-- Special case to handle cr at eol in buffer but not in show text
|
||||
local cr_at_eol_change = rtype == 'change' and vim.endswith(hunk.added.lines[offset + 1], '\r')
|
||||
|
||||
api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, {
|
||||
end_col = ecol,
|
||||
strict = not cr_at_eol_change,
|
||||
hl_group = rtype == 'add' and 'GitSignsAddInline'
|
||||
or rtype == 'change' and 'GitSignsChangeInline'
|
||||
or 'GitSignsDeleteInline',
|
||||
priority = 1001,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
local function update_show_deleted(bufnr)
|
||||
local bcache = cache[bufnr]
|
||||
|
||||
clear_deleted(bufnr)
|
||||
if config.show_deleted then
|
||||
for _, hunk in ipairs(bcache.hunks or {}) do
|
||||
M.show_deleted(bufnr, ns_rm, hunk)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @async
|
||||
--- @nodiscard
|
||||
--- @param bufnr integer
|
||||
--- @param check_compare_text? boolean
|
||||
--- @return boolean
|
||||
function M.schedule(bufnr, check_compare_text)
|
||||
async.scheduler()
|
||||
if not api.nvim_buf_is_valid(bufnr) then
|
||||
dprint('Buffer not valid, aborting')
|
||||
return false
|
||||
end
|
||||
if not cache[bufnr] then
|
||||
dprint('Has detached, aborting')
|
||||
return false
|
||||
end
|
||||
if check_compare_text and not cache[bufnr].compare_text then
|
||||
dprint('compare_text was invalid, aborting')
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Ensure updates cannot be interleaved.
|
||||
--- Since updates are asynchronous we need to make sure an update isn't performed
|
||||
--- whilst another one is in progress. If this happens then schedule another
|
||||
--- update after the current one has completed.
|
||||
--- @param bufnr integer
|
||||
M.update = throttle_by_id(function(bufnr)
|
||||
if not M.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
local bcache = cache[bufnr]
|
||||
local old_hunks, old_hunks_staged = bcache.hunks, bcache.hunks_staged
|
||||
bcache.hunks, bcache.hunks_staged = nil, nil
|
||||
|
||||
local git_obj = bcache.git_obj
|
||||
local file_mode = bcache.file_mode
|
||||
|
||||
if not bcache.compare_text or config._refresh_staged_on_update or file_mode then
|
||||
if file_mode then
|
||||
bcache.compare_text = util.file_lines(git_obj.file)
|
||||
else
|
||||
bcache.compare_text = git_obj:get_show_text()
|
||||
end
|
||||
if not M.schedule(bufnr, true) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local buftext = util.buf_lines(bufnr)
|
||||
|
||||
bcache.hunks = run_diff(bcache.compare_text, buftext)
|
||||
if not M.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
if config._signs_staged_enable and not file_mode and not git_obj.revision then
|
||||
if not bcache.compare_text_head or config._refresh_staged_on_update then
|
||||
bcache.compare_text_head = git_obj:get_show_text('HEAD')
|
||||
if not M.schedule(bufnr, true) then
|
||||
return
|
||||
end
|
||||
end
|
||||
local hunks_head = run_diff(bcache.compare_text_head, buftext)
|
||||
if not M.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
bcache.hunks_staged = gs_hunks.filter_common(hunks_head, bcache.hunks)
|
||||
end
|
||||
|
||||
-- Note the decoration provider may have invalidated bcache.hunks at this
|
||||
-- point
|
||||
if
|
||||
bcache.force_next_update
|
||||
or gs_hunks.compare_heads(bcache.hunks, old_hunks)
|
||||
or gs_hunks.compare_heads(bcache.hunks_staged, old_hunks_staged)
|
||||
then
|
||||
-- Apply signs to the window. Other signs will be added by the decoration
|
||||
-- provider as they are drawn.
|
||||
apply_win_signs(bufnr, vim.fn.line('w0'), vim.fn.line('w$'), true)
|
||||
|
||||
update_show_deleted(bufnr)
|
||||
bcache.force_next_update = false
|
||||
|
||||
local summary = gs_hunks.get_summary(bcache.hunks)
|
||||
summary.head = git_obj.repo.abbrev_head
|
||||
Status:update(bufnr, summary)
|
||||
end
|
||||
end, true)
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param keep_signs? boolean
|
||||
function M.detach(bufnr, keep_signs)
|
||||
if not keep_signs then
|
||||
-- Remove all signs
|
||||
signs_normal:remove(bufnr)
|
||||
if signs_staged then
|
||||
signs_staged:remove(bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.reset_signs()
|
||||
-- Remove all signs
|
||||
if signs_normal then
|
||||
signs_normal:reset()
|
||||
end
|
||||
if signs_staged then
|
||||
signs_staged:reset()
|
||||
end
|
||||
end
|
||||
|
||||
--- @param _cb 'win'
|
||||
--- @param _winid integer
|
||||
--- @param bufnr integer
|
||||
--- @param topline integer
|
||||
--- @param botline_guess integer
|
||||
--- @return false?
|
||||
local function on_win(_cb, _winid, bufnr, topline, botline_guess)
|
||||
local bcache = cache[bufnr]
|
||||
if not bcache or not bcache.hunks then
|
||||
return false
|
||||
end
|
||||
local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr))
|
||||
|
||||
apply_win_signs(bufnr, topline + 1, botline + 1, false)
|
||||
|
||||
if not (config.word_diff and config.diff_opts.internal) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--- @param _cb 'line'
|
||||
--- @param _winid integer
|
||||
--- @param bufnr integer
|
||||
--- @param row integer
|
||||
local function on_line(_cb, _winid, bufnr, row)
|
||||
apply_word_diff(bufnr, row)
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
-- Calling this before any await calls will stop nvim's intro messages being
|
||||
-- displayed
|
||||
api.nvim_set_decoration_provider(ns, {
|
||||
on_win = on_win,
|
||||
on_line = on_line,
|
||||
})
|
||||
|
||||
signs_normal = Signs.new(config.signs)
|
||||
if config._signs_staged_enable then
|
||||
signs_staged = Signs.new(config._signs_staged, 'staged')
|
||||
end
|
||||
|
||||
M.update_debounced = debounce_trailing(config.update_debounce, async.create(1, M.update))
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,20 @@
|
||||
local levels = vim.log.levels
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @type fun(fmt: string, ...: string)
|
||||
M.warn = vim.schedule_wrap(function(fmt, ...)
|
||||
vim.notify(fmt:format(...), levels.WARN, { title = 'gitsigns' })
|
||||
end)
|
||||
|
||||
--- @type fun(fmt: string, ...: string)
|
||||
M.error = vim.schedule_wrap(function(fmt, ...)
|
||||
vim.notify(fmt:format(...), vim.log.levels.ERROR, { title = 'gitsigns' })
|
||||
end)
|
||||
|
||||
--- @type fun(fmt: string, ...: string)
|
||||
M.error_once = vim.schedule_wrap(function(fmt, ...)
|
||||
vim.notify_once(fmt:format(...), vim.log.levels.ERROR, { title = 'gitsigns' })
|
||||
end)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,282 @@
|
||||
local M = {}
|
||||
|
||||
local api = vim.api
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param lines string[]
|
||||
--- @return integer
|
||||
local function bufnr_calc_width(bufnr, lines)
|
||||
return api.nvim_buf_call(bufnr, function()
|
||||
local width = 0
|
||||
for _, l in ipairs(lines) do
|
||||
if vim.fn.type(l) == vim.v.t_string then
|
||||
local len = vim.fn.strdisplaywidth(l)
|
||||
if len > width then
|
||||
width = len
|
||||
end
|
||||
end
|
||||
end
|
||||
return width + 1 -- Add 1 for some miinor padding
|
||||
end)
|
||||
end
|
||||
|
||||
-- Expand height until all lines are visible to account for wrapped lines.
|
||||
--- @param winid integer
|
||||
--- @param nlines integer
|
||||
--- @param border string
|
||||
local function expand_height(winid, nlines, border)
|
||||
local newheight = 0
|
||||
local maxheight = vim.o.lines - vim.o.cmdheight - (border ~= '' and 2 or 0)
|
||||
for _ = 0, 50 do
|
||||
local winheight = api.nvim_win_get_height(winid)
|
||||
if newheight > winheight then
|
||||
-- Window must be max height
|
||||
break
|
||||
end
|
||||
--- @type integer
|
||||
local wd = api.nvim_win_call(winid, function()
|
||||
return vim.fn.line('w$')
|
||||
end)
|
||||
if wd >= nlines then
|
||||
break
|
||||
end
|
||||
newheight = winheight + nlines - wd
|
||||
if newheight > maxheight then
|
||||
api.nvim_win_set_height(winid, maxheight)
|
||||
break
|
||||
end
|
||||
api.nvim_win_set_height(winid, newheight)
|
||||
end
|
||||
end
|
||||
|
||||
--- @class (exact) Gitsigns.HlMark
|
||||
--- @field hl_group string
|
||||
--- @field start_row? integer
|
||||
--- @field start_col? integer
|
||||
--- @field end_row? integer
|
||||
--- @field end_col? integer
|
||||
|
||||
--- Each element represents a multi-line segment
|
||||
--- @alias Gitsigns.LineSpec { [1]: string, [2]: Gitsigns.HlMark[]}[][]
|
||||
|
||||
--- @param hlmarks Gitsigns.HlMark[]
|
||||
--- @param row_offset integer
|
||||
local function offset_hlmarks(hlmarks, row_offset)
|
||||
for _, h in ipairs(hlmarks) do
|
||||
h.start_row = (h.start_row or 0) + row_offset
|
||||
if h.end_row then
|
||||
h.end_row = h.end_row + row_offset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Partition the text and Gitsigns.HlMarks from a Gitsigns.LineSpec
|
||||
--- @param fmt Gitsigns.LineSpec
|
||||
--- @return string[]
|
||||
--- @return Gitsigns.HlMark[]
|
||||
local function partition_linesspec(fmt)
|
||||
local lines = {} --- @type string[]
|
||||
local ret = {} --- @type Gitsigns.HlMark[]
|
||||
|
||||
local row = 0
|
||||
for _, section in ipairs(fmt) do
|
||||
local section_text = {} --- @type string[]
|
||||
local col = 0
|
||||
for _, part in ipairs(section) do
|
||||
local text, hls = part[1], part[2]
|
||||
|
||||
section_text[#section_text + 1] = text
|
||||
|
||||
local _, no_lines = text:gsub('\n', '')
|
||||
local end_row = row + no_lines --- @type integer
|
||||
local end_col = no_lines > 0 and 0 or col + #text --- @type integer
|
||||
|
||||
if type(hls) == 'string' then
|
||||
ret[#ret + 1] = {
|
||||
hl_group = hls,
|
||||
start_row = row,
|
||||
end_row = end_row,
|
||||
start_col = col,
|
||||
end_col = end_col,
|
||||
}
|
||||
else -- hl is Gitsigns.HlMark[]
|
||||
offset_hlmarks(hls, row)
|
||||
vim.list_extend(ret, hls)
|
||||
end
|
||||
|
||||
row = end_row
|
||||
col = end_col
|
||||
end
|
||||
|
||||
local section_lines = vim.split(table.concat(section_text), '\n', { plain = true })
|
||||
vim.list_extend(lines, section_lines)
|
||||
|
||||
row = row + 1
|
||||
end
|
||||
|
||||
return lines, ret
|
||||
end
|
||||
|
||||
--- @param id string|true
|
||||
local function close_all_but(id)
|
||||
for _, winid in ipairs(api.nvim_list_wins()) do
|
||||
if vim.w[winid].gitsigns_preview ~= nil and vim.w[winid].gitsigns_preview ~= id then
|
||||
pcall(api.nvim_win_close, winid, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param id string
|
||||
function M.close(id)
|
||||
for _, winid in ipairs(api.nvim_list_wins()) do
|
||||
if vim.w[winid].gitsigns_preview == id then
|
||||
pcall(api.nvim_win_close, winid, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ns = api.nvim_create_namespace('gitsigns_popup')
|
||||
|
||||
--- @param lines string[]
|
||||
--- @param highlights Gitsigns.HlMark[]
|
||||
--- @return integer bufnr
|
||||
local function create_buf(lines, highlights)
|
||||
local ts = vim.bo.tabstop
|
||||
local bufnr = api.nvim_create_buf(false, true)
|
||||
assert(bufnr, 'Failed to create buffer')
|
||||
|
||||
-- In case nvim was opened with '-M'
|
||||
vim.bo[bufnr].modifiable = true
|
||||
api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
|
||||
vim.bo[bufnr].modifiable = false
|
||||
|
||||
-- Set tabstop before calculating the buffer width so that the correct width
|
||||
-- is calculated
|
||||
vim.bo[bufnr].tabstop = ts
|
||||
|
||||
for _, hl in ipairs(highlights) do
|
||||
local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, {
|
||||
hl_group = hl.hl_group,
|
||||
end_row = hl.end_row,
|
||||
end_col = hl.end_col,
|
||||
hl_eol = true,
|
||||
})
|
||||
if not ok then
|
||||
error(vim.inspect(hl) .. '\n' .. err)
|
||||
end
|
||||
end
|
||||
|
||||
return bufnr
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param opts table
|
||||
--- @param id? string|true
|
||||
--- @return integer winid
|
||||
local function create_win(bufnr, opts, id)
|
||||
id = id or true
|
||||
|
||||
-- Close any popups not matching id
|
||||
close_all_but(id)
|
||||
|
||||
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||
|
||||
local opts1 = vim.deepcopy(opts or {})
|
||||
opts1.height = opts1.height or #lines -- Guess, adjust later
|
||||
opts1.width = opts1.width or bufnr_calc_width(bufnr, lines)
|
||||
|
||||
local winid = api.nvim_open_win(bufnr, false, opts1)
|
||||
|
||||
vim.w[winid].gitsigns_preview = id
|
||||
|
||||
if not opts.height then
|
||||
expand_height(winid, #lines, opts.border)
|
||||
end
|
||||
|
||||
if opts1.style == 'minimal' then
|
||||
-- If 'signcolumn' = auto:1-2, then a empty signcolumn will appear and cause
|
||||
-- line wrapping.
|
||||
vim.wo[winid].signcolumn = 'no'
|
||||
end
|
||||
|
||||
-- Close the popup when navigating to any window which is not the preview
|
||||
-- itself.
|
||||
local group = 'gitsigns_popup'
|
||||
local group_id = api.nvim_create_augroup(group, {})
|
||||
local old_cursor = api.nvim_win_get_cursor(0)
|
||||
|
||||
vim.keymap.set('n', 'q', '<cmd>quit!<cr>', { silent = true, buffer = bufnr })
|
||||
|
||||
api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
|
||||
group = group_id,
|
||||
callback = function()
|
||||
local cursor = api.nvim_win_get_cursor(0)
|
||||
-- Did the cursor REALLY change (neovim/neovim#12923)
|
||||
if
|
||||
(old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2])
|
||||
and api.nvim_get_current_win() ~= winid
|
||||
then
|
||||
-- Clear the augroup
|
||||
api.nvim_create_augroup(group, {})
|
||||
pcall(api.nvim_win_close, winid, true)
|
||||
return
|
||||
end
|
||||
old_cursor = cursor
|
||||
end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd('WinClosed', {
|
||||
pattern = tostring(winid),
|
||||
group = group_id,
|
||||
callback = function()
|
||||
-- Clear the augroup
|
||||
api.nvim_create_augroup(group, {})
|
||||
end,
|
||||
})
|
||||
|
||||
-- update window position to follow the cursor when scrolling
|
||||
api.nvim_create_autocmd('WinScrolled', {
|
||||
buffer = api.nvim_get_current_buf(),
|
||||
group = group_id,
|
||||
callback = function()
|
||||
if api.nvim_win_is_valid(winid) then
|
||||
api.nvim_win_set_config(winid, opts1)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return winid
|
||||
end
|
||||
|
||||
--- @param lines_spec {[1]: string, [2]: string|Gitsigns.HlMark[]}[][]
|
||||
--- @param opts table
|
||||
--- @param id? string
|
||||
--- @return integer winid, integer bufnr
|
||||
function M.create(lines_spec, opts, id)
|
||||
local lines, highlights = partition_linesspec(lines_spec)
|
||||
local bufnr = create_buf(lines, highlights)
|
||||
local winid = create_win(bufnr, opts, id)
|
||||
return winid, bufnr
|
||||
end
|
||||
|
||||
--- @param id string
|
||||
--- @return integer? winid
|
||||
function M.is_open(id)
|
||||
for _, winid in ipairs(api.nvim_list_wins()) do
|
||||
if vim.w[winid].gitsigns_preview == id then
|
||||
return winid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param id string
|
||||
--- @return integer? winid
|
||||
function M.focus_open(id)
|
||||
local winid = M.is_open(id)
|
||||
if winid then
|
||||
api.nvim_set_current_win(winid)
|
||||
end
|
||||
return winid
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,26 @@
|
||||
local M = {}
|
||||
|
||||
function M.mk_repeatable(fn)
|
||||
return function(...)
|
||||
local args = { ... }
|
||||
local nargs = select('#', ...)
|
||||
vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action"
|
||||
|
||||
M.repeat_action = function()
|
||||
fn(unpack(args, 1, nargs))
|
||||
if vim.fn.exists('*repeat#set') == 1 then
|
||||
local action = vim.api.nvim_replace_termcodes(
|
||||
string.format('<cmd>call %s()<cr>', vim.go.operatorfunc),
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
vim.fn['repeat#set'](action, -1)
|
||||
end
|
||||
end
|
||||
|
||||
vim.cmd('normal! g@l')
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,135 @@
|
||||
local api = vim.api
|
||||
|
||||
local config = require('gitsigns.config').config
|
||||
|
||||
--- @class Gitsigns.Sign
|
||||
--- @field type Gitsigns.SignType
|
||||
--- @field count? integer
|
||||
--- @field lnum integer
|
||||
|
||||
--- @class Gitsigns.Signs
|
||||
--- @field hls table<Gitsigns.SignType,Gitsigns.SignConfig>
|
||||
--- @field name string
|
||||
--- @field group string
|
||||
--- @field config table<string,Gitsigns.SignConfig>
|
||||
--- @field ns integer
|
||||
local M = {}
|
||||
|
||||
--- @param buf integer
|
||||
--- @param last_orig? integer
|
||||
--- @param last_new? integer
|
||||
function M:on_lines(buf, _, last_orig, last_new)
|
||||
-- Remove extmarks on line deletions to mimic
|
||||
-- the behaviour of vim signs.
|
||||
if last_orig > last_new then
|
||||
self:remove(buf, last_new + 1, last_orig)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param start_lnum? integer
|
||||
--- @param end_lnum? integer
|
||||
function M:remove(bufnr, start_lnum, end_lnum)
|
||||
if start_lnum then
|
||||
api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum - 1, end_lnum or start_lnum)
|
||||
else
|
||||
api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1)
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param signs Gitsigns.Sign[]
|
||||
function M:add(bufnr, signs)
|
||||
if not config.signcolumn and not config.numhl and not config.linehl then
|
||||
-- Don't place signs if it won't show anything
|
||||
return
|
||||
end
|
||||
|
||||
for _, s in ipairs(signs) do
|
||||
if not self:contains(bufnr, s.lnum) then
|
||||
local cs = self.config[s.type]
|
||||
local text = cs.text
|
||||
if config.signcolumn and cs.show_count and s.count then
|
||||
local count = s.count
|
||||
local cc = config.count_chars
|
||||
local count_char = cc[count] or cc['+'] or ''
|
||||
text = cs.text .. count_char
|
||||
end
|
||||
|
||||
local hls = self.hls[s.type]
|
||||
|
||||
local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum - 1, -1, {
|
||||
id = s.lnum,
|
||||
sign_text = config.signcolumn and text or '',
|
||||
priority = config.sign_priority,
|
||||
sign_hl_group = hls.hl,
|
||||
number_hl_group = config.numhl and hls.numhl or nil,
|
||||
line_hl_group = config.linehl and hls.linehl or nil,
|
||||
})
|
||||
|
||||
if not ok and config.debug_mode then
|
||||
vim.schedule(function()
|
||||
error(table.concat({
|
||||
string.format('Error placing extmark on line %d', s.lnum),
|
||||
err,
|
||||
}, '\n'))
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param start integer
|
||||
---@param last? integer
|
||||
---@return boolean
|
||||
function M:contains(bufnr, start, last)
|
||||
local marks = api.nvim_buf_get_extmarks(
|
||||
bufnr,
|
||||
self.ns,
|
||||
{ start - 1, 0 },
|
||||
{ last or start, 0 },
|
||||
{ limit = 1 }
|
||||
)
|
||||
return #marks > 0
|
||||
end
|
||||
|
||||
function M:reset()
|
||||
for _, buf in ipairs(api.nvim_list_bufs()) do
|
||||
self:remove(buf)
|
||||
end
|
||||
end
|
||||
|
||||
-- local function capitalise_word(x: string): string
|
||||
-- return x:sub(1, 1):upper()..x:sub(2)
|
||||
-- end
|
||||
|
||||
function M.new(cfg, name)
|
||||
local __FUNC__ = 'signs.init'
|
||||
|
||||
-- Add when config.signs.*.[hl,numhl,linehl] are removed
|
||||
-- for _, t in ipairs {
|
||||
-- 'add',
|
||||
-- 'change',
|
||||
-- 'delete',
|
||||
-- 'topdelete',
|
||||
-- 'changedelete',
|
||||
-- 'untracked',
|
||||
-- } do
|
||||
-- local hl = string.format('GitSigns%s%s', name, capitalise_word(t))
|
||||
-- obj.hls[t] = {
|
||||
-- hl = hl,
|
||||
-- numhl = hl..'Nr',
|
||||
-- linehl = hl..'Ln',
|
||||
-- }
|
||||
-- end
|
||||
|
||||
local self = setmetatable({}, { __index = M })
|
||||
self.config = cfg
|
||||
self.hls = name == 'staged' and config._signs_staged or config.signs
|
||||
self.group = 'gitsigns_signs_' .. (name or '')
|
||||
self.ns = api.nvim_create_namespace(self.group)
|
||||
return self
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,52 @@
|
||||
local api = vim.api
|
||||
|
||||
--- @class (exact) Gitsigns.StatusObj
|
||||
--- @field added? integer
|
||||
--- @field removed? integer
|
||||
--- @field changed? integer
|
||||
--- @field head? string
|
||||
--- @field root? string
|
||||
--- @field gitdir? string
|
||||
|
||||
local M = {}
|
||||
|
||||
--- @param bufnr integer
|
||||
local function autocmd_update(bufnr)
|
||||
api.nvim_exec_autocmds('User', {
|
||||
pattern = 'GitSignsUpdate',
|
||||
modeline = false,
|
||||
data = { buffer = bufnr },
|
||||
})
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param status Gitsigns.StatusObj
|
||||
function M:update(bufnr, status)
|
||||
if not api.nvim_buf_is_loaded(bufnr) then
|
||||
return
|
||||
end
|
||||
local bstatus = vim.b[bufnr].gitsigns_status_dict
|
||||
if bstatus then
|
||||
status = vim.tbl_extend('force', bstatus, status)
|
||||
end
|
||||
vim.b[bufnr].gitsigns_head = status.head or ''
|
||||
vim.b[bufnr].gitsigns_status_dict = status
|
||||
|
||||
local config = require('gitsigns.config').config
|
||||
|
||||
vim.b[bufnr].gitsigns_status = config.status_formatter(status)
|
||||
|
||||
autocmd_update(bufnr)
|
||||
end
|
||||
|
||||
function M:clear(bufnr)
|
||||
if not api.nvim_buf_is_loaded(bufnr) then
|
||||
return
|
||||
end
|
||||
vim.b[bufnr].gitsigns_head = nil
|
||||
vim.b[bufnr].gitsigns_status_dict = nil
|
||||
vim.b[bufnr].gitsigns_status = nil
|
||||
autocmd_update(bufnr)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,19 @@
|
||||
local log = require('gitsigns.debug.log')
|
||||
|
||||
local M = {}
|
||||
|
||||
local system = vim.system or require('gitsigns.system.compat')
|
||||
|
||||
--- @param cmd string[]
|
||||
--- @param opts vim.SystemOpts
|
||||
--- @param on_exit fun(obj: vim.SystemCompleted)
|
||||
--- @return vim.SystemObj
|
||||
function M.system(cmd, opts, on_exit)
|
||||
local __FUNC__ = 'run_job'
|
||||
if log.debug_mode then
|
||||
log.dprint(table.concat(cmd, ' '))
|
||||
end
|
||||
return system(cmd, opts, on_exit)
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,338 @@
|
||||
local uv = vim.loop
|
||||
|
||||
--- @param handle uv.uv_handle_t?
|
||||
local function close_handle(handle)
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end
|
||||
|
||||
--- @type vim.SystemSig
|
||||
local SIG = {
|
||||
HUP = 1, -- Hangup
|
||||
INT = 2, -- Interrupt from keyboard
|
||||
KILL = 9, -- Kill signal
|
||||
TERM = 15, -- Termination signal
|
||||
-- STOP = 17,19,23 -- Stop the process
|
||||
}
|
||||
|
||||
--- @param state vim.SystemState
|
||||
local function close_handles(state)
|
||||
close_handle(state.handle)
|
||||
close_handle(state.stdin)
|
||||
close_handle(state.stdout)
|
||||
close_handle(state.stderr)
|
||||
close_handle(state.timer)
|
||||
end
|
||||
|
||||
--- @class Gitsigns.SystemObj : vim.SystemObj
|
||||
--- @field private _state vim.SystemState
|
||||
local SystemObj = {}
|
||||
|
||||
--- @param state vim.SystemState
|
||||
--- @return vim.SystemObj
|
||||
local function new_systemobj(state)
|
||||
return setmetatable({
|
||||
pid = state.pid,
|
||||
_state = state,
|
||||
}, { __index = SystemObj })
|
||||
end
|
||||
|
||||
--- @param signal integer|string
|
||||
function SystemObj:kill(signal)
|
||||
self._state.handle:kill(signal)
|
||||
end
|
||||
|
||||
--- @package
|
||||
--- @param signal? vim.SystemSig
|
||||
function SystemObj:_timeout(signal)
|
||||
self._state.done = 'timeout'
|
||||
self:kill(signal or SIG.TERM)
|
||||
end
|
||||
|
||||
local MAX_TIMEOUT = 2 ^ 31
|
||||
|
||||
--- @param timeout? integer
|
||||
--- @return vim.SystemCompleted
|
||||
function SystemObj:wait(timeout)
|
||||
local state = self._state
|
||||
|
||||
local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
|
||||
return state.result ~= nil
|
||||
end, nil, true)
|
||||
|
||||
if not done then
|
||||
-- Send sigkill since this cannot be caught
|
||||
self:_timeout(SIG.KILL)
|
||||
vim.wait(timeout or state.timeout or MAX_TIMEOUT, function()
|
||||
return state.result ~= nil
|
||||
end, nil, true)
|
||||
end
|
||||
|
||||
return state.result
|
||||
end
|
||||
|
||||
--- @param data string[]|string|nil
|
||||
function SystemObj:write(data)
|
||||
local stdin = self._state.stdin
|
||||
|
||||
if not stdin then
|
||||
error('stdin has not been opened on this object')
|
||||
end
|
||||
|
||||
if type(data) == 'table' then
|
||||
for _, v in ipairs(data) do
|
||||
stdin:write(v)
|
||||
stdin:write('\n')
|
||||
end
|
||||
elseif type(data) == 'string' then
|
||||
stdin:write(data)
|
||||
elseif data == nil then
|
||||
-- Shutdown the write side of the duplex stream and then close the pipe.
|
||||
-- Note shutdown will wait for all the pending write requests to complete
|
||||
-- TODO(lewis6991): apparently shutdown doesn't behave this way.
|
||||
-- (https://github.com/neovim/neovim/pull/17620#discussion_r820775616)
|
||||
stdin:write('', function()
|
||||
stdin:shutdown(function()
|
||||
if stdin then
|
||||
stdin:close()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- @return boolean
|
||||
function SystemObj:is_closing()
|
||||
local handle = self._state.handle
|
||||
return handle == nil or handle:is_closing() or false
|
||||
end
|
||||
|
||||
--- @param output fun(err:string?, data: string?)|false
|
||||
--- @return uv.uv_stream_t?
|
||||
--- @return fun(err:string?, data: string?)? Handler
|
||||
local function setup_output(output)
|
||||
if output == nil then
|
||||
return assert(uv.new_pipe(false)), nil
|
||||
end
|
||||
|
||||
if type(output) == 'function' then
|
||||
return assert(uv.new_pipe(false)), output
|
||||
end
|
||||
|
||||
assert(output == false)
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
--- @param input string|string[]|true|nil
|
||||
--- @return uv.uv_stream_t?
|
||||
--- @return string|string[]?
|
||||
local function setup_input(input)
|
||||
if not input then
|
||||
return
|
||||
end
|
||||
|
||||
local towrite --- @type string|string[]?
|
||||
if type(input) == 'string' or type(input) == 'table' then
|
||||
towrite = input
|
||||
end
|
||||
|
||||
return assert(uv.new_pipe(false)), towrite
|
||||
end
|
||||
|
||||
--- @return table<string,string>
|
||||
local function base_env()
|
||||
local env = vim.fn.environ() --- @type table<string,string>
|
||||
env['NVIM'] = vim.v.servername
|
||||
env['NVIM_LISTEN_ADDRESS'] = nil
|
||||
return env
|
||||
end
|
||||
|
||||
--- uv.spawn will completely overwrite the environment
|
||||
--- when we just want to modify the existing one, so
|
||||
--- make sure to prepopulate it with the current env.
|
||||
--- @param env? table<string,string|number>
|
||||
--- @param clear_env? boolean
|
||||
--- @return string[]?
|
||||
local function setup_env(env, clear_env)
|
||||
if clear_env then
|
||||
return env
|
||||
end
|
||||
|
||||
--- @type table<string,string|number>
|
||||
env = vim.tbl_extend('force', base_env(), env or {})
|
||||
|
||||
local renv = {} --- @type string[]
|
||||
for k, v in pairs(env) do
|
||||
renv[#renv + 1] = string.format('%s=%s', k, tostring(v))
|
||||
end
|
||||
|
||||
return renv
|
||||
end
|
||||
|
||||
--- @param stream uv.uv_stream_t
|
||||
--- @param text? boolean
|
||||
--- @param bucket string[]
|
||||
--- @return fun(err: string?, data: string?)
|
||||
local function default_handler(stream, text, bucket)
|
||||
return function(err, data)
|
||||
if err then
|
||||
error(err)
|
||||
end
|
||||
if data ~= nil then
|
||||
if text then
|
||||
bucket[#bucket + 1] = data:gsub('\r\n', '\n')
|
||||
else
|
||||
bucket[#bucket + 1] = data
|
||||
end
|
||||
else
|
||||
stream:read_stop()
|
||||
stream:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param cmd string
|
||||
--- @param opts uv.spawn.options
|
||||
--- @param on_exit fun(code: integer, signal: integer)
|
||||
--- @param on_error fun()
|
||||
--- @return uv.uv_process_t, integer
|
||||
local function spawn(cmd, opts, on_exit, on_error)
|
||||
local handle, pid_or_err = uv.spawn(cmd, opts, on_exit)
|
||||
if not handle then
|
||||
on_error()
|
||||
error(pid_or_err)
|
||||
end
|
||||
return handle, pid_or_err --[[@as integer]]
|
||||
end
|
||||
|
||||
--- @param timeout integer
|
||||
--- @param cb fun()
|
||||
--- @return uv.uv_timer_t
|
||||
local function timer_oneshot(timeout, cb)
|
||||
local timer = assert(uv.new_timer())
|
||||
timer:start(timeout, 0, function()
|
||||
timer:stop()
|
||||
timer:close()
|
||||
cb()
|
||||
end)
|
||||
return timer
|
||||
end
|
||||
|
||||
--- @param state vim.SystemState
|
||||
--- @param code integer
|
||||
--- @param signal integer
|
||||
--- @param on_exit fun(result: vim.SystemCompleted)?
|
||||
local function _on_exit(state, code, signal, on_exit)
|
||||
close_handles(state)
|
||||
|
||||
local check = assert(uv.new_check())
|
||||
check:start(function()
|
||||
for _, pipe in pairs({ state.stdin, state.stdout, state.stderr }) do
|
||||
if not pipe:is_closing() then
|
||||
return
|
||||
end
|
||||
end
|
||||
check:stop()
|
||||
check:close()
|
||||
|
||||
if state.done == nil then
|
||||
state.done = true
|
||||
end
|
||||
|
||||
if (code == 0 or code == 1) and state.done == 'timeout' then
|
||||
-- Unix: code == 0
|
||||
-- Windows: code == 1
|
||||
code = 124
|
||||
end
|
||||
|
||||
local stdout_data = state.stdout_data
|
||||
local stderr_data = state.stderr_data
|
||||
|
||||
state.result = {
|
||||
code = code,
|
||||
signal = signal,
|
||||
stdout = stdout_data and table.concat(stdout_data) or nil,
|
||||
stderr = stderr_data and table.concat(stderr_data) or nil,
|
||||
}
|
||||
|
||||
if on_exit then
|
||||
on_exit(state.result)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Run a system command
|
||||
---
|
||||
--- @param cmd string[]
|
||||
--- @param opts? vim.SystemOpts
|
||||
--- @param on_exit? fun(out: vim.SystemCompleted)
|
||||
--- @return vim.SystemObj
|
||||
local function system(cmd, opts, on_exit)
|
||||
local __FUNC__ = 'run_job'
|
||||
vim.validate({
|
||||
cmd = { cmd, 'table' },
|
||||
opts = { opts, 'table', true },
|
||||
on_exit = { on_exit, 'function', true },
|
||||
})
|
||||
|
||||
opts = opts or {}
|
||||
|
||||
local stdout, stdout_handler = setup_output(opts.stdout)
|
||||
local stderr, stderr_handler = setup_output(opts.stderr)
|
||||
local stdin, towrite = setup_input(opts.stdin)
|
||||
|
||||
--- @type vim.SystemState
|
||||
local state = {
|
||||
done = false,
|
||||
cmd = cmd,
|
||||
timeout = opts.timeout,
|
||||
stdin = stdin,
|
||||
stdout = stdout,
|
||||
stderr = stderr,
|
||||
}
|
||||
|
||||
--- @diagnostic disable-next-line:missing-fields
|
||||
state.handle, state.pid = spawn(cmd[1], {
|
||||
args = vim.list_slice(cmd, 2),
|
||||
stdio = { stdin, stdout, stderr },
|
||||
cwd = opts.cwd,
|
||||
--- @diagnostic disable-next-line:assign-type-mismatch
|
||||
env = setup_env(opts.env, opts.clear_env),
|
||||
detached = opts.detach,
|
||||
hide = true,
|
||||
}, function(code, signal)
|
||||
_on_exit(state, code, signal, on_exit)
|
||||
end, function()
|
||||
close_handles(state)
|
||||
end)
|
||||
|
||||
if stdout then
|
||||
state.stdout_data = {}
|
||||
stdout:read_start(stdout_handler or default_handler(stdout, opts.text, state.stdout_data))
|
||||
end
|
||||
|
||||
if stderr then
|
||||
state.stderr_data = {}
|
||||
stderr:read_start(stderr_handler or default_handler(stderr, opts.text, state.stderr_data))
|
||||
end
|
||||
|
||||
local obj = new_systemobj(state)
|
||||
|
||||
if towrite then
|
||||
obj:write(towrite)
|
||||
obj:write(nil) -- close the stream
|
||||
end
|
||||
|
||||
if opts.timeout then
|
||||
state.timer = timer_oneshot(opts.timeout, function()
|
||||
if state.handle and state.handle:is_active() then
|
||||
obj:_timeout()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return obj
|
||||
end
|
||||
|
||||
return system
|
||||
@ -0,0 +1,34 @@
|
||||
local M = {}
|
||||
|
||||
local function eq(act, exp)
|
||||
assert(act == exp, string.format('%s != %s', act, exp))
|
||||
end
|
||||
|
||||
M._tests = {}
|
||||
|
||||
M._tests.expand_format = function()
|
||||
local util = require('gitsigns.util')
|
||||
assert('hello % world % 2021' == util.expand_format('<var1> % <var2> % <var_time:%Y>', {
|
||||
var1 = 'hello',
|
||||
var2 = 'world',
|
||||
var_time = 1616838297,
|
||||
}))
|
||||
end
|
||||
|
||||
M._tests.test_args = function()
|
||||
local parse_args = require('gitsigns.cli.argparse').parse_args
|
||||
|
||||
local pos_args, named_args = parse_args('hello there key=value, key1="a b c"')
|
||||
|
||||
eq(pos_args[1], 'hello')
|
||||
eq(pos_args[2], 'there')
|
||||
eq(named_args.key, 'value,')
|
||||
eq(named_args.key1, 'a b c')
|
||||
|
||||
pos_args, named_args = parse_args('base=HEAD~1 posarg')
|
||||
|
||||
eq(named_args.base, 'HEAD~1')
|
||||
eq(pos_args[1], 'posarg')
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,369 @@
|
||||
local M = {}
|
||||
|
||||
function M.path_exists(path)
|
||||
return vim.loop.fs_stat(path) and true or false
|
||||
end
|
||||
|
||||
local jit_os --- @type string
|
||||
|
||||
if jit then
|
||||
jit_os = jit.os:lower()
|
||||
end
|
||||
|
||||
local is_unix = false
|
||||
if jit_os then
|
||||
is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd'
|
||||
else
|
||||
local binfmt = package.cpath:match('%p[\\|/]?%p(%a+)')
|
||||
is_unix = binfmt ~= 'dll'
|
||||
end
|
||||
|
||||
--- @param file string
|
||||
--- @return string
|
||||
function M.dirname(file)
|
||||
return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep))
|
||||
end
|
||||
|
||||
--- @param path string
|
||||
--- @return string[]
|
||||
function M.file_lines(path)
|
||||
local file = assert(io.open(path, 'rb'))
|
||||
local contents = file:read('*a')
|
||||
file:close()
|
||||
return vim.split(contents, '\n')
|
||||
end
|
||||
|
||||
M.path_sep = package.config:sub(1, 1)
|
||||
|
||||
--- @param ... integer
|
||||
--- @return string
|
||||
local function make_bom(...)
|
||||
local r = {}
|
||||
---@diagnostic disable-next-line:no-unknown
|
||||
for i, a in ipairs({ ... }) do
|
||||
---@diagnostic disable-next-line:no-unknown
|
||||
r[i] = string.char(a)
|
||||
end
|
||||
return table.concat(r)
|
||||
end
|
||||
|
||||
local BOM_TABLE = {
|
||||
['utf-8'] = make_bom(0xef, 0xbb, 0xbf),
|
||||
['utf-16le'] = make_bom(0xff, 0xfe),
|
||||
['utf-16'] = make_bom(0xfe, 0xff),
|
||||
['utf-16be'] = make_bom(0xfe, 0xff),
|
||||
['utf-32le'] = make_bom(0xff, 0xfe, 0x00, 0x00),
|
||||
['utf-32'] = make_bom(0xff, 0xfe, 0x00, 0x00),
|
||||
['utf-32be'] = make_bom(0x00, 0x00, 0xfe, 0xff),
|
||||
['utf-7'] = make_bom(0x2b, 0x2f, 0x76),
|
||||
['utf-1'] = make_bom(0xf7, 0x54, 0x4c),
|
||||
}
|
||||
|
||||
---@param x string
|
||||
---@param encoding string
|
||||
---@return string
|
||||
local function add_bom(x, encoding)
|
||||
local bom = BOM_TABLE[encoding]
|
||||
if bom then
|
||||
return bom .. x
|
||||
end
|
||||
return x
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @return string[]
|
||||
function M.buf_lines(bufnr)
|
||||
-- nvim_buf_get_lines strips carriage returns if fileformat==dos
|
||||
local buftext = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
|
||||
local dos = vim.bo[bufnr].fileformat == 'dos'
|
||||
|
||||
if dos then
|
||||
for i = 1, #buftext - 1 do
|
||||
buftext[i] = buftext[i] .. '\r'
|
||||
end
|
||||
end
|
||||
|
||||
if vim.bo[bufnr].endofline then
|
||||
-- Add CR to the last line
|
||||
if dos then
|
||||
buftext[#buftext] = buftext[#buftext] .. '\r'
|
||||
end
|
||||
buftext[#buftext + 1] = ''
|
||||
end
|
||||
|
||||
if vim.bo[bufnr].bomb then
|
||||
buftext[1] = add_bom(buftext[1], vim.bo[bufnr].fileencoding)
|
||||
end
|
||||
|
||||
return buftext
|
||||
end
|
||||
|
||||
--- @param buf integer
|
||||
local function delete_alt(buf)
|
||||
local alt = vim.api.nvim_buf_call(buf, function()
|
||||
return vim.fn.bufnr('#')
|
||||
end)
|
||||
if alt ~= buf and alt ~= -1 then
|
||||
pcall(vim.api.nvim_buf_delete, alt, { force = true })
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param name string
|
||||
function M.buf_rename(bufnr, name)
|
||||
vim.api.nvim_buf_set_name(bufnr, name)
|
||||
delete_alt(bufnr)
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param start_row integer
|
||||
--- @param end_row integer
|
||||
--- @param lines string[]
|
||||
function M.set_lines(bufnr, start_row, end_row, lines)
|
||||
if vim.bo[bufnr].fileformat == 'dos' then
|
||||
lines = M.strip_cr(lines)
|
||||
end
|
||||
if start_row == 0 and end_row == -1 and lines[#lines] == '' then
|
||||
lines = vim.deepcopy(lines)
|
||||
lines[#lines] = nil
|
||||
end
|
||||
vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines)
|
||||
end
|
||||
|
||||
--- @return string
|
||||
function M.tmpname()
|
||||
if is_unix then
|
||||
return os.tmpname()
|
||||
end
|
||||
return vim.fn.tempname()
|
||||
end
|
||||
|
||||
--- @param time number
|
||||
--- @param divisor integer
|
||||
--- @param time_word string
|
||||
--- @return string
|
||||
local function to_relative_string(time, divisor, time_word)
|
||||
local num = math.floor(time / divisor)
|
||||
if num > 1 then
|
||||
time_word = time_word .. 's'
|
||||
end
|
||||
|
||||
return num .. ' ' .. time_word .. ' ago'
|
||||
end
|
||||
|
||||
--- @param timestamp number
|
||||
--- @return string
|
||||
function M.get_relative_time(timestamp)
|
||||
local current_timestamp = os.time()
|
||||
local elapsed = current_timestamp - timestamp
|
||||
|
||||
if elapsed == 0 then
|
||||
return 'a while ago'
|
||||
end
|
||||
|
||||
local minute_seconds = 60
|
||||
local hour_seconds = minute_seconds * 60
|
||||
local day_seconds = hour_seconds * 24
|
||||
local month_seconds = day_seconds * 30
|
||||
local year_seconds = month_seconds * 12
|
||||
|
||||
if elapsed < minute_seconds then
|
||||
return to_relative_string(elapsed, 1, 'second')
|
||||
elseif elapsed < hour_seconds then
|
||||
return to_relative_string(elapsed, minute_seconds, 'minute')
|
||||
elseif elapsed < day_seconds then
|
||||
return to_relative_string(elapsed, hour_seconds, 'hour')
|
||||
elseif elapsed < month_seconds then
|
||||
return to_relative_string(elapsed, day_seconds, 'day')
|
||||
elseif elapsed < year_seconds then
|
||||
return to_relative_string(elapsed, month_seconds, 'month')
|
||||
else
|
||||
return to_relative_string(elapsed, year_seconds, 'year')
|
||||
end
|
||||
end
|
||||
|
||||
--- @param opts vim.api.keyset.redraw
|
||||
function M.redraw(opts)
|
||||
if vim.fn.has('nvim-0.10') == 1 then
|
||||
vim.api.nvim__redraw(opts)
|
||||
else
|
||||
vim.api.nvim__buf_redraw_range(opts.buf, opts.range[1], opts.range[2])
|
||||
end
|
||||
end
|
||||
|
||||
--- @param xs string[]
|
||||
--- @return boolean
|
||||
local function is_dos(xs)
|
||||
-- Do not check CR at EOF
|
||||
for i = 1, #xs - 1 do
|
||||
if xs[i]:sub(-1) ~= '\r' then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Strip '\r' from the EOL of each line only if all lines end with '\r'
|
||||
--- @param xs0 string[]
|
||||
--- @return string[]
|
||||
function M.strip_cr(xs0)
|
||||
if not is_dos(xs0) then
|
||||
-- don't strip, return early
|
||||
return xs0
|
||||
end
|
||||
|
||||
-- all lines end with '\r', need to strip
|
||||
local xs = vim.deepcopy(xs0)
|
||||
for i = 1, #xs do
|
||||
xs[i] = xs[i]:sub(1, -2)
|
||||
end
|
||||
return xs
|
||||
end
|
||||
|
||||
--- @param base? string
|
||||
--- @return string?
|
||||
function M.norm_base(base)
|
||||
if base == ':0' then
|
||||
return
|
||||
end
|
||||
if base and base:sub(1, 1):match('[~\\^]') then
|
||||
base = 'HEAD' .. base
|
||||
end
|
||||
return base
|
||||
end
|
||||
|
||||
function M.emptytable()
|
||||
return setmetatable({}, {
|
||||
---@param t table<any,any>
|
||||
---@param k any
|
||||
---@return any
|
||||
__index = function(t, k)
|
||||
t[k] = {}
|
||||
return t[k]
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local function expand_date(fmt, time)
|
||||
if fmt == '%R' then
|
||||
return M.get_relative_time(time)
|
||||
end
|
||||
return os.date(fmt, time)
|
||||
end
|
||||
|
||||
---@param fmt string
|
||||
---@param info table<string,any>
|
||||
---@param reltime? boolean Use relative time as the default date format
|
||||
---@return string
|
||||
function M.expand_format(fmt, info, reltime)
|
||||
local ret = {} --- @type string[]
|
||||
|
||||
for _ = 1, 20 do -- loop protection
|
||||
-- Capture <name> or <name:format>
|
||||
local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)')
|
||||
if not match then
|
||||
break
|
||||
end
|
||||
--- @cast key string
|
||||
|
||||
ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1)
|
||||
|
||||
local v = info[key]
|
||||
|
||||
if v then
|
||||
if type(v) == 'table' then
|
||||
v = table.concat(v, '\n')
|
||||
end
|
||||
if vim.endswith(key, '_time') then
|
||||
if time_fmt == '' then
|
||||
time_fmt = reltime and '%R' or '%Y-%m-%d'
|
||||
end
|
||||
v = expand_date(time_fmt, v)
|
||||
end
|
||||
match = tostring(v)
|
||||
end
|
||||
ret[#ret + 1] = match
|
||||
end
|
||||
|
||||
ret[#ret + 1] = fmt
|
||||
return table.concat(ret, '')
|
||||
end
|
||||
|
||||
--- @param buf string
|
||||
--- @return boolean
|
||||
function M.bufexists(buf)
|
||||
--- @diagnostic disable-next-line:param-type-mismatch
|
||||
return vim.fn.bufexists(buf) == 1
|
||||
end
|
||||
|
||||
--- @param x Gitsigns.BlameInfo
|
||||
--- @return Gitsigns.BlameInfoPublic
|
||||
function M.convert_blame_info(x)
|
||||
--- @type Gitsigns.BlameInfoPublic
|
||||
local ret = vim.tbl_extend('error', x, x.commit)
|
||||
ret.commit = nil
|
||||
return ret
|
||||
end
|
||||
|
||||
--- Efficiently remove items from middle of a list a list.
|
||||
---
|
||||
--- Calling table.remove() in a loop will re-index the tail of the table on
|
||||
--- every iteration, instead this function will re-index the table exactly
|
||||
--- once.
|
||||
---
|
||||
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
||||
---
|
||||
---@param t any[]
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
function M.list_remove(t, first, last)
|
||||
local n = #t
|
||||
for i = 0, n - first do
|
||||
t[first + i] = t[last + 1 + i]
|
||||
t[last + 1 + i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Efficiently insert items into the middle of a list.
|
||||
---
|
||||
--- Calling table.insert() in a loop will re-index the tail of the table on
|
||||
--- every iteration, instead this function will re-index the table exactly
|
||||
--- once.
|
||||
---
|
||||
--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524
|
||||
---
|
||||
---@param t any[]
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
---@param v any
|
||||
function M.list_insert(t, first, last, v)
|
||||
local n = #t
|
||||
|
||||
-- Shift table forward
|
||||
for i = n - first, 0, -1 do
|
||||
t[last + 1 + i] = t[first + i]
|
||||
end
|
||||
|
||||
-- Fill in new values
|
||||
for i = first, last do
|
||||
t[i] = v
|
||||
end
|
||||
end
|
||||
|
||||
--- Run a function once and ignore subsequent calls
|
||||
--- @generic F: function
|
||||
--- @param fn F
|
||||
--- @return F
|
||||
function M.once(fn)
|
||||
local called = false
|
||||
return function(...)
|
||||
if called then
|
||||
return
|
||||
end
|
||||
called = true
|
||||
return fn(...)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,150 @@
|
||||
local api = vim.api
|
||||
local uv = vim.loop
|
||||
|
||||
local Status = require('gitsigns.status')
|
||||
local async = require('gitsigns.async')
|
||||
local log = require('gitsigns.debug.log')
|
||||
local util = require('gitsigns.util')
|
||||
|
||||
local cache = require('gitsigns.cache').cache
|
||||
local config = require('gitsigns.config').config
|
||||
local debounce_trailing = require('gitsigns.debounce').debounce_trailing
|
||||
local manager = require('gitsigns.manager')
|
||||
|
||||
local dprint = log.dprint
|
||||
local dprintf = log.dprintf
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param old_relpath string
|
||||
local function handle_moved(bufnr, old_relpath)
|
||||
local bcache = cache[bufnr]
|
||||
local git_obj = bcache.git_obj
|
||||
|
||||
local new_name = git_obj:has_moved()
|
||||
if new_name then
|
||||
dprintf('File moved to %s', new_name)
|
||||
git_obj.relpath = new_name
|
||||
if not git_obj.orig_relpath then
|
||||
git_obj.orig_relpath = old_relpath
|
||||
end
|
||||
elseif git_obj.orig_relpath then
|
||||
local orig_file = git_obj.repo.toplevel .. util.path_sep .. git_obj.orig_relpath
|
||||
if not git_obj:file_info(orig_file).relpath then
|
||||
return
|
||||
end
|
||||
--- File was moved in the index, but then reset
|
||||
dprintf('Moved file reset')
|
||||
git_obj.relpath = git_obj.orig_relpath
|
||||
git_obj.orig_relpath = nil
|
||||
else
|
||||
-- File removed from index, do nothing
|
||||
return
|
||||
end
|
||||
|
||||
git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath
|
||||
bcache.file = git_obj.file
|
||||
git_obj:update()
|
||||
if not manager.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
local bufexists = util.bufexists(bcache.file)
|
||||
local old_name = api.nvim_buf_get_name(bufnr)
|
||||
|
||||
if not bufexists then
|
||||
util.buf_rename(bufnr, bcache.file)
|
||||
end
|
||||
|
||||
local msg = bufexists and 'Cannot rename' or 'Renamed'
|
||||
dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file)
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
local watcher_handler = async.create(1, function(bufnr)
|
||||
local __FUNC__ = 'watcher_handler'
|
||||
|
||||
-- Avoid cache hit for detached buffer
|
||||
-- ref: https://github.com/lewis6991/gitsigns.nvim/issues/956
|
||||
if not manager.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
local git_obj = cache[bufnr].git_obj
|
||||
|
||||
git_obj.repo:update_abbrev_head()
|
||||
|
||||
if not manager.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
Status:update(bufnr, { head = git_obj.repo.abbrev_head })
|
||||
|
||||
local was_tracked = git_obj.object_name ~= nil
|
||||
local old_relpath = git_obj.relpath
|
||||
|
||||
git_obj:update()
|
||||
if not manager.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then
|
||||
-- File was tracked but is no longer tracked. Must of been removed or
|
||||
-- moved. Check if it was moved and switch to it.
|
||||
handle_moved(bufnr, old_relpath)
|
||||
if not manager.schedule(bufnr) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
cache[bufnr]:invalidate(true)
|
||||
|
||||
require('gitsigns.manager').update(bufnr)
|
||||
end)
|
||||
|
||||
local watcher_handler_debounced = debounce_trailing(200, watcher_handler, 1)
|
||||
|
||||
--- vim.inspect but on one line
|
||||
--- @param x any
|
||||
--- @return string
|
||||
local function inspect(x)
|
||||
return vim.inspect(x, { indent = '', newline = ' ' })
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
local WATCH_IGNORE = {
|
||||
ORIG_HEAD = true,
|
||||
FETCH_HEAD = true,
|
||||
}
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param gitdir string
|
||||
--- @return uv.uv_fs_event_t
|
||||
function M.watch_gitdir(bufnr, gitdir)
|
||||
dprintf('Watching git dir')
|
||||
local w = assert(uv.new_fs_event())
|
||||
w:start(gitdir, {}, function(err, filename, events)
|
||||
local __FUNC__ = 'watcher_cb'
|
||||
if err then
|
||||
dprintf('Git dir update error: %s', err)
|
||||
return
|
||||
end
|
||||
|
||||
local info = string.format("Git dir update: '%s' %s", filename, inspect(events))
|
||||
|
||||
-- The luv docs say filename is passed as a string but it has been observed
|
||||
-- to sometimes be nil.
|
||||
-- https://github.com/lewis6991/gitsigns.nvim/issues/848
|
||||
if filename == nil or WATCH_IGNORE[filename] or vim.endswith(filename, '.lock') then
|
||||
dprintf('%s (ignoring)', info)
|
||||
return
|
||||
end
|
||||
|
||||
dprint(info)
|
||||
|
||||
watcher_handler_debounced(bufnr)
|
||||
end)
|
||||
return w
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user