From 2f542c00ca6aa90dd15e0baa8a0fce5c8381dbe6 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 23 Feb 2023 11:16:10 +0000 Subject: [PATCH] perf: reduce startup time --- .github/workflows/ci.yml | 11 - Makefile | 15 +- gen_help.lua | 1 + lua/gitsigns.lua | 510 ++++++++------------------------------ lua/gitsigns/attach.lua | 411 +++++++++++++++++++++++++++++++ lua/gitsigns/cli.lua | 34 +-- lua/gitsigns/debug.lua | 4 +- lua/gitsigns/git.lua | 55 +++-- lua/gitsigns/manager.lua | 63 ----- lua/gitsigns/status.lua | 7 +- teal/gitsigns.tl | 516 ++++++++------------------------------- teal/gitsigns/attach.tl | 411 +++++++++++++++++++++++++++++++ teal/gitsigns/cli.tl | 38 +-- teal/gitsigns/debug.tl | 4 +- teal/gitsigns/git.tl | 55 +++-- teal/gitsigns/manager.tl | 63 ----- teal/gitsigns/status.tl | 7 +- types/vim.d.tl | 4 + types/vim/api.d.tl | 11 +- 19 files changed, 1165 insertions(+), 1055 deletions(-) create mode 100644 lua/gitsigns/attach.lua create mode 100644 teal/gitsigns/attach.tl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85276448..bdf6eaeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,6 @@ on: branches: [ main ] workflow_dispatch: -env: - CC: clang - jobs: commit_lint: runs-on: ubuntu-latest @@ -30,14 +27,6 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Install LuaJIT - uses: leafo/gh-actions-lua@v9 - with: - luaVersion: "luajit-2.1.0-beta3" - - - name: Install Luarocks - uses: leafo/gh-actions-luarocks@v4 - - name: Get Neovim SHA id: get-nvim-sha run: | diff --git a/Makefile b/Makefile index 3cb7eb6e..26108416 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,6 @@ export PJ_ROOT=$(PWD) -# Suppress built in rules. This reduces clutter when running with -d -MAKEFLAGS += --no-builtin-rules - FILTER ?= .* LUA_VERSION := 5.1 @@ -13,7 +10,7 @@ NEOVIM_BRANCH ?= master DEPS_DIR := $(PWD)/deps/nvim-$(NEOVIM_BRANCH) NVIM_DIR := $(DEPS_DIR)/neovim -LUAROCKS := luarocks --lua-version=$(LUA_VERSION) +LUAROCKS := $(DEPS_DIR)/luarocks/usr/bin/luarocks LUAROCKS_TREE := $(DEPS_DIR)/luarocks/usr LUAROCKS_LPATH := $(LUAROCKS_TREE)/share/lua/$(LUA_VERSION) LUAROCKS_INIT := eval $$($(LUAROCKS) --tree $(LUAROCKS_TREE) path) && @@ -23,25 +20,27 @@ LUAROCKS_INIT := eval $$($(LUAROCKS) --tree $(LUAROCKS_TREE) path) && $(NVIM_DIR): @mkdir -p $(DEPS_DIR) git clone --depth 1 https://github.com/neovim/neovim --branch $(NEOVIM_BRANCH) $@ + @# disable LTO to reduce compile time make -C $@ \ DEPS_BUILD_DIR=$(dir $(LUAROCKS_TREE)) \ - CMAKE_BUILD_TYPE=RelWithDebInfo + CMAKE_BUILD_TYPE=RelWithDebInfo \ + CMAKE_EXTRA_FLAGS='-DCI_BUILD=OFF -DENABLE_LTO=OFF' TL := $(LUAROCKS_TREE)/bin/tl -$(TL): +$(TL): $(NVIM_DIR) @mkdir -p $$(dirname $@) $(LUAROCKS) --tree $(LUAROCKS_TREE) install tl $(TL_VERSION) INSPECT := $(LUAROCKS_LPATH)/inspect.lua -$(INSPECT): +$(INSPECT): $(NVIM_DIR) @mkdir -p $$(dirname $@) $(LUAROCKS) --tree $(LUAROCKS_TREE) install inspect LUV := $(LUAROCKS_TREE)/lib/lua/$(LUA_VERSION)/luv.so -$(LUV): +$(LUV): $(NVIM_DIR) @mkdir -p $$(dirname $@) $(LUAROCKS) --tree $(LUAROCKS_TREE) install luv diff --git a/gen_help.lua b/gen_help.lua index b8e87b98..8a509f89 100755 --- a/gen_help.lua +++ b/gen_help.lua @@ -335,6 +335,7 @@ local function get_marker_text(marker) CONFIG = gen_config_doc, FUNCTIONS = gen_functions_doc{ 'teal/gitsigns.tl', + 'teal/gitsigns/attach.tl', 'teal/gitsigns/actions.tl', }, HIGHLIGHTS = gen_highlights_doc, diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index 7da40acb..1b0812d1 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -1,17 +1,6 @@ -local async = require('gitsigns.async') local void = require('gitsigns.async').void local scheduler = require('gitsigns.async').scheduler -local Status = require("gitsigns.status") -local git = require('gitsigns.git') -local manager = require('gitsigns.manager') -local util = require('gitsigns.util') -local hl = require('gitsigns.highlight') - -local gs_cache = require('gitsigns.cache') -local cache = gs_cache.cache -local CacheEntry = gs_cache.CacheEntry - local gs_config = require('gitsigns.config') local Config = gs_config.Config local config = gs_config.config @@ -20,386 +9,146 @@ local gs_debug = require("gitsigns.debug") local dprintf = gs_debug.dprintf local dprint = gs_debug.dprint -local Debounce = require("gitsigns.debounce") -local debounce_trailing = Debounce.debounce_trailing -local throttle_by_id = Debounce.throttle_by_id - local api = vim.api -local uv = vim.loop -local current_buf = api.nvim_get_current_buf - -local M = {GitContext = {}, } - - - - - - - - +local uv = require('gitsigns.uv') +local M = {} + -- from attach.tl -local GitContext = M.GitContext +local cwd_watcher ---- 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. ---- ---- Parameters: ~ ---- {bufnr} (number): Buffer number -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 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) - - cache:destroy(bufnr) -end +local update_cwd_head = void(function() + local paths = vim.fs.find('.git', { + limit = 1, + upward = true, + type = 'directory', + }) --- @return (string, string) Tuple of buffer name and commit -local function parse_fugitive_uri(name) - if vim.fn.exists('*FugitiveReal') == 0 then - dprint("Fugitive not installed") + if #paths == 0 then return end - local path = vim.fn.FugitiveReal(name) - 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 + if cwd_watcher then + cwd_watcher:stop() + else + cwd_watcher = uv.new_fs_poll(true) end - return path, commit -end -local function parse_gitsigns_uri(name) - -- TODO(lewis6991): Support submodules - local _, _, root_path, commit, rel_path = - name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]]) - if commit == ':0' then - -- ':0' means the index so clear commit so we attach normally - commit = nil - end - if root_path then - name = root_path .. '/' .. rel_path - end - return name, commit -end + local cwd = vim.loop.cwd() + local gitdir, head -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 not vim.wo.diff then - if vim.startswith(file, 'fugitive://') then - local path, commit = parse_fugitive_uri(file) - dprintf("Fugitive buffer for file '%s' from path '%s'", path, file) - path = uv.fs_realpath(path) - if path then - return path, commit - end - end + local gs_cache = require('gitsigns.cache') - if vim.startswith(file, 'gitsigns://') then - local path, commit = parse_gitsigns_uri(file) - dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file) - path = uv.fs_realpath(path) - if path then - return path, commit - end + -- Look in the cache first + for _, bcache in pairs(gs_cache.cache) do + local repo = bcache.git_obj.repo + if repo.toplevel == cwd then + head = repo.abbrev_head + gitdir = repo.gitdir + break end end - return file -end - -local vimgrep_running = false + local git = require('gitsigns.git') -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 + if not head or not gitdir then + local info = git.get_repo_info(cwd) + gitdir = info.gitdir + head = info.abbrev_head end - return manager.on_lines(bufnr, first, last_orig, last_new) -end - -local function on_reload(_, bufnr) - local __FUNC__ = 'on_reload' - dprint('Reload') - manager.update_debounced(bufnr) -end - -local function on_detach(_, bufnr) - M.detach(bufnr, true) -end - -local function on_attach_pre(bufnr) - local gitdir, toplevel - if config._on_attach_pre then - local res = async.wrap(config._on_attach_pre, 2)(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 -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 - --- Ensure attaches cannot be interleaved. --- Since attaches are asynchronous we need to make sure an attach isn't --- performed whilst another one is in progress. -local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd) - local __FUNC__ = 'attach' - if vimgrep_running then - dprint('attaching is disabled') - return - end + scheduler() + vim.g.gitsigns_head = head - if cache[cbuf] then - dprint('Already attached') + if not gitdir then return end - if aucmd then - dprintf('Attaching (trigger=%s)', aucmd) - else - dprint('Attaching') - end + local towatch = gitdir .. '/HEAD' - if not api.nvim_buf_is_loaded(cbuf) then - dprint('Non-loaded buffer') + if cwd_watcher:getpath() == towatch then + -- Already watching return end - local encoding = vim.bo[cbuf].fileencoding - if encoding == '' then - encoding = 'utf-8' - end - local file - local commit - local gitdir_oap - local toplevel_oap - - if ctx then - gitdir_oap = ctx.gitdir - toplevel_oap = ctx.toplevel - file = ctx.toplevel .. util.path_sep .. ctx.file - commit = ctx.commit - else - if api.nvim_buf_line_count(cbuf) > config.max_file_length then - dprint('Exceeds max_file_length') - return - end - - if vim.bo[cbuf].buftype ~= '' then - dprint('Non-normal buffer') + -- Watch .git/HEAD to detect branch changes + cwd_watcher:start( + towatch, + config.watch_gitdir.interval, + void(function(err) + local __FUNC__ = 'cwd_watcher_cb' + if err then + dprintf('Git dir update error: %s', err) return end + dprint('Git cwd dir update') - file, commit = get_buf_path(cbuf) - local file_dir = util.dirname(file) - - if not file_dir or not util.path_exists(file_dir) then - dprint('Not a path') - return - end - - gitdir_oap, toplevel_oap = on_attach_pre(cbuf) - end - - local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) - - if not git_obj and not ctx then - git_obj = try_worktrees(cbuf, file, encoding) + local new_head = git.get_repo_info(cwd).abbrev_head scheduler() - end - - if not git_obj then - dprint('Empty git obj') - return - end - local repo = git_obj.repo - - scheduler() - Status:update(cbuf, { - head = repo.abbrev_head, - root = repo.toplevel, - gitdir = repo.gitdir, - }) - - if vim.startswith(file, repo.gitdir .. util.path_sep) then - dprint('In non-standard git dir') - return - end - - if not 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. - scheduler() + vim.g.gitsigns_head = new_head + end)) - if config.on_attach and config.on_attach(cbuf) == false then - dprint('User on_attach() returned false') - return - end - - cache[cbuf] = CacheEntry.new({ - base = ctx and ctx.base or config.base, - file = file, - commit = commit, - gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), - git_obj = git_obj, - }) - - 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, - }) - - -- Initial update - manager.update(cbuf, cache[cbuf]) - - if config.keymaps and not vim.tbl_isempty(config.keymaps) then - require('gitsigns.mappings')(config.keymaps, cbuf) - end -end) - ---- Attach Gitsigns to the buffer. ---- ---- Attributes: ~ ---- {async} ---- ---- Parameters: ~ ---- {bufnr} (number): Buffer number ---- {ctx} (table|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|nil) ---- The git revision that the file should be compared to. -M.attach = void(function(bufnr, ctx, _trigger) - attach_throttled(bufnr or current_buf(), ctx, _trigger) end) local function setup_cli() - local funcs = M api.nvim_create_user_command('Gitsigns', function(params) - require('gitsigns.cli').run(funcs, params) + require('gitsigns.cli').run(params) end, { force = true, nargs = '*', range = true, complete = function(arglead, line) - return require('gitsigns.cli').complete(funcs, arglead, line) + return require('gitsigns.cli').complete(arglead, line) end, }) end -local function wrap_func(fn, ...) - local args = { ... } - local nargs = select('#', ...) - return function() - fn(unpack(args, 1, nargs)) +local function setup_debug() + gs_debug.debug_mode = config.debug_mode + gs_debug.verbose = config._verbose + + if config.debug_mode then + for nm, f in pairs(gs_debug.add_debug_functions()) do + (M)[nm] = f + end end end -local function autocmd(event, opts) - local opts0 = {} - if type(opts) == "function" then - opts0.callback = wrap_func(opts) - else - opts0 = opts +local function setup_attach() + scheduler() + + -- Attach to all open buffers + for _, buf in ipairs(api.nvim_list_bufs()) do + if api.nvim_buf_is_loaded(buf) and + api.nvim_buf_get_name(buf) ~= '' then + M.attach(buf, nil, 'setup') + scheduler() + end end - opts0.group = 'gitsigns' - api.nvim_create_autocmd(event, opts0) + + api.nvim_create_autocmd({ 'BufRead', 'BufNewFile', 'BufWritePost' }, { + group = 'gitsigns', + callback = function(data) + M.attach(nil, nil, data.event) + end, + }) end -local function on_or_after_vimenter(fn) - if vim.v.vim_did_enter == 1 then - fn() - else - api.nvim_create_autocmd('VimEnter', { - callback = wrap_func(fn), - once = true, - }) - end +local function setup_cwd_head() + scheduler() + update_cwd_head() + -- Need to debounce in case some plugin changes the cwd too often + -- (like vim-grepper) + api.nvim_create_autocmd('DirChanged', { + group = 'gitsigns', + callback = function() + local debounce = require("gitsigns.debounce").debounce_trailing + debounce(100, update_cwd_head) + end, + }) end --- Setup and start Gitsigns. @@ -423,81 +172,26 @@ M.setup = void(function(cfg) return end - gs_debug.debug_mode = config.debug_mode - gs_debug.verbose = config._verbose - - if config.debug_mode then - for nm, f in pairs(gs_debug.add_debug_functions(cache)) do - (M)[nm] = f - end - end - - manager.setup() - - Status.formatter = config.status_formatter - - -- Make sure highlights are setup on or after VimEnter so the colorscheme is - -- loaded. Do not set them up with vim.schedule as this removes the intro - -- message. - on_or_after_vimenter(hl.setup_highlights) - - setup_cli() - - git.enable_yadm = config.yadm.enable - git.set_version(config._git_version) - scheduler() - - -- Attach to all open buffers - for _, buf in ipairs(api.nvim_list_bufs()) do - if api.nvim_buf_is_loaded(buf) and - api.nvim_buf_get_name(buf) ~= '' then - M.attach(buf, nil, 'setup') - scheduler() - end - end - api.nvim_create_augroup('gitsigns', {}) - autocmd('VimLeavePre', M.detach_all) - autocmd('ColorScheme', hl.setup_highlights) - autocmd('BufRead', wrap_func(M.attach, nil, nil, 'BufRead')) - autocmd('BufNewFile', wrap_func(M.attach, nil, nil, 'BufNewFile')) - autocmd('BufWritePost', wrap_func(M.attach, nil, nil, 'BufWritePost')) - - autocmd('OptionSet', { - pattern = 'fileformat', - callback = function() - require('gitsigns.actions').refresh() - end, }) - - - -- vimpgrep creates and deletes lots of buffers so attaching to each one will - -- waste lots of resource and even slow down vimgrep. - autocmd('QuickFixCmdPre', { - pattern = '*vimgrep*', - callback = function() - vimgrep_running = true - end, - }) - - autocmd('QuickFixCmdPost', { - pattern = '*vimgrep*', - callback = function() - vimgrep_running = false - end, - }) - - require('gitsigns.current_line_blame').setup() - - scheduler() - manager.update_cwd_head() - -- Need to debounce in case some plugin changes the cwd too often - -- (like vim-grepper) - autocmd('DirChanged', debounce_trailing(100, manager.update_cwd_head)) + setup_debug() + setup_cli() + setup_attach() + setup_cwd_head() end) +local exported = { + 'attach', + 'actions', +} + return setmetatable(M, { __index = function(_, f) - return (require('gitsigns.actions'))[f] + for _, mod in ipairs(exported) do + local m = (require)('gitsigns.' .. mod) + if m[f] then + return m[f] + end + end end, }) diff --git a/lua/gitsigns/attach.lua b/lua/gitsigns/attach.lua new file mode 100644 index 00000000..efe2f6c0 --- /dev/null +++ b/lua/gitsigns/attach.lua @@ -0,0 +1,411 @@ +local async = require('gitsigns.async') +local git = require('gitsigns.git') + +local gs_debug = require("gitsigns.debug") +local dprintf = gs_debug.dprintf +local dprint = gs_debug.dprint + +local manager = require('gitsigns.manager') +local hl = require('gitsigns.highlight') + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry +local Status = require("gitsigns.status") + +local gs_config = require('gitsigns.config') +local config = gs_config.config + +local void = require('gitsigns.async').void +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 = {} + + + + + +local vimgrep_running = false + +-- @return (string, string) Tuple of buffer name and commit +local function parse_fugitive_uri(name) + if vim.fn.exists('*FugitiveReal') == 0 then + dprint("Fugitive not installed") + return + end + + local path = vim.fn.FugitiveReal(name) + 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 + +local function parse_gitsigns_uri(name) + -- TODO(lewis6991): Support submodules + local _, _, root_path, commit, rel_path = + name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]]) + if commit == ':0' then + -- ':0' means the index so clear commit so we attach normally + commit = nil + end + if root_path then + name = root_path .. '/' .. rel_path + end + return name, commit +end + +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 not vim.wo.diff then + if vim.startswith(file, 'fugitive://') then + local path, commit = parse_fugitive_uri(file) + dprintf("Fugitive buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + end + + if vim.startswith(file, 'gitsigns://') then + local path, commit = parse_gitsigns_uri(file) + dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + 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 + +local function on_reload(_, bufnr) + local __FUNC__ = 'on_reload' + dprint('Reload') + manager.update_debounced(bufnr) +end + +local function on_detach(_, bufnr) + M.detach(bufnr, true) +end + +local function on_attach_pre(bufnr) + local gitdir, toplevel + if config._on_attach_pre then + local res = async.wrap(config._on_attach_pre, 2)(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 + +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 done_setup = false + +local function setup() + if done_setup then + return + end + + done_setup = true + + manager.setup() + + hl.setup_highlights() + api.nvim_create_autocmd('ColorScheme', { + group = 'gitsigns', + callback = hl.setup_highlights, + }) + + api.nvim_create_autocmd('OptionSet', { + group = 'gitsigns', + pattern = 'fileformat', + callback = function() + require('gitsigns.actions').refresh() + end, }) + + + -- vimpgrep creates and deletes lots of buffers so attaching to each one will + -- waste lots of resource and even slow down vimgrep. + api.nvim_create_autocmd('QuickFixCmdPre', { + group = 'gitsigns', + pattern = '*vimgrep*', + callback = function() + vimgrep_running = true + end, + }) + + api.nvim_create_autocmd('QuickFixCmdPost', { + group = 'gitsigns', + pattern = '*vimgrep*', + callback = function() + vimgrep_running = false + end, + }) + + require('gitsigns.current_line_blame').setup() + + api.nvim_create_autocmd('VimLeavePre', { + group = 'gitsigns', + callback = M.detach_all, + }) + +end + +-- Ensure attaches cannot be interleaved. +-- Since attaches are asynchronous we need to make sure an attach isn't +-- performed whilst another one is in progress. +local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd) + local __FUNC__ = 'attach' + + setup() + + if vimgrep_running then + dprint('attaching is disabled') + return + end + + 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 + + local encoding = vim.bo[cbuf].fileencoding + if encoding == '' then + encoding = 'utf-8' + end + local file + local commit + local gitdir_oap + local toplevel_oap + + if ctx then + gitdir_oap = ctx.gitdir + toplevel_oap = ctx.toplevel + file = ctx.toplevel .. util.path_sep .. ctx.file + commit = ctx.commit + else + if api.nvim_buf_line_count(cbuf) > config.max_file_length then + dprint('Exceeds max_file_length') + return + end + + if vim.bo[cbuf].buftype ~= '' then + dprint('Non-normal buffer') + return + end + + file, commit = get_buf_path(cbuf) + local file_dir = util.dirname(file) + + if not file_dir or not util.path_exists(file_dir) then + dprint('Not a path') + return + end + + gitdir_oap, toplevel_oap = on_attach_pre(cbuf) + end + + local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) + + if not git_obj and not ctx then + git_obj = try_worktrees(cbuf, file, encoding) + async.scheduler() + end + + if not git_obj then + dprint('Empty git obj') + return + end + local repo = git_obj.repo + + async.scheduler() + Status:update(cbuf, { + head = repo.abbrev_head, + root = repo.toplevel, + gitdir = repo.gitdir, + }) + + if vim.startswith(file, repo.gitdir .. util.path_sep) then + dprint('In non-standard git dir') + return + end + + if not 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 config.on_attach and config.on_attach(cbuf) == false then + dprint('User on_attach() returned false') + return + end + + cache[cbuf] = CacheEntry.new({ + base = ctx and ctx.base or config.base, + file = file, + commit = commit, + gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), + git_obj = git_obj, + }) + + 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, + }) + + -- Initial update + manager.update(cbuf, cache[cbuf]) + + if config.keymaps and not vim.tbl_isempty(config.keymaps) then + require('gitsigns.mappings')(config.keymaps, cbuf) + end +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. +--- +--- Parameters: ~ +--- {bufnr} (number): Buffer number +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) + + cache:destroy(bufnr) +end + + +--- Attach Gitsigns to the buffer. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters: ~ +--- {bufnr} (number): Buffer number +--- {ctx} (table|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|nil) +--- The git revision that the file should be compared to. +M.attach = void(function(bufnr, ctx, _trigger) + attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger) +end) + +return M diff --git a/lua/gitsigns/cli.lua b/lua/gitsigns/cli.lua index 6e1f1158..4bc1007a 100644 --- a/lua/gitsigns/cli.lua +++ b/lua/gitsigns/cli.lua @@ -7,6 +7,9 @@ local message = require('gitsigns.message') local parse_args = require('gitsigns.cli.argparse').parse_args +local actions = require('gitsigns.actions') +local attach = require('gitsigns.attach') + -- try to parse each argument as a lua boolean, nil or number, if fails then -- keep argument as a string: -- @@ -29,14 +32,13 @@ local M = {} -function M.complete(funcs, arglead, line) +function M.complete(arglead, line) local words = vim.split(line, '%s+') local n = #words - local actions = require('gitsigns.actions') local matches = {} if n == 2 then - for _, m in ipairs({ actions, funcs }) do + for _, m in ipairs({ actions, attach }) do for func, _ in pairs(m) do if not func:match('^[a-z]') then -- exclude @@ -55,22 +57,19 @@ function M.complete(funcs, arglead, line) return matches end -M.run = void(function(funcs, params) +M.run = void(function(params) local pos_args_raw, named_args_raw = parse_args(params.args) local func = pos_args_raw[1] if not func then - func = async.wrap(vim.ui.select, 3)(M.complete(funcs, '', 'Gitsigns '), {}) + func = async.wrap(vim.ui.select, 3)(M.complete('', 'Gitsigns '), {}) 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) - local actions = require('gitsigns.actions') - local actions0 = actions - dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, { newline = ' ', indent = '' })) local cmd_func = actions._get_cmd_func(func) @@ -81,15 +80,16 @@ M.run = void(function(funcs, params) return end - if type(actions0[func]) == 'function' then - actions0[func](unpack(pos_args), named_args) - return - end - - if type(funcs[func]) == 'function' then - -- Note functions here do not have named arguments - funcs[func](unpack(pos_args)) - return + for m, has_named in pairs({ + [actions] = true, + [attach] = false, + }) 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) diff --git a/lua/gitsigns/debug.lua b/lua/gitsigns/debug.lua index 6696b98a..56f5b437 100644 --- a/lua/gitsigns/debug.lua +++ b/lua/gitsigns/debug.lua @@ -123,9 +123,11 @@ local function process(raw_item, path) return raw_item end -function M.add_debug_functions(cache) +function M.add_debug_functions() local R = {} R.dump_cache = function() + -- TODO(lewis6991): hack: use package.loaded to avoid circular deps + local cache = (package.loaded['gitsigns.cache']).cache local text = vim.inspect(cache, { process = process }) vim.api.nvim_echo({ { text } }, false, {}) return cache diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index 422939e7..59e60f37 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -5,6 +5,9 @@ local gsd = require("gitsigns.debug") local util = require('gitsigns.util') local subprocess = require('gitsigns.subprocess') +local gs_config = require('gitsigns.config') +local config = gs_config.config + local gs_hunks = require("gitsigns.hunks") local Hunk = gs_hunks.Hunk @@ -78,10 +81,6 @@ local M = {BlameInfo = {}, Version = {}, RepoInfo = {}, Repo = {}, FileProps = { - - - - @@ -167,8 +166,35 @@ local function check_version(version) return true end +--- @async +local function set_version(version) + if version ~= 'auto' then + M.version = parse_version(version) + return + end + + local _, _, stdout, stderr = async.wait(2, subprocess.run_job, { + command = 'git', args = { '--version' }, + }) + + local line = vim.split(stdout or '', '\n', true)[1] + if not line then + err("Unable to detect git version as 'git --version' failed to return anything") + eprint(stderr) + return + end + assert(type(line) == 'string', 'Unexpected output: ' .. line) + assert(startswith(line, 'git version'), 'Unexpected output: ' .. line) + local parts = vim.split(line, '%s+') + M.version = parse_version(parts[3]) +end + + --- @async local git_command = async.create(function(args, spec) + if not M.version then + set_version(config._git_version) + end spec = spec or {} spec.command = spec.command or 'git' spec.args = spec.command == 'git' and { @@ -310,25 +336,6 @@ function M.get_repo_info(path, cmd, gitdir, toplevel) return ret end ---- @async -function M.set_version(version) - if version ~= 'auto' then - M.version = parse_version(version) - return - end - local results, stderr = git_command({ '--version' }) - local line = results[1] - if not line then - err("Unable to detect git version as 'git --version' failed to return anything") - eprint(stderr) - return - end - assert(type(line) == 'string', 'Unexpected output: ' .. line) - assert(startswith(line, 'git version'), 'Unexpected output: ' .. line) - local parts = vim.split(line, '%s+') - M.version = parse_version(parts[3]) -end - -------------------------------------------------------------------------------- -- Git repo object methods -------------------------------------------------------------------------------- @@ -444,7 +451,7 @@ function Repo.new(dir, gitdir, toplevel) end -- Try yadm - if M.enable_yadm and not self.gitdir then + if config.yadm.enable and not self.gitdir then if vim.startswith(dir, os.getenv('HOME')) and #git_command({ 'ls-files', dir }, { command = 'yadm' }) ~= 0 then M.get_repo_info(dir, 'yadm', gitdir, toplevel) diff --git a/lua/gitsigns/manager.lua b/lua/gitsigns/manager.lua index 891e4a78..ae023baa 100644 --- a/lua/gitsigns/manager.lua +++ b/lua/gitsigns/manager.lua @@ -20,7 +20,6 @@ local eprint = gs_debug.eprint local subprocess = require('gitsigns.subprocess') local util = require('gitsigns.util') local run_diff = require('gitsigns.diff') -local git = require('gitsigns.git') local uv = require('gitsigns.uv') local gs_hunks = require("gitsigns.hunks") @@ -43,7 +42,6 @@ local M = {} - local scheduler_if_buf_valid = awrap(function(buf, cb) vim.schedule(function() if vim.api.nvim_buf_is_valid(buf) then @@ -444,67 +442,6 @@ function M.watch_gitdir(bufnr, gitdir) return w end -local cwd_watcher - -M.update_cwd_head = void(function() - if cwd_watcher then - cwd_watcher:stop() - else - cwd_watcher = uv.new_fs_poll(true) - end - - local cwd = vim.loop.cwd() - local gitdir, head - - -- Look in the cache first - for _, bcache in pairs(cache) do - local repo = bcache.git_obj.repo - if repo.toplevel == cwd then - head = repo.abbrev_head - gitdir = repo.gitdir - break - end - end - - if not head or not gitdir then - local info = git.get_repo_info(cwd) - gitdir = info.gitdir - head = info.abbrev_head - end - - scheduler() - vim.g.gitsigns_head = head - - if not gitdir then - return - end - - local towatch = gitdir .. '/HEAD' - - if cwd_watcher:getpath() == towatch then - -- Already watching - return - end - - -- Watch .git/HEAD to detect branch changes - cwd_watcher:start( - towatch, - config.watch_gitdir.interval, - void(function(err) - local __FUNC__ = 'cwd_watcher_cb' - if err then - dprintf('Git dir update error: %s', err) - return - end - dprint('Git cwd dir update') - - local new_head = git.get_repo_info(cwd).abbrev_head - scheduler() - vim.g.gitsigns_head = new_head - end)) - -end) - function M.reset_signs() -- Remove all signs signs_normal:reset() diff --git a/lua/gitsigns/status.lua b/lua/gitsigns/status.lua index f4f43de9..d4e92fb8 100644 --- a/lua/gitsigns/status.lua +++ b/lua/gitsigns/status.lua @@ -1,5 +1,6 @@ local api = vim.api + local StatusObj = {} @@ -11,7 +12,6 @@ local StatusObj = {} local Status = { StatusObj = StatusObj, - formatter = nil, } function Status:update(bufnr, status) @@ -24,7 +24,10 @@ function Status:update(bufnr, status) end vim.b[bufnr].gitsigns_head = status.head or '' vim.b[bufnr].gitsigns_status_dict = status - vim.b[bufnr].gitsigns_status = self.formatter(status) + + local config = require('gitsigns.config').config + + vim.b[bufnr].gitsigns_status = config.status_formatter(status) end function Status:clear(bufnr) diff --git a/teal/gitsigns.tl b/teal/gitsigns.tl index 34dc067e..50dde5bc 100644 --- a/teal/gitsigns.tl +++ b/teal/gitsigns.tl @@ -1,17 +1,6 @@ -local async = require('gitsigns.async') local void = require('gitsigns.async').void local scheduler = require('gitsigns.async').scheduler -local Status = require("gitsigns.status") -local git = require('gitsigns.git') -local manager = require('gitsigns.manager') -local util = require('gitsigns.util') -local hl = require('gitsigns.highlight') - -local gs_cache = require('gitsigns.cache') -local cache = gs_cache.cache -local CacheEntry = gs_cache.CacheEntry - local gs_config = require('gitsigns.config') local Config = gs_config.Config local config = gs_config.config @@ -20,386 +9,146 @@ local gs_debug = require("gitsigns.debug") local dprintf = gs_debug.dprintf local dprint = gs_debug.dprint -local Debounce = require("gitsigns.debounce") -local debounce_trailing = Debounce.debounce_trailing -local throttle_by_id = Debounce.throttle_by_id - local api = vim.api -local uv = vim.loop -local current_buf = api.nvim_get_current_buf +local uv = require('gitsigns.uv') local record M - setup : function(cfg: Config) - detach : function(bufnr: integer, _keep_signs: boolean) - detach_all : function() - attach : function(cbuf: integer, ctx: GitContext, trigger: string) - - record GitContext - toplevel: string - gitdir: string - file: string - commit: string - base: string - end -end - -local GitContext = M.GitContext - ---- Detach Gitsigns from all buffers it is attached to. -function M.detach_all() - for k, _ in pairs(cache as {integer:CacheEntry}) do - M.detach(k) - end -end - ---- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not ---- provided then the current buffer is used. ---- ---- Parameters: ~ ---- {bufnr} (number): Buffer number -function M.detach(bufnr: integer, _keep_signs: boolean) - -- 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 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) - - cache:destroy(bufnr) -end - --- @return (string, string) Tuple of buffer name and commit -local function parse_fugitive_uri(name: string): string, string - if vim.fn.exists('*FugitiveReal') == 0 then - dprint("Fugitive not installed") - return - end - - local path = vim.fn.FugitiveReal(name) - 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 - -local function parse_gitsigns_uri(name: string): string, string - -- TODO(lewis6991): Support submodules - local _, _, root_path, commit, rel_path = - name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]]) - if commit == ':0' then - -- ':0' means the index so clear commit so we attach normally - commit = nil - end - if root_path then - name = root_path .. '/' .. rel_path - end - return name, commit -end - -local function get_buf_path(bufnr: integer): string, string - local file = - uv.fs_realpath(api.nvim_buf_get_name(bufnr)) - or - api.nvim_buf_call(bufnr, function(): string - return vim.fn.expand('%:p') - end) - - if not vim.wo.diff then - if vim.startswith(file, 'fugitive://') then - local path, commit = parse_fugitive_uri(file) - dprintf("Fugitive buffer for file '%s' from path '%s'", path, file) - path = uv.fs_realpath(path) - if path then - return path, commit - end - end + setup: function(cfg: Config) - if vim.startswith(file, 'gitsigns://') then - local path, commit = parse_gitsigns_uri(file) - dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file) - path = uv.fs_realpath(path) - if path then - return path, commit - end - end - end - - return file + -- from attach.tl + attach: function(cbuf: integer, ctx: table, trigger: string) end -local vimgrep_running = false +local cwd_watcher: vim.loop.FSPollObj -local function on_lines(_, bufnr: integer, _, first: integer, last_orig: integer, last_new: integer, byte_count: integer): boolean - 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 - -local function on_reload(_, bufnr: integer) - local __FUNC__ = 'on_reload' - dprint('Reload') - manager.update_debounced(bufnr) -end - -local function on_detach(_, bufnr: integer) - M.detach(bufnr, true) -end - -local function on_attach_pre(bufnr: integer): string, string - local gitdir, toplevel: string, string - if config._on_attach_pre then - local res: any = async.wrap(config._on_attach_pre, 2)(bufnr) - dprintf('ran on_attach_pre with result %s', vim.inspect(res)) - if res is table then - if type(res.gitdir) == 'string' then - gitdir = res.gitdir as string - end - if type(res.toplevel) == 'string' then - toplevel = res.toplevel as string - end - end - end - return gitdir, toplevel -end - -local function try_worktrees(_bufnr: integer, file: string, encoding: string): git.Obj - 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 - --- Ensure attaches cannot be interleaved. --- Since attaches are asynchronous we need to make sure an attach isn't --- performed whilst another one is in progress. -local attach_throttled = throttle_by_id(function(cbuf: integer, ctx: GitContext, aucmd: string) - local __FUNC__ = 'attach' - if vimgrep_running then - dprint('attaching is disabled') - return - end +local update_cwd_head = void(function() + local paths = vim.fs.find('.git', { + limit = 1, + upward = true, + type = 'directory' + }) - if cache[cbuf] then - dprint('Already attached') + if #paths == 0 then return end - if aucmd then - dprintf('Attaching (trigger=%s)', aucmd) + if cwd_watcher then + cwd_watcher:stop() else - dprint('Attaching') + cwd_watcher = uv.new_fs_poll(true) end - if not api.nvim_buf_is_loaded(cbuf) then - dprint('Non-loaded buffer') - return - end + local cwd = vim.loop.cwd() + local gitdir, head: string, string - local encoding = vim.bo[cbuf].fileencoding - if encoding == '' then - encoding = 'utf-8' - end - local file: string - local commit: string - local gitdir_oap: string - local toplevel_oap: string - - if ctx then - gitdir_oap = ctx.gitdir - toplevel_oap = ctx.toplevel - file = ctx.toplevel .. util.path_sep .. ctx.file - commit = ctx.commit - else - if api.nvim_buf_line_count(cbuf) > config.max_file_length then - dprint('Exceeds max_file_length') - return - end + local gs_cache = require('gitsigns.cache') - if vim.bo[cbuf].buftype ~= '' then - dprint('Non-normal buffer') - return + -- Look in the cache first + for _, bcache in pairs(gs_cache.cache as {number:gs_cache.CacheEntry}) do + local repo = bcache.git_obj.repo + if repo.toplevel == cwd then + head = repo.abbrev_head + gitdir = repo.gitdir + break end - - file, commit = get_buf_path(cbuf) - local file_dir = util.dirname(file) - - if not file_dir or not util.path_exists(file_dir) then - dprint('Not a path') - return - end - - gitdir_oap, toplevel_oap = on_attach_pre(cbuf) end - local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) + local git = require('gitsigns.git') - if not git_obj and not ctx then - git_obj = try_worktrees(cbuf, file, encoding) - scheduler() + if not head or not gitdir then + local info = git.get_repo_info(cwd) + gitdir = info.gitdir + head = info.abbrev_head end - if not git_obj then - dprint('Empty git obj') - return - end - local repo = git_obj.repo - scheduler() - Status:update(cbuf, { - head = repo.abbrev_head, - root = repo.toplevel, - gitdir = repo.gitdir, - }) + vim.g.gitsigns_head = head - if vim.startswith(file, repo.gitdir..util.path_sep) then - dprint('In non-standard git dir') + if not gitdir then return end - if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then - dprint('Not a file') - return - end + local towatch = gitdir..'/HEAD' - if not git_obj.relpath then - dprint('Cannot resolve file in repo') + if cwd_watcher:getpath() == towatch then + -- Already watching 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. - scheduler() - - if config.on_attach and config.on_attach(cbuf) == false then - dprint('User on_attach() returned false') - return - end - - cache[cbuf] = CacheEntry.new { - base = ctx and ctx.base or config.base, - file = file, - commit = commit, - gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), - git_obj = git_obj - } - - 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 - }) - - -- Initial update - manager.update(cbuf, cache[cbuf]) - - if config.keymaps and not vim.tbl_isempty(config.keymaps) then - require('gitsigns.mappings')(config.keymaps as {string:any}, cbuf) - end -end) + -- Watch .git/HEAD to detect branch changes + cwd_watcher:start( + towatch, + config.watch_gitdir.interval, + void(function(err: string) + local __FUNC__ = 'cwd_watcher_cb' + if err then + dprintf('Git dir update error: %s', err) + return + end + dprint('Git cwd dir update') ---- Attach Gitsigns to the buffer. ---- ---- Attributes: ~ ---- {async} ---- ---- Parameters: ~ ---- {bufnr} (number): Buffer number ---- {ctx} (table|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|nil) ---- The git revision that the file should be compared to. -M.attach = void(function(bufnr: integer, ctx: GitContext, _trigger: string) - attach_throttled(bufnr or current_buf(), ctx, _trigger) + local new_head = git.get_repo_info(cwd).abbrev_head + scheduler() + vim.g.gitsigns_head = new_head + end) + ) end) local function setup_cli() - local funcs = M as {string:function} api.nvim_create_user_command('Gitsigns', function(params: api.UserCmdParams) - require'gitsigns.cli'.run(funcs, params) + require'gitsigns.cli'.run(params) end, { force = true, nargs = '*', range = true, complete = function(arglead: string, line: string): {string} - return require'gitsigns.cli'.complete(funcs, arglead, line) + return require'gitsigns.cli'.complete(arglead, line) end}) end -local function wrap_func(fn: function, ...: any): function() - local args = {...} - local nargs = select('#', ...) - return function() - fn(unpack(args, 1, nargs)) +local function setup_debug() + gs_debug.debug_mode = config.debug_mode + gs_debug.verbose = config._verbose + + if config.debug_mode then + for nm, f in pairs(gs_debug.add_debug_functions()) do + (M as {string:function})[nm] = f + end end end -local function autocmd(event: string, opts: function|vim.api.AutoCmdOpts) - local opts0: vim.api.AutoCmdOpts = {} - if opts is function then - opts0.callback = wrap_func(opts) - else - opts0 = opts +local function setup_attach() + scheduler() + + -- Attach to all open buffers + for _, buf in ipairs(api.nvim_list_bufs()) do + if api.nvim_buf_is_loaded(buf) + and api.nvim_buf_get_name(buf) ~= '' then + M.attach(buf, nil, 'setup') + scheduler() + end end - opts0.group = 'gitsigns' - api.nvim_create_autocmd(event, opts0) + + api.nvim_create_autocmd({'BufRead', 'BufNewFile', 'BufWritePost'}, { + group = 'gitsigns', + callback = function(data: vim.api.AutoCmdOpts.CallbackData) + M.attach(nil, nil, data.event) + end + }) end -local function on_or_after_vimenter(fn: function) - if vim.v.vim_did_enter == 1 then - fn() - else - api.nvim_create_autocmd('VimEnter', { - callback = wrap_func(fn), - once = true - }) - end +local function setup_cwd_head() + scheduler() + update_cwd_head() + -- Need to debounce in case some plugin changes the cwd too often + -- (like vim-grepper) + api.nvim_create_autocmd('DirChanged', { + group = 'gitsigns', + callback = function() + local debounce = require("gitsigns.debounce").debounce_trailing + debounce(100, update_cwd_head) + end + }) end --- Setup and start Gitsigns. @@ -423,81 +172,26 @@ M.setup = void(function(cfg: Config) return end - gs_debug.debug_mode = config.debug_mode - gs_debug.verbose = config._verbose - - if config.debug_mode then - for nm, f in pairs(gs_debug.add_debug_functions(cache)) do - (M as {string:function})[nm] = f - end - end - - manager.setup() - - Status.formatter = config.status_formatter as function(Status.StatusObj): string - - -- Make sure highlights are setup on or after VimEnter so the colorscheme is - -- loaded. Do not set them up with vim.schedule as this removes the intro - -- message. - on_or_after_vimenter(hl.setup_highlights) - - setup_cli() - - git.enable_yadm = config.yadm.enable - git.set_version(config._git_version) - scheduler() - - -- Attach to all open buffers - for _, buf in ipairs(api.nvim_list_bufs()) do - if api.nvim_buf_is_loaded(buf) - and api.nvim_buf_get_name(buf) ~= '' then - M.attach(buf, nil, 'setup') - scheduler() - end - end - api.nvim_create_augroup('gitsigns', {}) - autocmd('VimLeavePre' , M.detach_all) - autocmd('ColorScheme' , hl.setup_highlights) - autocmd('BufRead' , wrap_func(M.attach, nil, nil, 'BufRead')) - autocmd('BufNewFile' , wrap_func(M.attach, nil, nil, 'BufNewFile')) - autocmd('BufWritePost', wrap_func(M.attach, nil, nil, 'BufWritePost')) - - autocmd('OptionSet', { - pattern = 'fileformat', - callback = function() - require('gitsigns.actions').refresh() - end} - ) - - -- vimpgrep creates and deletes lots of buffers so attaching to each one will - -- waste lots of resource and even slow down vimgrep. - autocmd('QuickFixCmdPre', { - pattern ='*vimgrep*', - callback = function() - vimgrep_running = true - end - }) - - autocmd('QuickFixCmdPost', { - pattern ='*vimgrep*', - callback = function() - vimgrep_running = false - end - }) - - require('gitsigns.current_line_blame').setup() - - scheduler() - manager.update_cwd_head() - -- Need to debounce in case some plugin changes the cwd too often - -- (like vim-grepper) - autocmd('DirChanged', debounce_trailing(100, manager.update_cwd_head)) + setup_debug() + setup_cli() + setup_attach() + setup_cwd_head() end) +local exported = { + 'attach', + 'actions' +} + return setmetatable(M, { __index = function(_, f: string): any - return (require('gitsigns.actions') as {string:function})[f] + for _, mod in ipairs(exported) do + local m = (require as function)('gitsigns.'..mod) as table + if m[f] then + return m[f] + end + end end }) diff --git a/teal/gitsigns/attach.tl b/teal/gitsigns/attach.tl new file mode 100644 index 00000000..29db7527 --- /dev/null +++ b/teal/gitsigns/attach.tl @@ -0,0 +1,411 @@ +local async = require('gitsigns.async') +local git = require('gitsigns.git') + +local gs_debug = require("gitsigns.debug") +local dprintf = gs_debug.dprintf +local dprint = gs_debug.dprint + +local manager = require('gitsigns.manager') +local hl = require('gitsigns.highlight') + +local gs_cache = require('gitsigns.cache') +local cache = gs_cache.cache +local CacheEntry = gs_cache.CacheEntry +local Status = require("gitsigns.status") + +local gs_config = require('gitsigns.config') +local config = gs_config.config + +local void = require('gitsigns.async').void +local util = require('gitsigns.util') + +local throttle_by_id = require("gitsigns.debounce").throttle_by_id + +local api = vim.api +local uv = vim.loop + +local record GitContext + toplevel: string + gitdir: string + file: string + commit: string + base: string +end + +local record M + attach: function(cbuf: integer, ctx: GitContext, trigger: string) + detach: function(bufnr: integer, _keep_signs: boolean) + detach_all: function() +end + +local vimgrep_running = false + +-- @return (string, string) Tuple of buffer name and commit +local function parse_fugitive_uri(name: string): string, string + if vim.fn.exists('*FugitiveReal') == 0 then + dprint("Fugitive not installed") + return + end + + local path = vim.fn.FugitiveReal(name) + 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 + +local function parse_gitsigns_uri(name: string): string, string + -- TODO(lewis6991): Support submodules + local _, _, root_path, commit, rel_path = + name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]]) + if commit == ':0' then + -- ':0' means the index so clear commit so we attach normally + commit = nil + end + if root_path then + name = root_path .. '/' .. rel_path + end + return name, commit +end + +local function get_buf_path(bufnr: integer): string, string + local file = + uv.fs_realpath(api.nvim_buf_get_name(bufnr)) + or + api.nvim_buf_call(bufnr, function(): string + return vim.fn.expand('%:p') + end) + + if not vim.wo.diff then + if vim.startswith(file, 'fugitive://') then + local path, commit = parse_fugitive_uri(file) + dprintf("Fugitive buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + end + + if vim.startswith(file, 'gitsigns://') then + local path, commit = parse_gitsigns_uri(file) + dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file) + path = uv.fs_realpath(path) + if path then + return path, commit + end + end + end + + return file +end + +local function on_lines(_, bufnr: integer, _, first: integer, last_orig: integer, last_new: integer, byte_count: integer): boolean + 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 + +local function on_reload(_, bufnr: integer) + local __FUNC__ = 'on_reload' + dprint('Reload') + manager.update_debounced(bufnr) +end + +local function on_detach(_, bufnr: integer) + M.detach(bufnr, true) +end + +local function on_attach_pre(bufnr: integer): string, string + local gitdir, toplevel: string, string + if config._on_attach_pre then + local res: any = async.wrap(config._on_attach_pre, 2)(bufnr) + dprintf('ran on_attach_pre with result %s', vim.inspect(res)) + if res is table then + if type(res.gitdir) == 'string' then + gitdir = res.gitdir as string + end + if type(res.toplevel) == 'string' then + toplevel = res.toplevel as string + end + end + end + return gitdir, toplevel +end + +local function try_worktrees(_bufnr: integer, file: string, encoding: string): git.Obj + 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 done_setup = false + +local function setup() + if done_setup then + return + end + + done_setup = true + + manager.setup() + + hl.setup_highlights() + api.nvim_create_autocmd('ColorScheme', { + group = 'gitsigns', + callback = hl.setup_highlights + }) + + api.nvim_create_autocmd('OptionSet', { + group = 'gitsigns', + pattern = 'fileformat', + callback = function() + require('gitsigns.actions').refresh() + end} + ) + + -- vimpgrep creates and deletes lots of buffers so attaching to each one will + -- waste lots of resource and even slow down vimgrep. + api.nvim_create_autocmd('QuickFixCmdPre', { + group = 'gitsigns', + pattern ='*vimgrep*', + callback = function() + vimgrep_running = true + end + }) + + api.nvim_create_autocmd('QuickFixCmdPost', { + group = 'gitsigns', + pattern ='*vimgrep*', + callback = function() + vimgrep_running = false + end + }) + + require('gitsigns.current_line_blame').setup() + + api.nvim_create_autocmd('VimLeavePre' , { + group = 'gitsigns', + callback = M.detach_all + }) + +end + +-- Ensure attaches cannot be interleaved. +-- Since attaches are asynchronous we need to make sure an attach isn't +-- performed whilst another one is in progress. +local attach_throttled = throttle_by_id(function(cbuf: integer, ctx: GitContext, aucmd: string) + local __FUNC__ = 'attach' + + setup() + + if vimgrep_running then + dprint('attaching is disabled') + return + end + + 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 + + local encoding = vim.bo[cbuf].fileencoding + if encoding == '' then + encoding = 'utf-8' + end + local file: string + local commit: string + local gitdir_oap: string + local toplevel_oap: string + + if ctx then + gitdir_oap = ctx.gitdir + toplevel_oap = ctx.toplevel + file = ctx.toplevel .. util.path_sep .. ctx.file + commit = ctx.commit + else + if api.nvim_buf_line_count(cbuf) > config.max_file_length then + dprint('Exceeds max_file_length') + return + end + + if vim.bo[cbuf].buftype ~= '' then + dprint('Non-normal buffer') + return + end + + file, commit = get_buf_path(cbuf) + local file_dir = util.dirname(file) + + if not file_dir or not util.path_exists(file_dir) then + dprint('Not a path') + return + end + + gitdir_oap, toplevel_oap = on_attach_pre(cbuf) + end + + local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) + + if not git_obj and not ctx then + git_obj = try_worktrees(cbuf, file, encoding) + async.scheduler() + end + + if not git_obj then + dprint('Empty git obj') + return + end + local repo = git_obj.repo + + async.scheduler() + Status:update(cbuf, { + head = repo.abbrev_head, + root = repo.toplevel, + gitdir = repo.gitdir, + }) + + if vim.startswith(file, repo.gitdir..util.path_sep) then + dprint('In non-standard git dir') + return + end + + if not 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 config.on_attach and config.on_attach(cbuf) == false then + dprint('User on_attach() returned false') + return + end + + cache[cbuf] = CacheEntry.new { + base = ctx and ctx.base or config.base, + file = file, + commit = commit, + gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), + git_obj = git_obj + } + + 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 + }) + + -- Initial update + manager.update(cbuf, cache[cbuf]) + + if config.keymaps and not vim.tbl_isempty(config.keymaps) then + require('gitsigns.mappings')(config.keymaps as {string:any}, cbuf) + end +end) + +--- Detach Gitsigns from all buffers it is attached to. +function M.detach_all() + for k, _ in pairs(cache as {integer:CacheEntry}) do + M.detach(k) + end +end + +--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not +--- provided then the current buffer is used. +--- +--- Parameters: ~ +--- {bufnr} (number): Buffer number +function M.detach(bufnr: integer, _keep_signs: boolean) + -- 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) + + cache:destroy(bufnr) +end + + +--- Attach Gitsigns to the buffer. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters: ~ +--- {bufnr} (number): Buffer number +--- {ctx} (table|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|nil) +--- The git revision that the file should be compared to. +M.attach = void(function(bufnr: integer, ctx: GitContext, _trigger: string) + attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger) +end) + +return M diff --git a/teal/gitsigns/cli.tl b/teal/gitsigns/cli.tl index 1c890b94..db5e8552 100644 --- a/teal/gitsigns/cli.tl +++ b/teal/gitsigns/cli.tl @@ -7,6 +7,9 @@ local message = require'gitsigns.message' local parse_args = require('gitsigns.cli.argparse').parse_args +local actions = require('gitsigns.actions') +local attach = require('gitsigns.attach') + -- try to parse each argument as a lua boolean, nil or number, if fails then -- keep argument as a string: -- @@ -26,18 +29,17 @@ local function parse_to_lua(a: string): any end local record M - run : function(funcs: {string:function}, params: vim.api.UserCmdParams) + run: function(params: vim.api.UserCmdParams) end -function M.complete(funcs: {string:function}, arglead: string, line: string): {string} +function M.complete(arglead: string, line: string): {string} local words = vim.split(line, '%s+') local n: integer = #words - local actions = require('gitsigns.actions') local matches: {string} = {} if n == 2 then - for _, m in ipairs{actions as {string:function}, funcs} do - for func, _ in pairs(m) do + for _, m in ipairs{actions, attach} do + for func, _ in pairs(m as {string:function}) do if not func:match('^[a-z]') then -- exclude elseif vim.startswith(func, arglead) then @@ -55,22 +57,19 @@ function M.complete(funcs: {string:function}, arglead: string, line: string): {s return matches end -M.run = void(function(funcs: {string:function}, params: vim.api.UserCmdParams) +M.run = void(function(params: vim.api.UserCmdParams) local pos_args_raw, named_args_raw = parse_args(params.args) local func = pos_args_raw[1] if not func then - func = async.wrap(vim.ui.select, 3)(M.complete(funcs, '', 'Gitsigns '), {}) + func = async.wrap(vim.ui.select, 3)(M.complete('', 'Gitsigns '), {}) end local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2)) as {any} local named_args = vim.tbl_map(parse_to_lua, named_args_raw) as {string:any} local args = vim.tbl_extend('error', pos_args, named_args) - local actions = require('gitsigns.actions') - local actions0 = actions as {string:function} - dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, {newline=' ', indent=''})) local cmd_func = actions._get_cmd_func(func) @@ -81,15 +80,16 @@ M.run = void(function(funcs: {string:function}, params: vim.api.UserCmdParams) return end - if type(actions0[func]) == 'function' then - actions0[func](unpack(pos_args), named_args) - return - end - - if type(funcs[func]) == 'function' then - -- Note functions here do not have named arguments - funcs[func](unpack(pos_args)) - return + for m, has_named in pairs{ + [actions] = true, + [attach] = false + } do + local f = (m as {string:any})[func] + if f is 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) diff --git a/teal/gitsigns/debug.tl b/teal/gitsigns/debug.tl index a50597ef..427d14ba 100644 --- a/teal/gitsigns/debug.tl +++ b/teal/gitsigns/debug.tl @@ -123,9 +123,11 @@ local function process(raw_item: any, path: {string}): any return raw_item end -function M.add_debug_functions(cache: any): {string:function} +function M.add_debug_functions(): {string:function} local R: {string:function} = {} R.dump_cache = function(): any + -- TODO(lewis6991): hack: use package.loaded to avoid circular deps + local cache = (package.loaded['gitsigns.cache'] as table).cache local text = vim.inspect(cache, { process = process }) vim.api.nvim_echo({{text}}, false, {}) return cache diff --git a/teal/gitsigns/git.tl b/teal/gitsigns/git.tl index 59c7b1d3..56e59a34 100644 --- a/teal/gitsigns/git.tl +++ b/teal/gitsigns/git.tl @@ -5,6 +5,9 @@ local gsd = require("gitsigns.debug") local util = require('gitsigns.util') local subprocess = require('gitsigns.subprocess') +local gs_config = require('gitsigns.config') +local config = gs_config.config + local gs_hunks = require("gitsigns.hunks") local Hunk = gs_hunks.Hunk @@ -56,10 +59,6 @@ local record M end version: Version - enable_yadm: boolean - - set_version: function(string) - record RepoInfo gitdir: string toplevel: string @@ -167,8 +166,35 @@ local function check_version(version: {number,number,number}): boolean return true end +--- @async +local function set_version(version: string) + if version ~= 'auto' then + M.version = parse_version(version) + return + end + + local _, _, stdout, stderr = async.wait(2, subprocess.run_job, { + command = 'git', args = { '--version' } + }) + + local line = vim.split(stdout or '', '\n', true)[1] + if not line then + err("Unable to detect git version as 'git --version' failed to return anything") + eprint(stderr) + return + end + assert(type(line) == 'string', 'Unexpected output: '..line) + assert(startswith(line, 'git version'), 'Unexpected output: '..line) + local parts = vim.split(line, '%s+') + M.version = parse_version(parts[3]) +end + + --- @async local git_command = async.create(function(args: {string}, spec: GJobSpec): {string}, string + if not M.version then + set_version(config._git_version) + end spec = spec or {} spec.command = spec.command or 'git' spec.args = spec.command == 'git' and { @@ -310,25 +336,6 @@ function M.get_repo_info(path: string, cmd: string, gitdir: string, toplevel: st return ret end ---- @async -function M.set_version(version: string) - if version ~= 'auto' then - M.version = parse_version(version) - return - end - local results, stderr = git_command{'--version'} - local line = results[1] - if not line then - err("Unable to detect git version as 'git --version' failed to return anything") - eprint(stderr) - return - end - assert(type(line) == 'string', 'Unexpected output: '..line) - assert(startswith(line, 'git version'), 'Unexpected output: '..line) - local parts = vim.split(line, '%s+') - M.version = parse_version(parts[3]) -end - -------------------------------------------------------------------------------- -- Git repo object methods -------------------------------------------------------------------------------- @@ -444,7 +451,7 @@ function Repo.new(dir: string, gitdir: string, toplevel: string): Repo end -- Try yadm - if M.enable_yadm and not self.gitdir then + if config.yadm.enable and not self.gitdir then if vim.startswith(dir, os.getenv('HOME')) and #git_command({'ls-files', dir}, {command = 'yadm'}) ~= 0 then M.get_repo_info(dir, 'yadm', gitdir, toplevel) diff --git a/teal/gitsigns/manager.tl b/teal/gitsigns/manager.tl index 52051e08..7ab08e69 100644 --- a/teal/gitsigns/manager.tl +++ b/teal/gitsigns/manager.tl @@ -20,7 +20,6 @@ local eprint = gs_debug.eprint local subprocess = require('gitsigns.subprocess') local util = require('gitsigns.util') local run_diff = require('gitsigns.diff') -local git = require('gitsigns.git') local uv = require('gitsigns.uv') local gs_hunks = require("gitsigns.hunks") @@ -38,7 +37,6 @@ local record M update_debounced : function(bufnr: integer, CacheEntry) on_lines : function(buf: integer, first: integer, last_orig: integer, last_new: integer): boolean watch_gitdir : function(bufnr: integer, gitdir: string): vim.loop.FSPollObj - update_cwd_head : function() detach : function(bufnr: integer, keep_signs: boolean) reset_signs : function() setup : function() @@ -444,67 +442,6 @@ function M.watch_gitdir(bufnr: integer, gitdir: string): vim.loop.FSPollObj return w end -local cwd_watcher: vim.loop.FSPollObj - -M.update_cwd_head = void(function() - if cwd_watcher then - cwd_watcher:stop() - else - cwd_watcher = uv.new_fs_poll(true) - end - - local cwd = vim.loop.cwd() - local gitdir, head: string, string - - -- Look in the cache first - for _, bcache in pairs(cache as {number:CacheEntry}) do - local repo = bcache.git_obj.repo - if repo.toplevel == cwd then - head = repo.abbrev_head - gitdir = repo.gitdir - break - end - end - - if not head or not gitdir then - local info = git.get_repo_info(cwd) - gitdir = info.gitdir - head = info.abbrev_head - end - - scheduler() - vim.g.gitsigns_head = head - - if not gitdir then - return - end - - local towatch = gitdir..'/HEAD' - - if cwd_watcher:getpath() == towatch then - -- Already watching - return - end - - -- Watch .git/HEAD to detect branch changes - cwd_watcher:start( - towatch, - config.watch_gitdir.interval, - void(function(err: string) - local __FUNC__ = 'cwd_watcher_cb' - if err then - dprintf('Git dir update error: %s', err) - return - end - dprint('Git cwd dir update') - - local new_head = git.get_repo_info(cwd).abbrev_head - scheduler() - vim.g.gitsigns_head = new_head - end) - ) -end) - function M.reset_signs() -- Remove all signs signs_normal:reset() diff --git a/teal/gitsigns/status.tl b/teal/gitsigns/status.tl index d9ba9f06..b6992d97 100644 --- a/teal/gitsigns/status.tl +++ b/teal/gitsigns/status.tl @@ -1,5 +1,6 @@ local api = vim.api + local record StatusObj added : integer removed : integer @@ -11,7 +12,6 @@ end local Status = { StatusObj = StatusObj, - formatter: function(StatusObj): string = nil } function Status:update(bufnr: integer, status: StatusObj) @@ -24,7 +24,10 @@ function Status:update(bufnr: integer, status: StatusObj) end vim.b[bufnr].gitsigns_head = status.head or '' vim.b[bufnr].gitsigns_status_dict = status - vim.b[bufnr].gitsigns_status = self.formatter(status) + + local config = require('gitsigns.config').config + + vim.b[bufnr].gitsigns_status = config.status_formatter(status) end function Status:clear(bufnr: integer) diff --git a/types/vim.d.tl b/types/vim.d.tl index 964f94a2..ed65aa45 100644 --- a/types/vim.d.tl +++ b/types/vim.d.tl @@ -226,6 +226,10 @@ local record M end is_thread: function(): boolean + + record fs + find: function(string|{string}, table): {string} + end end return M diff --git a/types/vim/api.d.tl b/types/vim/api.d.tl index e100f012..2cbf6d42 100644 --- a/types/vim/api.d.tl +++ b/types/vim/api.d.tl @@ -92,7 +92,16 @@ local record M nvim_create_augroup: function(string, AugroupOpts): integer record AutoCmdOpts - callback: function() + record CallbackData + id: integer + group: integer + event: string + match: string + buf: integer + file: string + data: any + end + callback: function(CallbackData) command: string group: integer|string pattern: string|{string}