From 79f5059a2dd3fbe8e1496711c7a9da29bab4c776 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 | 25 +- Makefile | 15 +- gen_help.lua | 1 + lua/gitsigns.lua | 513 ++++++++-------------------------- lua/gitsigns/attach.lua | 411 +++++++++++++++++++++++++++ lua/gitsigns/cli.lua | 43 +-- lua/gitsigns/config.lua | 6 + lua/gitsigns/debug.lua | 141 ++-------- lua/gitsigns/debug/log.lua | 109 ++++++++ lua/gitsigns/git.lua | 73 ++--- lua/gitsigns/highlight.lua | 6 +- lua/gitsigns/manager.lua | 71 +---- lua/gitsigns/signs.lua | 2 +- lua/gitsigns/status.lua | 7 +- lua/gitsigns/subprocess.lua | 10 +- teal/gitsigns.tl | 521 ++++++++--------------------------- teal/gitsigns/attach.tl | 411 +++++++++++++++++++++++++++ teal/gitsigns/cli.tl | 49 ++-- teal/gitsigns/config.tl | 6 + teal/gitsigns/debug.tl | 141 ++-------- teal/gitsigns/debug/log.tl | 109 ++++++++ teal/gitsigns/git.tl | 73 ++--- teal/gitsigns/highlight.tl | 6 +- teal/gitsigns/manager.tl | 71 +---- teal/gitsigns/signs.tl | 2 +- teal/gitsigns/status.tl | 7 +- teal/gitsigns/subprocess.tl | 10 +- test/gitdir_watcher_spec.lua | 17 +- test/gitsigns_spec.lua | 18 +- test/gs_helpers.lua | 34 +-- test/highlights_spec.lua | 1 + types/vim.d.tl | 4 + types/vim/api.d.tl | 11 +- 33 files changed, 1539 insertions(+), 1385 deletions(-) create mode 100644 lua/gitsigns/attach.lua create mode 100644 lua/gitsigns/debug/log.lua create mode 100644 teal/gitsigns/attach.tl create mode 100644 teal/gitsigns/debug/log.tl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85276448..b77808ca 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 @@ -21,7 +18,7 @@ jobs: strategy: fail-fast: true matrix: - neovim_branch: ['v0.7.2', 'v0.8.3', 'nightly'] + neovim_branch: ['v0.8.3', 'nightly'] runs-on: ubuntu-latest env: NEOVIM_BRANCH: ${{ matrix.neovim_branch }} @@ -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: | @@ -50,12 +39,6 @@ jobs: path: deps key: ${{ steps.get-nvim-sha.outputs.sha }}-${{ hashFiles('.github/workflows/ci.yml, Makefile') }} - - name: Install Lua Deps - run: make lua_deps - - - name: Check lua files are built from latest teal - run: make tl-ensure - - name: Install Neovim build dependencies if: steps.cache-deps.outputs.cache-hit != 'true' run: | @@ -78,5 +61,11 @@ jobs: if: steps.cache-deps.outputs.cache-hit != 'true' run: make test_deps + - name: Install Lua Deps + run: make lua_deps + + - name: Check lua files are built from latest teal + run: make tl-ensure + - name: Run Test run: make test 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..02b31f2a 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -1,405 +1,159 @@ -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 -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 log = require('gitsigns.debug.log') +local dprintf = log.dprintf +local dprint = log.dprint 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 ---- Detach Gitsigns from all buffers it is attached to. -function M.detach_all() - for k, _ in pairs(cache) do - M.detach(k) - end -end +local cwd_watcher ---- 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 git = require('gitsigns.git') -local vimgrep_running = false - -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 + scheduler() + vim.g.gitsigns_head = head -local function try_worktrees(_bufnr, file, encoding) - if not config.worktrees then + if not gitdir 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 towatch = gitdir .. '/HEAD' --- 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') + if cwd_watcher:getpath() == towatch then + -- Already watching 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') + -- 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') - 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() - - 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, - }) + vim.g.gitsigns_head = new_head + end)) - -- 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 exported = { + 'attach', + 'actions', +} + +local function setup_debug() + log.debug_mode = config.debug_mode + log.verbose = config._verbose + + if config.debug_mode then + exported[#exported + 1] = 'debug' 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. @@ -417,87 +171,36 @@ M.setup = void(function(cfg) print('gitsigns: git not in path. Aborting setup') return end + if config.yadm.enable and vim.fn.executable('yadm') == 0 then print("gitsigns: yadm not in path. Ignoring 'yadm.enable' in config") config.yadm.enable = false 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_debug() 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, - }) + if config._test_mode then + require('gitsigns.attach')._setup() + require('gitsigns.git')._set_version(config._git_version) + end - require('gitsigns.current_line_blame').setup() + setup_attach() + setup_cwd_head() - 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)) + M._setup_done = true end) 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..daa72d91 --- /dev/null +++ b/lua/gitsigns/attach.lua @@ -0,0 +1,411 @@ +local async = require('gitsigns.async') +local git = require('gitsigns.git') + +local log = require("gitsigns.debug.log") +local dprintf = log.dprintf +local dprint = log.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 + +function M._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' + + M._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..c3675932 100644 --- a/lua/gitsigns/cli.lua +++ b/lua/gitsigns/cli.lua @@ -1,12 +1,22 @@ local async = require('gitsigns.async') local void = require('gitsigns.async').void -local gs_debug = require("gitsigns.debug") -local dprintf = gs_debug.dprintf +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') + +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: -- @@ -29,14 +39,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 pairs(sources) do for func, _ in pairs(m) do if not func:match('^[a-z]') then -- exclude @@ -55,22 +64,20 @@ function M.complete(funcs, arglead, line) return matches end -M.run = void(function(funcs, params) +M.run = void(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.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 +88,13 @@ 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(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) diff --git a/lua/gitsigns/config.lua b/lua/gitsigns/config.lua index 30de8ba9..c35c32cf 100644 --- a/lua/gitsigns/config.lua +++ b/lua/gitsigns/config.lua @@ -135,6 +135,7 @@ local M = {Config = {DiffOpts = {}, SignConfig = {}, watch_gitdir = {}, current_ + M.config = {} M.schema = { @@ -712,6 +713,11 @@ M.schema = { ]], }, + _test_mode = { + type = 'boolean', + default = false, + }, + word_diff = { type = 'boolean', default = false, diff --git a/lua/gitsigns/debug.lua b/lua/gitsigns/debug.lua index 6696b98a..fb8b497d 100644 --- a/lua/gitsigns/debug.lua +++ b/lua/gitsigns/debug.lua @@ -1,110 +1,6 @@ -local M = { - debug_mode = false, - verbose = false, - messages = {}, -} +local log = require('gitsigns.debug.log') -local function getvarvalue(name, lvl) - lvl = lvl + 1 - local value - local found - - -- 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 - return getfenv(func)[name] -end - -local function get_context(lvl) - lvl = lvl + 1 - local ret = {} - 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. -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 - 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] = msg - if M.debug_mode then - error(msg) - end -end - -function M.eprint(msg) - eprint(msg, 1) -end - -function M.eprintf(fmt, ...) - eprint(fmt:format(...), 1) -end +local M = {} local function process(raw_item, path) if path[#path] == vim.inspect.METATABLE then @@ -123,28 +19,25 @@ local function process(raw_item, path) return raw_item end -function M.add_debug_functions(cache) - local R = {} - R.dump_cache = function() - local text = vim.inspect(cache, { process = process }) - vim.api.nvim_echo({ { text } }, false, {}) - return cache - end +function M.dump_cache() + -- 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 +end - R.debug_messages = function(noecho) - if not noecho then - for _, m in ipairs(M.messages) do - vim.api.nvim_echo({ { m } }, false, {}) - end +function M.debug_messages(noecho) + if not noecho then + for _, m in ipairs(log.messages) do + vim.api.nvim_echo({ { m } }, false, {}) end - return M.messages - end - - R.clear_debug = function() - M.messages = {} end + return log.messages +end - return R +function M.clear_debug() + log.messages = {} end return M diff --git a/lua/gitsigns/debug/log.lua b/lua/gitsigns/debug/log.lua new file mode 100644 index 00000000..3ca2246b --- /dev/null +++ b/lua/gitsigns/debug/log.lua @@ -0,0 +1,109 @@ +local M = { + debug_mode = false, + verbose = false, + messages = {}, +} + +local function getvarvalue(name, lvl) + lvl = lvl + 1 + local value + local found + + -- 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 + return getfenv(func)[name] +end + +local function get_context(lvl) + lvl = lvl + 1 + local ret = {} + 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. +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 + 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] = msg + if M.debug_mode then + error(msg) + end +end + +function M.eprint(msg) + eprint(msg, 1) +end + +function M.eprintf(fmt, ...) + eprint(fmt:format(...), 1) +end + +return M diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index 422939e7..3cfb8d3d 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -1,18 +1,21 @@ local async = require('gitsigns.async') local scheduler = require('gitsigns.async').scheduler -local gsd = require("gitsigns.debug") +local log = require("gitsigns.debug.log") 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 local uv = vim.loop local startswith = vim.startswith -local dprint = require("gitsigns.debug").dprint -local eprint = require("gitsigns.debug").eprint +local dprint = require('gitsigns.debug.log').dprint +local eprint = require('gitsigns.debug.log').eprint local err = require('gitsigns.message').error @@ -78,10 +81,6 @@ local M = {BlameInfo = {}, Version = {}, RepoInfo = {}, Repo = {}, FileProps = { - - - - @@ -167,8 +166,35 @@ local function check_version(version) return true end +--- @async +function M._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 + M._set_version(config._git_version) + end spec = spec or {} spec.command = spec.command or 'git' spec.args = spec.command == 'git' and { @@ -187,7 +213,7 @@ local git_command = async.create(function(args, spec) if not spec.suppress_stderr then if stderr then local cmd_str = table.concat({ spec.command, unpack(args) }, ' ') - gsd.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr) + log.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr) end end @@ -199,10 +225,10 @@ local git_command = async.create(function(args, spec) stdout_lines[#stdout_lines] = nil end - if gsd.verbose then - gsd.vprintf('%d lines:', #stdout_lines) + if log.verbose then + log.vprintf('%d lines:', #stdout_lines) for i = 1, math.min(10, #stdout_lines) do - gsd.vprintf('\t%s', stdout_lines[i]) + log.vprintf('\t%s', stdout_lines[i]) end end @@ -235,7 +261,7 @@ local function process_abbrev_head(gitdir, head_str, path, cmd) suppress_stderr = true, cwd = path, })[1] or '' - if gsd.debug_mode and short_sha ~= '' then + if log.debug_mode and short_sha ~= '' then short_sha = 'HEAD' end if util.path_exists(gitdir .. '/rebase-merge') or @@ -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) @@ -501,7 +508,7 @@ function Obj:file_info(file, silent) -- Suppress_stderr 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 - gsd.eprint(stderr) + log.eprint(stderr) end end diff --git a/lua/gitsigns/highlight.lua b/lua/gitsigns/highlight.lua index 4fc134ba..e4193224 100644 --- a/lua/gitsigns/highlight.lua +++ b/lua/gitsigns/highlight.lua @@ -168,8 +168,11 @@ local function cmul(x, factor) 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 + local function derive(hl, hldef) - local dprintf = require("gitsigns.debug").dprintf for _, d in ipairs(hldef) do if is_hl_set(d) then dprintf('Deriving %s from %s', hl, d) @@ -201,7 +204,6 @@ end -- Setup a GitSign* highlight by deriving it from other potentially present -- highlights. M.setup_highlights = function() - local dprintf = require("gitsigns.debug").dprintf for _, hlg in ipairs(M.hls) do for hl, hldef in pairs(hlg) do if is_hl_set(hl) then diff --git a/lua/gitsigns/manager.lua b/lua/gitsigns/manager.lua index 891e4a78..6d7df375 100644 --- a/lua/gitsigns/manager.lua +++ b/lua/gitsigns/manager.lua @@ -12,15 +12,14 @@ local Status = require("gitsigns.status") local debounce_trailing = require('gitsigns.debounce').debounce_trailing local throttle_by_id = require('gitsigns.debounce').throttle_by_id -local gs_debug = require("gitsigns.debug") -local dprint = gs_debug.dprint -local dprintf = gs_debug.dprintf -local eprint = gs_debug.eprint +local log = require('gitsigns.debug.log') +local dprint = log.dprint +local dprintf = log.dprintf +local eprint = log.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/signs.lua b/lua/gitsigns/signs.lua index 922bfc15..92ae0dfc 100644 --- a/lua/gitsigns/signs.lua +++ b/lua/gitsigns/signs.lua @@ -1,7 +1,7 @@ local config = require('gitsigns.config').config local SignsConfig = require('gitsigns.config').Config.SignsConfig -local dprint = require('gitsigns.debug').dprint +local dprint = require('gitsigns.debug.log').dprint local B = require('gitsigns.signs.base') 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/lua/gitsigns/subprocess.lua b/lua/gitsigns/subprocess.lua index 6b3b94b8..26594e5d 100644 --- a/lua/gitsigns/subprocess.lua +++ b/lua/gitsigns/subprocess.lua @@ -1,4 +1,4 @@ -local gsd = require("gitsigns.debug") +local log = require("gitsigns.debug.log") local guv = require("gitsigns.uv") local uv = vim.loop @@ -47,7 +47,7 @@ end local function handle_reader(pipe, output) pipe:read_start(function(err, data) if err then - gsd.eprint(err) + log.eprint(err) end if data then output[#output + 1] = data @@ -59,9 +59,9 @@ end function M.run_job(obj, callback) local __FUNC__ = 'run_job' - if gsd.debug_mode then + if log.debug_mode then local cmd = obj.command .. ' ' .. table.concat(obj.args, ' ') - gsd.dprint(cmd) + log.dprint(cmd) end local stdout_data = {} @@ -75,7 +75,7 @@ function M.run_job(obj, callback) end local handle, pid - handle, pid = guv.spawn(obj.command, { + handle, pid = vim.loop.spawn(obj.command, { args = obj.args, stdio = { stdin, stdout, stderr }, cwd = obj.cwd, diff --git a/teal/gitsigns.tl b/teal/gitsigns.tl index 34dc067e..29fabebf 100644 --- a/teal/gitsigns.tl +++ b/teal/gitsigns.tl @@ -1,405 +1,159 @@ -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 -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 log = require('gitsigns.debug.log') +local dprintf = log.dprintf +local dprint = log.dprint 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 + setup: function(cfg: Config) -local GitContext = M.GitContext + -- from attach.tl + attach: function(cbuf: integer, ctx: table, trigger: string) ---- 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 + _setup_done: boolean 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) +local cwd_watcher: vim.loop.FSPollObj - -- 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 - - 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 vimgrep_running = false - -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') - end - - if not api.nvim_buf_is_loaded(cbuf) then - dprint('Non-loaded buffer') - return + cwd_watcher = uv.new_fs_poll(true) 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 + local cwd = vim.loop.cwd() + local gitdir, head: string, string - file, commit = get_buf_path(cbuf) - local file_dir = util.dirname(file) + local gs_cache = require('gitsigns.cache') - if not file_dir or not util.path_exists(file_dir) then - dprint('Not a path') - 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 - - 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) - 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 + local git = require('gitsigns.git') - if not config.attach_to_untracked and git_obj.object_name == nil then - dprint('File is untracked') - return + if not head or not gitdir then + local info = git.get_repo_info(cwd) + gitdir = info.gitdir + head = info.abbrev_head end - -- On windows os.tmpname() crashes in callback threads so initialise this - -- variable on the main thread. scheduler() + vim.g.gitsigns_head = head - if config.on_attach and config.on_attach(cbuf) == false then - dprint('User on_attach() returned false') + if not gitdir then 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 - } + local towatch = gitdir..'/HEAD' - if not api.nvim_buf_is_loaded(cbuf) then - dprint('Un-loaded buffer') + if cwd_watcher:getpath() == towatch then + -- Already watching 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 exported = { + 'attach', + 'actions' +} + +local function setup_debug() + log.debug_mode = config.debug_mode + log.verbose = config._verbose + + if config.debug_mode then + exported[#exported+1] = 'debug' 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. @@ -417,87 +171,36 @@ M.setup = void(function(cfg: Config) print('gitsigns: git not in path. Aborting setup') return end + if config.yadm.enable and vim.fn.executable('yadm') == 0 then print("gitsigns: yadm not in path. Ignoring 'yadm.enable' in config") config.yadm.enable = false 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_debug() 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 - }) + if config._test_mode then + require'gitsigns.attach'._setup() + require'gitsigns.git'._set_version(config._git_version) + end - require('gitsigns.current_line_blame').setup() + setup_attach() + setup_cwd_head() - 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)) + M._setup_done = true end) 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..86eb5eb3 --- /dev/null +++ b/teal/gitsigns/attach.tl @@ -0,0 +1,411 @@ +local async = require('gitsigns.async') +local git = require('gitsigns.git') + +local log = require("gitsigns.debug.log") +local dprintf = log.dprintf +local dprint = log.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 + +function M._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' + + M._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..95cf23d9 100644 --- a/teal/gitsigns/cli.tl +++ b/teal/gitsigns/cli.tl @@ -1,12 +1,22 @@ local async = require('gitsigns.async') local void = require('gitsigns.async').void -local gs_debug = require("gitsigns.debug") -local dprintf = gs_debug.dprintf -local message = require'gitsigns.message' +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') + +local sources = { + [actions] = true, + [attach] = false, + [gs_debug] = false +} as {{string:function}:boolean} + -- try to parse each argument as a lua boolean, nil or number, if fails then -- keep argument as a string: -- @@ -26,18 +36,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 pairs(sources) 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 +64,20 @@ 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 __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.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 +88,13 @@ 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(sources) 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/config.tl b/teal/gitsigns/config.tl index 29b61549..94db3bb7 100644 --- a/teal/gitsigns/config.tl +++ b/teal/gitsigns/config.tl @@ -129,6 +129,7 @@ local record M _git_version: string _verbose: boolean + _test_mode: boolean end schema: {string:SchemaElem} @@ -712,6 +713,11 @@ M.schema = { ]] }, + _test_mode = { + type = 'boolean', + default = false, + }, + word_diff = { type = 'boolean', default = false, diff --git a/teal/gitsigns/debug.tl b/teal/gitsigns/debug.tl index a50597ef..edaa4f8a 100644 --- a/teal/gitsigns/debug.tl +++ b/teal/gitsigns/debug.tl @@ -1,110 +1,6 @@ -local M = { - debug_mode = false, - verbose = false, - messages: {string} = {} -} +local log = require'gitsigns.debug.log' -local function getvarvalue(name: string, lvl: integer): any - lvl = lvl + 1 - local value: any - local found: boolean - - -- try local variables - local i = 1 - while true do - local n, v = debug.getlocal(lvl as function, i) as (string, any) - 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 as function - i = 1 - while true do - local n, v = debug.getupvalue(func, i) as (string, any) - if not n then break end - if n == name then return v end - i = i + 1 - end - - -- not found; get global - return getfenv(func)[name] -end - -local function get_context(lvl: integer): table - lvl = lvl + 1 - local ret: table = {} - ret.name = getvarvalue('__FUNC__', lvl) as string - 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. -local function cprint(obj: any, lvl: integer) - lvl = lvl + 1 - local msg = obj is string and obj or vim.inspect(obj) - local ctx = get_context(lvl) - local msg2: 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: any) - if not M.debug_mode then return end - cprint(obj, 2) -end - -function M.dprintf(obj: string, ...:any) - if not M.debug_mode then return end - cprint(obj:format(...), 2) -end - -function M.vprint(obj: any) - if not (M.debug_mode and M.verbose) then return end - cprint(obj, 2) -end - -function M.vprintf(obj: string, ...:any) - if not (M.debug_mode and M.verbose) then return end - cprint(obj:format(...), 2) -end - -local function eprint(msg: string, level: integer) - 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] = msg - if M.debug_mode then - error(msg) - end -end - -function M.eprint(msg: string) - eprint(msg, 1) -end - -function M.eprintf(fmt: string, ...:any) - eprint(fmt:format(...), 1) -end +local M = {} local function process(raw_item: any, path: {string}): any if path[#path] == vim.inspect.METATABLE then @@ -123,28 +19,25 @@ local function process(raw_item: any, path: {string}): any return raw_item end -function M.add_debug_functions(cache: any): {string:function} - local R: {string:function} = {} - R.dump_cache = function(): any - local text = vim.inspect(cache, { process = process }) - vim.api.nvim_echo({{text}}, false, {}) - return cache - end +function M.dump_cache(): 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 +end - R.debug_messages = function(noecho: boolean): {string} - if not noecho then - for _, m in ipairs(M.messages) do - vim.api.nvim_echo({{m}}, false, {}) - end +function M.debug_messages(noecho: boolean): {string} + if not noecho then + for _, m in ipairs(log.messages) do + vim.api.nvim_echo({{m}}, false, {}) end - return M.messages - end - - R.clear_debug = function() - M.messages = {} end + return log.messages +end - return R +function M.clear_debug() + log.messages = {} end return M diff --git a/teal/gitsigns/debug/log.tl b/teal/gitsigns/debug/log.tl new file mode 100644 index 00000000..3557f22b --- /dev/null +++ b/teal/gitsigns/debug/log.tl @@ -0,0 +1,109 @@ +local M = { + debug_mode = false, + verbose = false, + messages: {string} = {}, +} + +local function getvarvalue(name: string, lvl: integer): any + lvl = lvl + 1 + local value: any + local found: boolean + + -- try local variables + local i = 1 + while true do + local n, v = debug.getlocal(lvl as function, i) as (string, any) + 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 as function + i = 1 + while true do + local n, v = debug.getupvalue(func, i) as (string, any) + if not n then break end + if n == name then return v end + i = i + 1 + end + + -- not found; get global + return getfenv(func)[name] +end + +local function get_context(lvl: integer): table + lvl = lvl + 1 + local ret: table = {} + ret.name = getvarvalue('__FUNC__', lvl) as string + 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. +local function cprint(obj: any, lvl: integer) + lvl = lvl + 1 + local msg = obj is string and obj or vim.inspect(obj) + local ctx = get_context(lvl) + local msg2: 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: any) + if not M.debug_mode then return end + cprint(obj, 2) +end + +function M.dprintf(obj: string, ...:any) + if not M.debug_mode then return end + cprint(obj:format(...), 2) +end + +function M.vprint(obj: any) + if not (M.debug_mode and M.verbose) then return end + cprint(obj, 2) +end + +function M.vprintf(obj: string, ...:any) + if not (M.debug_mode and M.verbose) then return end + cprint(obj:format(...), 2) +end + +local function eprint(msg: string, level: integer) + 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] = msg + if M.debug_mode then + error(msg) + end +end + +function M.eprint(msg: string) + eprint(msg, 1) +end + +function M.eprintf(fmt: string, ...:any) + eprint(fmt:format(...), 1) +end + +return M diff --git a/teal/gitsigns/git.tl b/teal/gitsigns/git.tl index 59c7b1d3..9f34d6e5 100644 --- a/teal/gitsigns/git.tl +++ b/teal/gitsigns/git.tl @@ -1,18 +1,21 @@ local async = require('gitsigns.async') local scheduler = require('gitsigns.async').scheduler -local gsd = require("gitsigns.debug") +local log = require("gitsigns.debug.log") 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 local uv = vim.loop local startswith = vim.startswith -local dprint = require("gitsigns.debug").dprint -local eprint = require("gitsigns.debug").eprint +local dprint = require('gitsigns.debug.log').dprint +local eprint = require('gitsigns.debug.log').eprint local err = require('gitsigns.message').error local record GJobSpec @@ -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 +function M._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 + M._set_version(config._git_version) + end spec = spec or {} spec.command = spec.command or 'git' spec.args = spec.command == 'git' and { @@ -187,7 +213,7 @@ local git_command = async.create(function(args: {string}, spec: GJobSpec): {stri if not spec.suppress_stderr then if stderr then local cmd_str = table.concat({spec.command, unpack(args)}, ' ') - gsd.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr) + log.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr) end end @@ -199,10 +225,10 @@ local git_command = async.create(function(args: {string}, spec: GJobSpec): {stri stdout_lines[#stdout_lines] = nil end - if gsd.verbose then - gsd.vprintf('%d lines:', #stdout_lines) + if log.verbose then + log.vprintf('%d lines:', #stdout_lines) for i = 1, math.min(10, #stdout_lines) do - gsd.vprintf('\t%s', stdout_lines[i]) + log.vprintf('\t%s', stdout_lines[i]) end end @@ -235,7 +261,7 @@ local function process_abbrev_head(gitdir: string, head_str: string, path: strin suppress_stderr = true, cwd = path, })[1] or '' - if gsd.debug_mode and short_sha ~= '' then + if log.debug_mode and short_sha ~= '' then short_sha = 'HEAD' end if util.path_exists(gitdir..'/rebase-merge') @@ -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) @@ -501,7 +508,7 @@ function Obj:file_info(file: string, silent: boolean): M.FileProps -- Suppress_stderr 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 - gsd.eprint(stderr) + log.eprint(stderr) end end diff --git a/teal/gitsigns/highlight.tl b/teal/gitsigns/highlight.tl index 6d25a7eb..9d68b96f 100644 --- a/teal/gitsigns/highlight.tl +++ b/teal/gitsigns/highlight.tl @@ -168,8 +168,11 @@ local function cmul(x: integer, factor: number): integer return math.floor(math.floor(r*factor) * 2^16 + math.floor(g*factor) * 2^8 + math.floor(b*factor)) end +local function dprintf(fmt: string, ...:any) + require('gitsigns.debug.log').dprintf(fmt, ...) +end + local function derive(hl: string, hldef: Hldef) - local dprintf = require("gitsigns.debug").dprintf for _, d in ipairs(hldef) do if is_hl_set(d) then dprintf('Deriving %s from %s', hl, d) @@ -201,7 +204,6 @@ end -- Setup a GitSign* highlight by deriving it from other potentially present -- highlights. M.setup_highlights = function() - local dprintf = require("gitsigns.debug").dprintf for _, hlg in ipairs(M.hls) do for hl, hldef in pairs(hlg) do if is_hl_set(hl) then diff --git a/teal/gitsigns/manager.tl b/teal/gitsigns/manager.tl index 52051e08..bba5363a 100644 --- a/teal/gitsigns/manager.tl +++ b/teal/gitsigns/manager.tl @@ -12,15 +12,14 @@ local Status = require("gitsigns.status") local debounce_trailing = require('gitsigns.debounce').debounce_trailing local throttle_by_id = require('gitsigns.debounce').throttle_by_id -local gs_debug = require("gitsigns.debug") -local dprint = gs_debug.dprint -local dprintf = gs_debug.dprintf -local eprint = gs_debug.eprint +local log = require('gitsigns.debug.log') +local dprint = log.dprint +local dprintf = log.dprintf +local eprint = log.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/signs.tl b/teal/gitsigns/signs.tl index 0eed091f..953b164f 100644 --- a/teal/gitsigns/signs.tl +++ b/teal/gitsigns/signs.tl @@ -1,7 +1,7 @@ local config = require('gitsigns.config').config local SignsConfig = require('gitsigns.config').Config.SignsConfig -local dprint = require('gitsigns.debug').dprint +local dprint = require('gitsigns.debug.log').dprint local B = require('gitsigns.signs.base') 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/teal/gitsigns/subprocess.tl b/teal/gitsigns/subprocess.tl index a6da1cab..3250ecf7 100644 --- a/teal/gitsigns/subprocess.tl +++ b/teal/gitsigns/subprocess.tl @@ -1,4 +1,4 @@ -local gsd = require("gitsigns.debug") +local log = require("gitsigns.debug.log") local guv = require("gitsigns.uv") local uv = vim.loop @@ -47,7 +47,7 @@ end local function handle_reader(pipe: uv.Pipe, output: {string}) pipe:read_start(function(err: string, data: string) if err then - gsd.eprint(err) + log.eprint(err) end if data then output[#output+1] = data @@ -59,9 +59,9 @@ end function M.run_job(obj: M.JobSpec, callback: function(integer, integer, string, string)) local __FUNC__ = 'run_job' - if gsd.debug_mode then + if log.debug_mode then local cmd: string = obj.command..' '..table.concat(obj.args, ' ') - gsd.dprint(cmd) + log.dprint(cmd) end local stdout_data: {string} = {} @@ -75,7 +75,7 @@ function M.run_job(obj: M.JobSpec, callback: function(integer, integer, string, end local handle, pid: uv.Process, integer - handle, pid = guv.spawn(obj.command, { + handle, pid = vim.loop.spawn(obj.command, { args = obj.args, stdio = { stdin, stdout, stderr }, cwd = obj.cwd diff --git a/test/gitdir_watcher_spec.lua b/test/gitdir_watcher_spec.lua index 7bf9ffb5..845e3345 100644 --- a/test/gitdir_watcher_spec.lua +++ b/test/gitdir_watcher_spec.lua @@ -41,30 +41,26 @@ describe('gitdir_watcher', function() end) it('can follow moved files', function() - local screen = Screen.new(20, 17) - screen:attach({ext_messages=false}) setup_test_repo() setup_gitsigns(test_config) command('Gitsigns clear_debug') edit(test_file) - local test_file2 = test_file..'2' - local test_file3 = test_file..'3' - match_debug_messages { - 'attach(1): Attaching (trigger=BufRead)', + 'attach(1): Attaching (trigger=BufReadPost)', p"run_job: git .* config user.name", p"run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD", p('run_job: git .* ls%-files .* '..helpers.pesc(test_file)), 'watch_gitdir(1): Watching git dir', p'run_job: git .* show :0:dummy.txt', - 'update(1): updates: 1, jobs: 6', + 'update(1): updates: 1, jobs: 5', } eq({[1] = test_file}, get_bufs()) command('Gitsigns clear_debug') + local test_file2 = test_file..'2' git{'mv', test_file, test_file2} match_debug_messages { @@ -76,13 +72,14 @@ describe('gitdir_watcher', function() p('run_job: git .* ls%-files .* '..helpers.pesc(test_file2)), p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt to .*/dummy.txt2', p'run_job: git .* show :0:dummy.txt2', - 'update(1): updates: 2, jobs: 11' + 'update(1): updates: 2, jobs: 10' } eq({[1] = test_file2}, get_bufs()) command('Gitsigns clear_debug') + local test_file3 = test_file..'3' git{'mv', test_file2, test_file3} match_debug_messages { @@ -94,7 +91,7 @@ describe('gitdir_watcher', function() p('run_job: git .* ls%-files .* '..helpers.pesc(test_file3)), p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt2 to .*/dummy.txt3', p'run_job: git .* show :0:dummy.txt3', - 'update(1): updates: 3, jobs: 16' + 'update(1): updates: 3, jobs: 15' } eq({[1] = test_file3}, get_bufs()) @@ -113,7 +110,7 @@ describe('gitdir_watcher', function() p('run_job: git .* ls%-files .* '..helpers.pesc(test_file)), p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt3 to .*/dummy.txt', p'run_job: git .* show :0:dummy.txt', - 'update(1): updates: 4, jobs: 22' + 'update(1): updates: 4, jobs: 21' } eq({[1] = test_file}, get_bufs()) diff --git a/test/gitsigns_spec.lua b/test/gitsigns_spec.lua index 20f78ca8..d44772ec 100644 --- a/test/gitsigns_spec.lua +++ b/test/gitsigns_spec.lua @@ -83,18 +83,18 @@ describe('gitsigns', function() -- Don't set this too low, or else the test will lock up config.watch_gitdir = {interval = 100} setup_gitsigns(config) + command('Gitsigns clear_debug') edit(test_file) expectf(function() match_dag(debug_messages(), { - p'run_job: git .* %-%-version', - 'attach(1): Attaching (trigger=BufRead)', + 'attach(1): Attaching (trigger=BufReadPost)', p'run_job: git .* config user.name', p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', p('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..helpers.pesc(test_file)), 'watch_gitdir(1): Watching git dir', p'run_job: git .* show :0:dummy.txt', - 'update(1): updates: 1, jobs: 7' + 'update(1): updates: 1, jobs: 6' }) end) @@ -118,7 +118,7 @@ describe('gitsigns', function() edit(tmpfile) match_debug_messages { - 'attach(1): Attaching (trigger=BufRead)', + 'attach(1): Attaching (trigger=BufReadPost)', p'run_job: git .* config user.name', p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', 'new: Not in git repo', @@ -167,7 +167,7 @@ describe('gitsigns', function() edit(scratch..'/.git/index') match_debug_messages { - 'attach(1): Attaching (trigger=BufRead)', + 'attach(1): Attaching (trigger=BufReadPost)', 'new: In git dir', 'attach(1): Empty git obj' } @@ -183,7 +183,7 @@ describe('gitsigns', function() edit(ignored_file) match_debug_messages { - 'attach(1): Attaching (trigger=BufRead)', + 'attach(1): Attaching (trigger=BufReadPost)', p'run_job: git .* config user.name', p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', p'run_job: git .* ls%-files .*/dummy_ignored.txt', @@ -225,7 +225,7 @@ describe('gitsigns', function() command("Gitsigns clear_debug") command("copen") match_debug_messages { - 'attach(2): Attaching (trigger=BufRead)', + 'attach(2): Attaching (trigger=BufReadPost)', 'attach(2): Non-normal buffer', } end) @@ -349,7 +349,7 @@ describe('gitsigns', function() edit(test_file) match_debug_messages { - 'attach(1): Attaching (trigger=BufRead)', + 'attach(1): Attaching (trigger=BufReadPost)', p'run_job: git .* config user.name', p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD', p'run_job: git .* rev%-parse %-%-short HEAD', @@ -484,7 +484,7 @@ describe('gitsigns', function() table.insert(messages, p'run_job: git .* diff .* /tmp/lua_.* /tmp/lua_.*') end - local jobs = internal_diff and 9 or 10 + local jobs = internal_diff and 8 or 9 table.insert(messages, "update(1): updates: 1, jobs: "..jobs) match_debug_messages(messages) diff --git a/test/gs_helpers.lua b/test/gs_helpers.lua index 42ad4d65..95ee0428 100644 --- a/test/gs_helpers.lua +++ b/test/gs_helpers.lua @@ -21,6 +21,7 @@ M.newfile = M.scratch.."/newfile.txt" M.test_config = { debug_mode = true, + _test_mode = true, signs = { add = {hl = 'DiffAdd' , text = '+'}, delete = {hl = 'DiffDelete', text = '_'}, @@ -47,15 +48,6 @@ local test_file_text = { 'content', 'doesn\'t', 'matter,', 'it', 'just', 'needs', 'to', 'be', 'static.' } ---- Escapes magic chars in |lua-patterns|. ---- ----@see https://github.com/rxi/lume ----@param s string String to escape ----@return string %-escaped pattern string -function M.pesc(s) - return (s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')) -end - function M.git(args) system{"git", "-C", M.scratch, unpack(args)} end @@ -64,7 +56,6 @@ function M.cleanup() system{"rm", "-rf", M.scratch} end - function M.setup_git() M.git{"init", '-b', 'master'} @@ -105,7 +96,8 @@ function M.expectf(cond, interval) local duration = 0 interval = interval or 1 while duration < timeout do - if pcall(cond) then + local ok, ret = pcall(cond) + if ok and (ret == nil or ret == true) then return end duration = duration + interval @@ -124,7 +116,7 @@ function M.edit(path) end function M.write_to_file(path, text) - local f = io.open(path, 'wb') + local f = assert(io.open(path, 'wb')) for _, l in ipairs(text) do f:write(l) f:write('\n') @@ -132,6 +124,13 @@ function M.write_to_file(path, text) f:close() end +local function spec_text(s) + if type(s) == 'table' then + return s.text + end + return s +end + function M.match_lines(lines, spec) local i = 1 for lid, line in ipairs(lines) do @@ -153,12 +152,13 @@ function M.match_lines(lines, spec) i = i + 1 end end + if i < #spec + 1 then local msg = {'lines:'} for _, l in ipairs(lines) do - msg[#msg+1] = string.format( '"%s"', l) + msg[#msg+1] = string.format(' - "%s"', l) end - error(('Did not match pattern \'%s\' with %s'):format(spec[i], table.concat(msg, '\n'))) + error(('Did not match pattern \'%s\' with %s'):format(spec_text(spec[i]), table.concat(msg, '\n'))) end end @@ -209,7 +209,7 @@ function M.n(str) end function M.debug_messages() - return exec_lua("return require'gitsigns'.debug_messages(true)") + return exec_lua("return require'gitsigns.debug.log'.messages") end function M.match_dag(lines, spec) @@ -224,6 +224,8 @@ function M.match_debug_messages(spec) end) end +local git_version + function M.setup_gitsigns(config, extra) extra = extra or '' exec_lua([[ @@ -232,7 +234,7 @@ function M.setup_gitsigns(config, extra) require('gitsigns').setup(...) ]], config) M.expectf(function() - exec_capture('au gitsigns') + return exec_lua[[return require'gitsigns'._setup_done == true]] end) end diff --git a/test/highlights_spec.lua b/test/highlights_spec.lua index 63f0c160..7cb73762 100644 --- a/test/highlights_spec.lua +++ b/test/highlights_spec.lua @@ -58,6 +58,7 @@ describe('highlights', function() config.signs.topdelete.hl = nil config.numhl = true config.linehl = true + config._test_mode = true exec_lua('gs.setup(...)', config) 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}