diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2475da1..60dbafd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: true matrix: - neovim_branch: ['v0.8.3', 'nightly'] + neovim_branch: ['v0.8.3', 'v0.9.1', 'nightly'] runs-on: ubuntu-latest env: NEOVIM_BRANCH: ${{ matrix.neovim_branch }} @@ -56,11 +56,5 @@ 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/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..a2b3447 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 100 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferSingle" +call_parentheses = "Always" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6eb7cae..ef89ce0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,23 +3,9 @@ - [Luarocks](https://luarocks.org/) - `brew install luarocks` -## Writing Teal - - **Do not edit files in the lua dir**. - -Gitsigns is implemented in teal which is essentially lua+types. -The teal source files are generated into lua files and must be checked in together when making changes. -CI will enforce this. - -Once you have made changes in teal, the corresponding lua files can be built with: - -``` -make tl-build -``` - ## Generating docs -Most of the documentation is handwritten however the documentation for the configuration is generated from `teal/gitsigns/config.tl` which contains the configuration schema. +Most of the documentation is handwritten however the documentation for the configuration is generated from `lua/gitsigns/config.lua` which contains the configuration schema. The documentation is generated with the lua script `gen_help.lua` which has been developed just enough to handle the current configuration schema so from time to time this script might need small improvements to handle new features but for the most part it works. The documentation can be updated with: @@ -46,40 +32,3 @@ To run the testsuite: ``` make test ``` - -## [Diagnostic-ls](https://github.com/iamcco/diagnostic-languageserver) config for teal - -``` -require('lspconfig').diagnosticls.setup{ - filetypes = {'teal'}, - init_options = { - filetypes = {teal = {'tealcheck'}}, - linters = { - tealcheck = { - sourceName = "tealcheck", - command = "tl", - args = {'check', '%file'}, - isStdout = false, isStderr = true, - rootPatterns = {"tlconfig.lua", ".git"}, - formatPattern = { - '^([^:]+):(\\d+):(\\d+): (.+)$', { - sourceName = 1, sourceNameFilter = true, - line = 2, column = 3, message = 4 - } - } - } - } - } -} -``` - -## [null-ls.nvim](https://github.com/jose-elias-alvarez/null-ls.nvim) config for teal - -``` -local null_ls = require("null-ls") - -null_ls.config {sources = { - null_ls.builtins.diagnostics.teal -}} -require("lspconfig")["null-ls"].setup {} -``` diff --git a/Makefile b/Makefile index 2610841..462cb62 100644 --- a/Makefile +++ b/Makefile @@ -4,13 +4,12 @@ export PJ_ROOT=$(PWD) FILTER ?= .* LUA_VERSION := 5.1 -TL_VERSION := 0.14.1 NEOVIM_BRANCH ?= master DEPS_DIR := $(PWD)/deps/nvim-$(NEOVIM_BRANCH) NVIM_DIR := $(DEPS_DIR)/neovim -LUAROCKS := $(DEPS_DIR)/luarocks/usr/bin/luarocks +LUAROCKS := luarocks LUAROCKS_TREE := $(DEPS_DIR)/luarocks/usr LUAROCKS_LPATH := $(LUAROCKS_TREE)/share/lua/$(LUA_VERSION) LUAROCKS_INIT := eval $$($(LUAROCKS) --tree $(LUAROCKS_TREE) path) && @@ -26,17 +25,12 @@ $(NVIM_DIR): CMAKE_BUILD_TYPE=RelWithDebInfo \ CMAKE_EXTRA_FLAGS='-DCI_BUILD=OFF -DENABLE_LTO=OFF' -TL := $(LUAROCKS_TREE)/bin/tl - -$(TL): $(NVIM_DIR) - @mkdir -p $$(dirname $@) - $(LUAROCKS) --tree $(LUAROCKS_TREE) install tl $(TL_VERSION) - INSPECT := $(LUAROCKS_LPATH)/inspect.lua $(INSPECT): $(NVIM_DIR) @mkdir -p $$(dirname $@) $(LUAROCKS) --tree $(LUAROCKS_TREE) install inspect + touch $@ LUV := $(LUAROCKS_TREE)/lib/lua/$(LUA_VERSION)/luv.so @@ -45,7 +39,7 @@ $(LUV): $(NVIM_DIR) $(LUAROCKS) --tree $(LUAROCKS_TREE) install luv .PHONY: lua_deps -lua_deps: $(TL) $(INSPECT) +lua_deps: $(INSPECT) .PHONY: test_deps test_deps: $(NVIM_DIR) @@ -73,24 +67,10 @@ test: $(NVIM_DIR) -@stty sane -.PHONY: tl-check -tl-check: $(TL) - $(TL) check teal/*.tl teal/**/*.tl - -.PHONY: tl-build -tl-build: tlconfig.lua $(TL) $(LUV) - @$(TL) build - @$(LUAROCKS_INIT) ./etc/add_comments.lua - @echo Updated lua files - .PHONY: gen_help gen_help: $(INSPECT) @$(LUAROCKS_INIT) ./gen_help.lua @echo Updated help .PHONY: build -build: tl-build gen_help - -.PHONY: tl-ensure -tl-ensure: tl-build - git diff --exit-code -- lua +build: gen_help diff --git a/README.md b/README.md index 90dfb9e..8b8586e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Gitter](https://badges.gitter.im/gitsigns-nvim/community.svg)](https://gitter.im/gitsigns-nvim/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -Super fast git decorations implemented purely in lua/teal. +Super fast git decorations implemented purely in Lua. ## Preview diff --git a/doc/gitsigns.txt b/doc/gitsigns.txt index 13f6df5..5f058d3 100644 --- a/doc/gitsigns.txt +++ b/doc/gitsigns.txt @@ -594,11 +594,11 @@ on_attach *gitsigns-config-on_attach* watch_gitdir *gitsigns-config-watch_gitdir* Type: `table[extended]` Default: > - { + `{ enable = true, - interval = 1000, - follow_files = true - } + follow_files = true, + interval = 1000 + }` < When opening a file, a libuv watcher is placed on the respective `.git` directory to detect when changes happen to use as a trigger to @@ -685,18 +685,9 @@ base *gitsigns-config-base* count_chars *gitsigns-config-count_chars* Type: `table` Default: > - { - [1] = '1', -- '₁', - [2] = '2', -- '₂', - [3] = '3', -- '₃', - [4] = '4', -- '₄', - [5] = '5', -- '₅', - [6] = '6', -- '₆', - [7] = '7', -- '₇', - [8] = '8', -- '₈', - [9] = '9', -- '₉', - ['+'] = '>', -- '₊', - } + `{ "1", "2", "3", "4", "5", "6", "7", "8", "9", + ["+"] = ">" + }` < The count characters used when `signs.*.show_count` is enabled. The `+` entry is used as a fallback. With the default, any count outside @@ -728,13 +719,13 @@ max_file_length *gitsigns-config-max_file_length* preview_config *gitsigns-config-preview_config* Type: `table[extended]` Default: > - { - border = 'single', - style = 'minimal', - relative = 'cursor', - row = 0, - col = 1 - } + `{ + border = "single", + col = 1, + relative = "cursor", + row = 0, + style = "minimal" + }` < Option overrides for the Gitsigns preview window. Table is passed directly to `nvim_open_win`. @@ -760,12 +751,12 @@ current_line_blame *gitsigns-config-current_line_blame* current_line_blame_opts *gitsigns-config-current_line_blame_opts* Type: `table[extended]` Default: > - { + `{ + delay = 1000, virt_text = true, - virt_text_pos = 'eol', - virt_text_priority = 100, - delay = 1000 - } + virt_text_pos = "eol", + virt_text_priority = 100 + }` < Options for the current line blame annotation. @@ -792,9 +783,9 @@ current_line_blame_formatter_opts Type: `table[extended]` Default: > - { - relative_time = false - } + `{ + relative_time = false + }` < Options for the current line blame annotation formatter. @@ -802,7 +793,7 @@ current_line_blame_formatter_opts • relative_time: boolean current_line_blame_formatter *gitsigns-config-current_line_blame_formatter* - Type: `string|function`, Default: `' , - '` + Type: `string|function`, Default: `" , - "` String or function used to format the virtual text of |gitsigns-config-current_line_blame|. @@ -880,7 +871,7 @@ current_line_blame_formatter *gitsigns-config-current_line_blame_formatter* current_line_blame_formatter_nc *gitsigns-config-current_line_blame_formatter_nc* - Type: `string|function`, Default: `' '` + Type: `string|function`, Default: `" "` String or function used to format the virtual text of |gitsigns-config-current_line_blame| for lines that aren't committed. @@ -894,8 +885,12 @@ trouble *gitsigns-config-trouble* quickfix/location list window. yadm *gitsigns-config-yadm* - Type: `table`, Default: `{ enable = false }` - + Type: `table` + Default: > + `{ + enable = false + }` +< yadm configuration. word_diff *gitsigns-config-word_diff* diff --git a/etc/add_comments.lua b/etc/add_comments.lua deleted file mode 100755 index d163137..0000000 --- a/etc/add_comments.lua +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/sh -_=[[ -exec luajit "$0" "$@" -]] - -local uv = require'luv' - -local function read_file(path) - local f = assert(io.open(path, 'r')) - local t = f:read("*all") - f:close() - return t -end - -local function join_paths(...) - return table.concat({ ... }, '/'):gsub('//+', '/') -end - -local function dir(path) - --- @async - return coroutine.wrap(function() - local dirs = { { path, 1 } } - while #dirs > 0 do - local dir0, level = unpack(table.remove(dirs, 1)) - local dir1 = level == 1 and dir0 or join_paths(path, dir0) - local fs = uv.fs_scandir(dir1) - while fs do - local name, t = uv.fs_scandir_next(fs) - if not name then - break - end - local f = level == 1 and name or join_paths(dir0, name) - if t == 'directory' then - dirs[#dirs + 1] = { f, level + 1 } - else - coroutine.yield(f, t) - end - end - end - end) -end - -local function write_file(path, lines) - local f = assert(io.open(path, 'w')) - f:write(table.concat(lines, '\n')) - f:close() -end - -local function read_file_lines(path) - local lines = {} - for l in read_file(path):gmatch("([^\n]*)\n?") do - table.insert(lines, l) - end - return lines -end - -for p in dir('teal') do - local path = join_paths('teal', p) - local op = p:gsub('%.tl$', '.lua') - local opath = join_paths('lua', op) - - local lines = read_file_lines(path) - - local comments = {} - for i, l in ipairs(lines) do - local comment = l:match('%s*%-%-.*') - if comment then - comments[i] = comment:gsub(' ', ' ') - end - end - - local olines = read_file_lines(opath) - - for i, l in pairs(comments) do - if not olines[i]:match('%-%-.*') then - olines[i] = olines[i]..l - end - end - write_file(opath, olines) -end - diff --git a/gen_help.lua b/gen_help.lua index 8a509f8..74d5271 100755 --- a/gen_help.lua +++ b/gen_help.lua @@ -15,10 +15,6 @@ function table.slice(tbl, first, last, step) return sliced end -local function is_simple_type(t) - return t == 'number' or t == 'string' or t == 'boolean' -end - local function startswith(str, start) return str.sub(str, 1, string.len(start)) == start end @@ -30,18 +26,11 @@ local function read_file(path) return t end -local function read_file_lines(path) - local lines = {} - for l in read_file(path):gmatch("([^\n]*)\n?") do - table.insert(lines, l) - end - return lines -end - -- To make sure the output is consistent between runs (to minimise diffs), we -- need to iterate through the schema keys in a deterministic way. To do this we -- do a smple scan over the file the schema is defined in and collect the keys -- in the order they are defined. +--- @return string[] local function get_ordered_schema_keys() local c = read_file('lua/gitsigns/config.lua') @@ -58,7 +47,7 @@ local function get_ordered_schema_keys() if startswith(l, '}') then break end - if l:find('^ (%w+).*') then + if l:find('^ (%w+).*') then local lc = l:gsub('^%s*([%w_]+).*', '%1') table.insert(keys, lc) end @@ -67,52 +56,6 @@ local function get_ordered_schema_keys() return keys end -local function get_default(field) - local cfg = read_file_lines('teal/gitsigns/config.tl') - - local fs, fe - for i = 1, #cfg do - local l = cfg[i] - if l:match('^ '..field..' =') then - fs = i - end - if fs and l:match('^ }') then - fe = i - break - end - end - - local ds, de - for i = fs, fe do - local l = cfg[i] - if l:match('^ default =') then - ds = i - if l:match('},') or l:match('nil,') or l:match("default = '.*'") then - de = i - break - end - end - if ds and l:match('^ }') then - de = i - break - end - end - - local ret = {} - for i = ds, de do - local l = cfg[i] - if i == ds then - l = l:gsub('%s*default = ', '') - end - if i == de then - l = l:gsub('(.*),', '%1') - end - table.insert(ret, l) - end - - return table.concat(ret, '\n') -end - local function gen_config_doc_deprecated(dep_info, out) if type(dep_info) == 'table' and dep_info.hard then out(' HARD-DEPRECATED') @@ -153,19 +96,12 @@ local function gen_config_doc_field(field, out) end if v.description then - local d + local d --- @type string if v.default_help ~= nil then d = v.default_help - elseif is_simple_type(v.type) then - d = inspect(v.default) - d = ('`%s`'):format(d) else - d = get_default(field) - if d:find('\n') then - d = d:gsub('\n([^\n\r])', '\n%1') - else - d = ('`%s`'):format(d) - end + d = inspect(v.default):gsub('\n', '\n ') + d = ('`%s`'):format(d) end local vtype = (function() @@ -192,8 +128,9 @@ local function gen_config_doc_field(field, out) end end +--- @return string local function gen_config_doc() - local res = {} + local res = {} ---@type string[] local function out(line) res[#res+1] = line or '' end @@ -203,6 +140,8 @@ local function gen_config_doc() return table.concat(res, '\n') end +--- @param line string +--- @return string local function parse_func_header(line) local func = line:match('%w+%.([%w_]+)') if not func then @@ -212,7 +151,7 @@ local function parse_func_header(line) line:match('function%((.*)%)') or -- M.name = function(args) line:match('function%s+%w+%.[%w_]+%((.*)%)') -- function M.name(args) local args = {} - for k in string.gmatch(args_raw, "([%w_]+):") do + for k in string.gmatch(args_raw, "([%w_]+)") do if k:sub(1, 1) ~= '_' then args[#args+1] = string.format('{%s}', k) end @@ -224,6 +163,8 @@ local function parse_func_header(line) ) end +--- @param path string +--- @return string local function gen_functions_doc_from_file(path) local i = read_file(path):gmatch("([^\n]*)\n?") @@ -262,6 +203,8 @@ local function gen_functions_doc_from_file(path) return table.concat(res, '\n') end +--- @param files string[] +--- @return string local function gen_functions_doc(files) local res = '' for _, path in ipairs(files) do @@ -270,8 +213,9 @@ local function gen_functions_doc(files) return res end +--- @return string local function gen_highlights_doc() - local res = {} + local res = {} --- @type string[] local highlights = require('lua.gitsigns.highlight') local name_max = 0 @@ -286,12 +230,11 @@ local function gen_highlights_doc() for _, hl in ipairs(highlights.hls) do for name, spec in pairs(hl) do if not spec.hidden then - local fallbacks_tbl = {} + local fallbacks_tbl = {} --- @type string[] for _, f in ipairs(spec) do fallbacks_tbl[#fallbacks_tbl+1] = string.format('`%s`', f) end local fallbacks = table.concat(fallbacks_tbl, ', ') - local pad = string.rep(' ', name_max - name:len()) res[#res+1] = string.format('%s*hl-%s*', string.rep(' ', 56), name) res[#res+1] = string.format('%s', name) if spec.desc then @@ -306,11 +249,12 @@ local function gen_highlights_doc() return table.concat(res, '\n') end +--- @return string local function get_setup_from_readme() local i = read_file('README.md'):gmatch("([^\n]*)\n?") - local res = {} + local res = {} --- @type string[] local function append(line) - res[#res+1] = line ~= '' and ' '..line or '' + res[#res+1] = line ~= '' and ' '..line or '' end for l in i do if l:match("require%('gitsigns'%).setup {") then @@ -332,14 +276,16 @@ end local function get_marker_text(marker) return ({ VERSION = '0.7-dev', - CONFIG = gen_config_doc, - FUNCTIONS = gen_functions_doc{ - 'teal/gitsigns.tl', - 'teal/gitsigns/attach.tl', - 'teal/gitsigns/actions.tl', - }, - HIGHLIGHTS = gen_highlights_doc, - SETUP = get_setup_from_readme + CONFIG = function() return gen_config_doc() end, + FUNCTIONS = function() + return gen_functions_doc{ + 'lua/gitsigns.lua', + 'lua/gitsigns/attach.lua', + 'lua/gitsigns/actions.lua', + } + end, + HIGHLIGHTS = function() return gen_highlights_doc() end, + SETUP = function() return get_setup_from_readme() end, })[marker] end diff --git a/gitsigns.nvim-scm-1.rockspec b/gitsigns.nvim-scm-1.rockspec index d3a6fa9..773609e 100644 --- a/gitsigns.nvim-scm-1.rockspec +++ b/gitsigns.nvim-scm-1.rockspec @@ -7,7 +7,7 @@ version = _MODREV .. _SPECREV description = { summary = 'Git signs written in pure lua', detailed = [[ - Super fast git decorations implemented purely in lua/teal. + Super fast git decorations implemented purely in Lua. ]], homepage = 'http://github.com/lewis6991/gitsigns.nvim', license = 'MIT/X11', diff --git a/lua/README.md b/lua/README.md deleted file mode 100644 index 2fc5ae8..0000000 --- a/lua/README.md +++ /dev/null @@ -1,3 +0,0 @@ -**WARNING**: Do not edit the files in this directory. The files are generated from [teal](https://github.com/teal-language/tl). For the original source files please look in the [teal](../teal) directory. - -See [Makefile](../Makefile) for targets on handling the teal files. diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index 02b31f2..7940ed8 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -2,7 +2,6 @@ local void = require('gitsigns.async').void local scheduler = require('gitsigns.async').scheduler local gs_config = require('gitsigns.config') -local Config = gs_config.Config local config = gs_config.config local log = require('gitsigns.debug.log') @@ -14,193 +13,186 @@ local uv = require('gitsigns.uv') local M = {} - - -- from attach.tl - - - - - local cwd_watcher local update_cwd_head = void(function() - local paths = vim.fs.find('.git', { - limit = 1, - upward = true, - type = 'directory', - }) + local paths = vim.fs.find('.git', { + limit = 1, + upward = true, + type = 'directory', + }) - if #paths == 0 then - return - end + if #paths == 0 then + return + end - if cwd_watcher then - cwd_watcher:stop() - else - cwd_watcher = uv.new_fs_poll(true) - end + if cwd_watcher then + cwd_watcher:stop() + else + cwd_watcher = uv.new_fs_poll(true) + end - local cwd = vim.loop.cwd() - local gitdir, head + local cwd = vim.loop.cwd() + local gitdir, head - local gs_cache = require('gitsigns.cache') + local gs_cache = require('gitsigns.cache') - -- 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 + -- 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 - local git = require('gitsigns.git') + local git = require('gitsigns.git') - if not head or not gitdir then - local info = git.get_repo_info(cwd) - gitdir = info.gitdir - head = info.abbrev_head - 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 + scheduler() + vim.g.gitsigns_head = head - if not gitdir then - return - end + if not gitdir then + return + end - local towatch = gitdir .. '/HEAD' + local towatch = gitdir .. '/HEAD' - if cwd_watcher:getpath() == towatch then - -- Already watching - return - end + 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) + -- 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 + 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) + ) end) local function setup_cli() - api.nvim_create_user_command('Gitsigns', function(params) - require('gitsigns.cli').run(params) - end, { - force = true, - nargs = '*', - range = true, - complete = function(arglead, line) - return require('gitsigns.cli').complete(arglead, line) - end, }) + api.nvim_create_user_command('Gitsigns', function(params) + require('gitsigns.cli').run(params) + end, { + force = true, + nargs = '*', + range = true, + complete = function(arglead, line) + return require('gitsigns.cli').complete(arglead, line) + end, + }) end local exported = { - 'attach', - 'actions', + 'attach', + 'actions', } local function setup_debug() - log.debug_mode = config.debug_mode - log.verbose = config._verbose + log.debug_mode = config.debug_mode + log.verbose = config._verbose - if config.debug_mode then - exported[#exported + 1] = 'debug' - end + if config.debug_mode then + exported[#exported + 1] = 'debug' + end end local function setup_attach() - scheduler() + 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 + -- 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_autocmd({ 'BufRead', 'BufNewFile', 'BufWritePost' }, { - group = 'gitsigns', - callback = function(data) - M.attach(nil, nil, data.event) - end, - }) + api.nvim_create_autocmd({ 'BufRead', 'BufNewFile', 'BufWritePost' }, { + group = 'gitsigns', + callback = function(data) + M.attach(nil, nil, data.event) + end, + }) 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, - }) + 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. --- --- Attributes: ~ ---- {async} +--- {async} --- --- Parameters: ~ ---- {cfg} Table object containing configuration for ---- Gitsigns. See |gitsigns-usage| for more details. +--- {cfg} Table object containing configuration for +--- Gitsigns. See |gitsigns-usage| for more details. M.setup = void(function(cfg) - gs_config.build(cfg) + gs_config.build(cfg) - if vim.fn.executable('git') == 0 then - print('gitsigns: git not in path. Aborting setup') - return - end + if vim.fn.executable('git') == 0 then + 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 + 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 - setup_debug() - setup_cli() + setup_debug() + setup_cli() - api.nvim_create_augroup('gitsigns', {}) + api.nvim_create_augroup('gitsigns', {}) - if config._test_mode then - require('gitsigns.attach')._setup() - require('gitsigns.git')._set_version(config._git_version) - end + if config._test_mode then + require('gitsigns.attach')._setup() + require('gitsigns.git')._set_version(config._git_version) + end - setup_attach() - setup_cwd_head() + setup_attach() + setup_cwd_head() - M._setup_done = true + M._setup_done = true end) return setmetatable(M, { - __index = function(_, f) - for _, mod in ipairs(exported) do - local m = (require)('gitsigns.' .. mod) - if m[f] then - return m[f] - end + __index = function(_, f) + for _, mod in ipairs(exported) do + local m = (require)('gitsigns.' .. mod) + if m[f] then + return m[f] end - end, + end + end, }) diff --git a/lua/gitsigns/actions.lua b/lua/gitsigns/actions.lua index f6a29fa..1034103 100644 --- a/lua/gitsigns/actions.lua +++ b/lua/gitsigns/actions.lua @@ -20,280 +20,206 @@ local Hunk_Public = gs_hunks.Hunk_Public local api = vim.api local current_buf = api.nvim_get_current_buf - - - - - - - - - - - - - - - - - - - - - - - - - - - -local M = {QFListOpts = {}, } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +local M = { QFListOpts = {} } -- Variations of functions from M which are used for the Gitsigns command local C = {} - - local CP = {} local ns_inline = api.nvim_create_namespace('gitsigns_preview_inline') local function complete_heads(arglead) - local all = vim.fn.systemlist({ 'git', 'rev-parse', '--symbolic', '--branches', '--tags', '--remotes' }) - return vim.tbl_filter(function(x) - return vim.startswith(x, arglead) - end, all) + local all = + vim.fn.systemlist({ 'git', 'rev-parse', '--symbolic', '--branches', '--tags', '--remotes' }) + return vim.tbl_filter(function(x) + return vim.startswith(x, arglead) + end, all) end --- Toggle |gitsigns-config-signbooleancolumn| --- --- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. --- --- Returns:~ ---- Current value of |gitsigns-config-signcolumn| +--- Current value of |gitsigns-config-signcolumn| M.toggle_signs = function(value) - if value ~= nil then - config.signcolumn = value - else - config.signcolumn = not config.signcolumn - end - M.refresh() - return config.signcolumn + if value ~= nil then + config.signcolumn = value + else + config.signcolumn = not config.signcolumn + end + M.refresh() + return config.signcolumn end --- Toggle |gitsigns-config-numhl| --- --- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. --- --- Returns:~ ---- Current value of |gitsigns-config-numhl| +--- Current value of |gitsigns-config-numhl| M.toggle_numhl = function(value) - if value ~= nil then - config.numhl = value - else - config.numhl = not config.numhl - end - M.refresh() - return config.numhl + if value ~= nil then + config.numhl = value + else + config.numhl = not config.numhl + end + M.refresh() + return config.numhl end --- Toggle |gitsigns-config-linehl| --- --- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. --- --- Returns:~ ---- Current value of |gitsigns-config-linehl| +--- Current value of |gitsigns-config-linehl| M.toggle_linehl = function(value) - if value ~= nil then - config.linehl = value - else - config.linehl = not config.linehl - end - M.refresh() - return config.linehl + if value ~= nil then + config.linehl = value + else + config.linehl = not config.linehl + end + M.refresh() + return config.linehl end --- Toggle |gitsigns-config-word_diff| --- --- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. --- --- Returns:~ ---- Current value of |gitsigns-config-word_diff| +--- Current value of |gitsigns-config-word_diff| M.toggle_word_diff = function(value) - if value ~= nil then - config.word_diff = value - else - config.word_diff = not config.word_diff - end - -- Don't use refresh() to avoid flicker - api.nvim__buf_redraw_range(0, vim.fn.line('w0') - 1, vim.fn.line('w$')) - return config.word_diff + if value ~= nil then + config.word_diff = value + else + config.word_diff = not config.word_diff + end + -- Don't use refresh() to avoid flicker + api.nvim__buf_redraw_range(0, vim.fn.line('w0') - 1, vim.fn.line('w$')) + return config.word_diff end --- Toggle |gitsigns-config-current_line_blame| --- --- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. --- --- Returns:~ ---- Current value of |gitsigns-config-current_line_blame| +--- Current value of |gitsigns-config-current_line_blame| M.toggle_current_line_blame = function(value) - if value ~= nil then - config.current_line_blame = value - else - config.current_line_blame = not config.current_line_blame - end - M.refresh() - return config.current_line_blame + if value ~= nil then + config.current_line_blame = value + else + config.current_line_blame = not config.current_line_blame + end + M.refresh() + return config.current_line_blame end --- Toggle |gitsigns-config-show_deleted| --- --- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. +--- {value} boolean|nil Value to set toggle. If `nil` +--- the toggle value is inverted. --- --- Returns:~ ---- Current value of |gitsigns-config-show_deleted| +--- Current value of |gitsigns-config-show_deleted| M.toggle_deleted = function(value) - if value ~= nil then - config.show_deleted = value - else - config.show_deleted = not config.show_deleted - end - M.refresh() - return config.show_deleted + if value ~= nil then + config.show_deleted = value + else + config.show_deleted = not config.show_deleted + end + M.refresh() + return config.show_deleted end local function get_cursor_hunk(bufnr, hunks) - bufnr = bufnr or current_buf() + bufnr = bufnr or current_buf() - if not hunks then - hunks = {} - vim.list_extend(hunks, cache[bufnr].hunks or {}) - vim.list_extend(hunks, cache[bufnr].hunks_staged or {}) - end + if not hunks then + hunks = {} + vim.list_extend(hunks, cache[bufnr].hunks or {}) + vim.list_extend(hunks, cache[bufnr].hunks_staged or {}) + end - local lnum = api.nvim_win_get_cursor(0)[1] - return gs_hunks.find_hunk(lnum, hunks) + local lnum = api.nvim_win_get_cursor(0)[1] + return gs_hunks.find_hunk(lnum, hunks) end local function update(bufnr) - manager.update(bufnr) - scheduler() - if vim.wo.diff then - require('gitsigns.diffthis').update(bufnr) - end + manager.update(bufnr) + scheduler() + if vim.wo.diff then + require('gitsigns.diffthis').update(bufnr) + end end local function get_range(params) - local range - if params.range > 0 then - range = { params.line1, params.line2 } - end - return range + local range + if params.range > 0 then + range = { params.line1, params.line2 } + end + return range end local function get_hunks(bufnr, bcache, greedy, staged) - local hunks + local hunks - if greedy then - -- Re-run the diff without linematch - local buftext = util.buf_lines(bufnr) - local text - if staged then - text = bcache.compare_text_head - else - text = bcache.compare_text - end - if text then - hunks = run_diff(text, buftext, false) - end - scheduler() - else - if staged then - hunks = bcache.hunks_staged - else - hunks = bcache.hunks - end - end + if greedy then + -- Re-run the diff without linematch + local buftext = util.buf_lines(bufnr) + local text + if staged then + text = bcache.compare_text_head + else + text = bcache.compare_text + end + if text then + hunks = run_diff(text, buftext, false) + end + scheduler() + else + if staged then + hunks = bcache.hunks_staged + else + hunks = bcache.hunks + end + end - return hunks + return hunks end local function get_hunk(bufnr, range, greedy, staged) - local bcache = cache[bufnr] - local hunks = get_hunks(bufnr, bcache, greedy, staged) - local hunk - if range then - table.sort(range) - local top, bot = range[1], range[2] - hunk = gs_hunks.create_partial_hunk(hunks, top, bot) - hunk.added.lines = api.nvim_buf_get_lines(bufnr, top - 1, bot, false) - hunk.removed.lines = vim.list_slice( + local bcache = cache[bufnr] + local hunks = get_hunks(bufnr, bcache, greedy, staged) + local hunk + if range then + table.sort(range) + local top, bot = range[1], range[2] + hunk = gs_hunks.create_partial_hunk(hunks, top, bot) + hunk.added.lines = api.nvim_buf_get_lines(bufnr, top - 1, bot, false) + hunk.removed.lines = vim.list_slice( bcache.compare_text, hunk.removed.start, - hunk.removed.start + hunk.removed.count - 1) - - else - hunk = get_cursor_hunk(bufnr, hunks) - end - return hunk + hunk.removed.start + hunk.removed.count - 1 + ) + else + hunk = get_cursor_hunk(bufnr, hunks) + end + return hunk end --- Stage the hunk at the cursor position, or all lines in the @@ -303,53 +229,53 @@ end --- lines within the range will be staged. --- --- Attributes: ~ ---- {async} +--- {async} --- --- Parameters:~ ---- {range} table|nil List-like table of two integers making ---- up the line range from which you want to stage the hunks. ---- If running via command line, then this is taken from the ---- command modifiers. ---- {opts} table|nil Additional options: ---- • {greedy}: (boolean) ---- Stage all contiguous hunks. Only useful if 'diff_opts' ---- contains `linematch`. Defaults to `true`. +--- {range} table|nil List-like table of two integers making +--- up the line range from which you want to stage the hunks. +--- If running via command line, then this is taken from the +--- command modifiers. +--- {opts} table|nil Additional options: +--- • {greedy}: (boolean) +--- Stage all contiguous hunks. Only useful if 'diff_opts' +--- contains `linematch`. Defaults to `true`. --- M.stage_hunk = mk_repeatable(void(function(range, opts) - opts = opts or {} - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + opts = opts or {} + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - if not util.path_exists(bcache.file) then - print("Error: Cannot stage lines. Please add the file to the working tree.") - return - end + if not util.path_exists(bcache.file) then + print('Error: Cannot stage lines. Please add the file to the working tree.') + return + end - local hunk = get_hunk(bufnr, range, opts.greedy ~= false, false) + local hunk = get_hunk(bufnr, range, opts.greedy ~= false, false) - local invert = false - if not hunk then - invert = true - hunk = get_hunk(bufnr, range, opts.greedy ~= false, true) - end + local invert = false + if not hunk then + invert = true + hunk = get_hunk(bufnr, range, opts.greedy ~= false, true) + end - if not hunk then - return - end + if not hunk then + return + end - bcache.git_obj:stage_hunks({ hunk }, invert) + bcache.git_obj:stage_hunks({ hunk }, invert) - table.insert(bcache.staged_diffs, hunk) + table.insert(bcache.staged_diffs, hunk) - bcache:invalidate() - update(bufnr) + bcache:invalidate() + update(bufnr) end)) C.stage_hunk = function(_, params) - M.stage_hunk(get_range(params)) + M.stage_hunk(get_range(params)) end --- Reset the lines of the hunk at the cursor position, or all @@ -359,53 +285,53 @@ end --- hunk, only the lines within the range will be reset. --- --- Parameters:~ ---- {range} table|nil List-like table of two integers making ---- up the line range from which you want to reset the hunks. ---- If running via command line, then this is taken from the ---- command modifiers. ---- {opts} table|nil Additional options: ---- • {greedy}: (boolean) ---- Stage all contiguous hunks. Only useful if 'diff_opts' ---- contains `linematch`. Defaults to `true`. +--- {range} table|nil List-like table of two integers making +--- up the line range from which you want to reset the hunks. +--- If running via command line, then this is taken from the +--- command modifiers. +--- {opts} table|nil Additional options: +--- • {greedy}: (boolean) +--- Stage all contiguous hunks. Only useful if 'diff_opts' +--- contains `linematch`. Defaults to `true`. --- M.reset_hunk = mk_repeatable(void(function(range, opts) - opts = opts or {} - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + opts = opts or {} + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - local hunk = get_hunk(bufnr, range, opts.greedy ~= false, false) + local hunk = get_hunk(bufnr, range, opts.greedy ~= false, false) - if not hunk then - return - end + if not hunk then + return + end - local lstart, lend - if hunk.type == 'delete' then - lstart = hunk.added.start - lend = hunk.added.start - else - lstart = hunk.added.start - 1 - lend = hunk.added.start - 1 + hunk.added.count - end - util.set_lines(bufnr, lstart, lend, hunk.removed.lines) + local lstart, lend + if hunk.type == 'delete' then + lstart = hunk.added.start + lend = hunk.added.start + else + lstart = hunk.added.start - 1 + lend = hunk.added.start - 1 + hunk.added.count + end + util.set_lines(bufnr, lstart, lend, hunk.removed.lines) end)) C.reset_hunk = function(_, params) - M.reset_hunk(get_range(params)) + M.reset_hunk(get_range(params)) end --- Reset the lines of all hunks in the buffer. M.reset_buffer = function() - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - util.set_lines(bufnr, 0, -1, bcache.compare_text) + util.set_lines(bufnr, 0, -1, bcache.compare_text) end --- Undo the last call of stage_hunk(). @@ -414,57 +340,57 @@ end --- session can be undone. --- --- Attributes: ~ ---- {async} +--- {async} M.undo_stage_hunk = void(function() - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - local hunk = table.remove(bcache.staged_diffs) - if not hunk then - print("No hunks to undo") - return - end + local hunk = table.remove(bcache.staged_diffs) + if not hunk then + print('No hunks to undo') + return + end - bcache.git_obj:stage_hunks({ hunk }, true) - bcache:invalidate() - update(bufnr) + bcache.git_obj:stage_hunks({ hunk }, true) + bcache:invalidate() + update(bufnr) end) --- Stage all hunks in current buffer. --- --- Attributes: ~ ---- {async} +--- {async} M.stage_buffer = void(function() - local bufnr = current_buf() + local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + local bcache = cache[bufnr] + if not bcache then + return + end - -- Only process files with existing hunks - local hunks = bcache.hunks - if #hunks == 0 then - print("No unstaged changes in file to stage") - return - end + -- Only process files with existing hunks + local hunks = bcache.hunks + if #hunks == 0 then + print('No unstaged changes in file to stage') + return + end - if not util.path_exists(bcache.git_obj.file) then - print("Error: Cannot stage file. Please add it to the working tree.") - return - end + if not util.path_exists(bcache.git_obj.file) then + print('Error: Cannot stage file. Please add it to the working tree.') + return + end - bcache.git_obj:stage_hunks(hunks) + bcache.git_obj:stage_hunks(hunks) - for _, hunk in ipairs(hunks) do - table.insert(bcache.staged_diffs, hunk) - end + for _, hunk in ipairs(hunks) do + table.insert(bcache.staged_diffs, hunk) + end - bcache:invalidate() - update(bufnr) + bcache:invalidate() + update(bufnr) end) --- Unstage all hunks for current buffer in the index. Note: @@ -472,120 +398,119 @@ end) --- stages, this runs an `git reset` on current buffers file. --- --- Attributes: ~ ---- {async} +--- {async} M.reset_buffer_index = void(function() - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - -- `bcache.staged_diffs` won't contain staged changes outside of current - -- neovim session so signs added from this unstage won't be complete They will - -- however be fixed by gitdir watcher and properly updated We should implement - -- some sort of initial population from git diff, after that this function can - -- be improved to check if any staged hunks exists and it can undo changes - -- using git apply line by line instead of resetting whole file - bcache.staged_diffs = {} + -- `bcache.staged_diffs` won't contain staged changes outside of current + -- neovim session so signs added from this unstage won't be complete They will + -- however be fixed by gitdir watcher and properly updated We should implement + -- some sort of initial population from git diff, after that this function can + -- be improved to check if any staged hunks exists and it can undo changes + -- using git apply line by line instead of resetting whole file + bcache.staged_diffs = {} - bcache.git_obj:unstage_file() + bcache.git_obj:unstage_file() - bcache:invalidate() - update(bufnr) + bcache:invalidate() + update(bufnr) end) local function process_nav_opts(opts) - -- show navigation message - if opts.navigation_message == nil then - opts.navigation_message = not vim.opt.shortmess:get().S - end + -- show navigation message + if opts.navigation_message == nil then + opts.navigation_message = not vim.opt.shortmess:get().S + end - -- wrap around - if opts.wrap == nil then - opts.wrap = vim.opt.wrapscan:get() - end + -- wrap around + if opts.wrap == nil then + opts.wrap = vim.opt.wrapscan:get() + end - if opts.foldopen == nil then - opts.foldopen = vim.tbl_contains(vim.opt.foldopen:get(), 'search') - end + if opts.foldopen == nil then + opts.foldopen = vim.tbl_contains(vim.opt.foldopen:get(), 'search') + end - if opts.greedy == nil then - opts.greedy = true - end + if opts.greedy == nil then + opts.greedy = true + end end -- Defer function to the next main event local function defer(fn) - if vim.in_fast_event() then - vim.schedule(fn) - else - vim.defer_fn(fn, 1) - end + if vim.in_fast_event() then + vim.schedule(fn) + else + vim.defer_fn(fn, 1) + end end local function has_preview_inline(bufnr) - return #api.nvim_buf_get_extmarks(bufnr, ns_inline, 0, -1, { limit = 1 }) > 0 + return #api.nvim_buf_get_extmarks(bufnr, ns_inline, 0, -1, { limit = 1 }) > 0 end local nav_hunk = void(function(opts) - process_nav_opts(opts) - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + process_nav_opts(opts) + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - local hunks = {} - vim.list_extend(hunks, get_hunks(bufnr, bcache, opts.greedy, false) or {}) - local hunks_head = get_hunks(bufnr, bcache, opts.greedy, true) or {} - vim.list_extend(hunks, gs_hunks.filter_common(hunks_head, bcache.hunks)) + local hunks = {} + vim.list_extend(hunks, get_hunks(bufnr, bcache, opts.greedy, false) or {}) + local hunks_head = get_hunks(bufnr, bcache, opts.greedy, true) or {} + vim.list_extend(hunks, gs_hunks.filter_common(hunks_head, bcache.hunks)) - if not hunks or vim.tbl_isempty(hunks) then - if opts.navigation_message then - api.nvim_echo({ { 'No hunks', 'WarningMsg' } }, false, {}) - end - return - end - local line = api.nvim_win_get_cursor(0)[1] + if not hunks or vim.tbl_isempty(hunks) then + if opts.navigation_message then + api.nvim_echo({ { 'No hunks', 'WarningMsg' } }, false, {}) + end + return + end + local line = api.nvim_win_get_cursor(0)[1] - local hunk, index = gs_hunks.find_nearest_hunk(line, hunks, opts.forwards, opts.wrap) + local hunk, index = gs_hunks.find_nearest_hunk(line, hunks, opts.forwards, opts.wrap) - if hunk == nil then - if opts.navigation_message then - api.nvim_echo({ { 'No more hunks', 'WarningMsg' } }, false, {}) - end - return - end + if hunk == nil then + if opts.navigation_message then + api.nvim_echo({ { 'No more hunks', 'WarningMsg' } }, false, {}) + end + return + end - local row = opts.forwards and hunk.added.start or hunk.vend - if row then - -- Handle topdelete - if row == 0 then - row = 1 - end - vim.cmd([[ normal! m' ]]) -- add current cursor position to the jump list - api.nvim_win_set_cursor(0, { row, 0 }) - if opts.foldopen then - vim.cmd('silent! foldopen!') - end - if opts.preview or popup.is_open('hunk') ~= nil then - -- Use defer so the cursor change can settle, otherwise the popup might - -- appear in the old position - defer(function() - -- Close the popup in case one is open which will cause it to focus the - -- popup - popup.close('hunk') - M.preview_hunk() - end) - elseif has_preview_inline(bufnr) then - defer(M.preview_hunk_inline) - end + local row = opts.forwards and hunk.added.start or hunk.vend + if row then + -- Handle topdelete + if row == 0 then + row = 1 + end + vim.cmd([[ normal! m' ]]) -- add current cursor position to the jump list + api.nvim_win_set_cursor(0, { row, 0 }) + if opts.foldopen then + vim.cmd('silent! foldopen!') + end + if opts.preview or popup.is_open('hunk') ~= nil then + -- Use defer so the cursor change can settle, otherwise the popup might + -- appear in the old position + defer(function() + -- Close the popup in case one is open which will cause it to focus the + -- popup + popup.close('hunk') + M.preview_hunk() + end) + elseif has_preview_inline(bufnr) then + defer(M.preview_hunk_inline) + end - if index ~= nil and opts.navigation_message then - api.nvim_echo({ { string.format('Hunk %d of %d', index, #hunks), 'None' } }, false, {}) - end - - end + if index ~= nil and opts.navigation_message then + api.nvim_echo({ { string.format('Hunk %d of %d', index, #hunks), 'None' } }, false, {}) + end + end end) --- Jump to the next hunk in the current buffer. If a hunk preview @@ -593,27 +518,27 @@ end) --- at the next hunk. --- --- Parameters: ~ ---- {opts} table|nil Configuration table. Keys: ---- • {wrap}: (boolean) ---- Whether to loop around file or not. Defaults ---- to the value 'wrapscan' ---- • {navigation_message}: (boolean) ---- Whether to show navigation messages or not. ---- Looks at 'shortmess' for default behaviour. ---- • {foldopen}: (boolean) ---- Expand folds when navigating to a hunk which is ---- inside a fold. Defaults to `true` if 'foldopen' ---- contains `search`. ---- • {preview}: (boolean) ---- Automatically open preview_hunk() upon navigating ---- to a hunk. ---- • {greedy}: (boolean) ---- Only navigate between non-contiguous hunks. Only useful if ---- 'diff_opts' contains `linematch`. Defaults to `true`. +--- {opts} table|nil Configuration table. Keys: +--- • {wrap}: (boolean) +--- Whether to loop around file or not. Defaults +--- to the value 'wrapscan' +--- • {navigation_message}: (boolean) +--- Whether to show navigation messages or not. +--- Looks at 'shortmess' for default behaviour. +--- • {foldopen}: (boolean) +--- Expand folds when navigating to a hunk which is +--- inside a fold. Defaults to `true` if 'foldopen' +--- contains `search`. +--- • {preview}: (boolean) +--- Automatically open preview_hunk() upon navigating +--- to a hunk. +--- • {greedy}: (boolean) +--- Only navigate between non-contiguous hunks. Only useful if +--- 'diff_opts' contains `linematch`. Defaults to `true`. M.next_hunk = function(opts) - opts = opts or {} - opts.forwards = true - nav_hunk(opts) + opts = opts or {} + opts.forwards = true + nav_hunk(opts) end --- Jump to the previous hunk in the current buffer. If a hunk preview @@ -621,264 +546,265 @@ end --- at the previous hunk. --- --- Parameters: ~ ---- See |gitsigns.next_hunk()|. +--- See |gitsigns.next_hunk()|. M.prev_hunk = function(opts) - opts = opts or {} - opts.forwards = false - nav_hunk(opts) + opts = opts or {} + opts.forwards = false + nav_hunk(opts) end local HlMark = popup.HlMark -local function lines_format(fmt, - info) +local function lines_format(fmt, info) + local ret = vim.deepcopy(fmt) - local ret = vim.deepcopy(fmt) + for _, line in ipairs(ret) do + for _, s in ipairs(line) do + s[1] = util.expand_format(s[1], info) + end + end - for _, line in ipairs(ret) do - for _, s in ipairs(line) do - s[1] = util.expand_format(s[1], info) - end - end - - return ret + return ret end local function hlmarks_for_hunk(hunk, hl) - local hls = {} + local hls = {} - local removed, added = hunk.removed, hunk.added + local removed, added = hunk.removed, hunk.added - if hl then - hls[#hls + 1] = { - hl_group = hl, - start_row = 0, - end_row = removed.count + added.count, - } - end - - hls[#hls + 1] = { - hl_group = 'GitSignsDeletePreview', + if hl then + hls[#hls + 1] = { + hl_group = hl, start_row = 0, - end_row = removed.count, - } - - hls[#hls + 1] = { - hl_group = 'GitSignsAddPreview', - start_row = removed.count, end_row = removed.count + added.count, - } + } + end - if config.diff_opts.internal then - local removed_regions, added_regions = + hls[#hls + 1] = { + hl_group = 'GitSignsDeletePreview', + start_row = 0, + end_row = removed.count, + } + + hls[#hls + 1] = { + hl_group = 'GitSignsAddPreview', + start_row = removed.count, + end_row = removed.count + added.count, + } + + if config.diff_opts.internal then + local removed_regions, added_regions = require('gitsigns.diff_int').run_word_diff(removed.lines, added.lines) - for _, region in ipairs(removed_regions) do - hls[#hls + 1] = { - hl_group = 'GitSignsDeleteInline', - start_row = region[1] - 1, - start_col = region[3], - end_col = region[4], - } - end - for _, region in ipairs(added_regions) do - hls[#hls + 1] = { - hl_group = 'GitSignsAddInline', - start_row = region[1] + removed.count - 1, - start_col = region[3], - end_col = region[4], - } - end - end + for _, region in ipairs(removed_regions) do + hls[#hls + 1] = { + hl_group = 'GitSignsDeleteInline', + start_row = region[1] - 1, + start_col = region[3], + end_col = region[4], + } + end + for _, region in ipairs(added_regions) do + hls[#hls + 1] = { + hl_group = 'GitSignsAddInline', + start_row = region[1] + removed.count - 1, + start_col = region[3], + end_col = region[4], + } + end + end - return hls + return hls end local function insert_hunk_hlmarks(fmt, hunk) - for _, line in ipairs(fmt) do - for _, s in ipairs(line) do - local hl = s[2] - if s[1] == '' and type(hl) == "string" then - s[2] = hlmarks_for_hunk(hunk, hl) - end + for _, line in ipairs(fmt) do + for _, s in ipairs(line) do + local hl = s[2] + if s[1] == '' and type(hl) == 'string' then + s[2] = hlmarks_for_hunk(hunk, hl) end - end + end + end end local function noautocmd(f) - return function() - local ei = vim.o.eventignore - vim.o.eventignore = 'all' - f() - vim.o.eventignore = ei - end + return function() + local ei = vim.o.eventignore + vim.o.eventignore = 'all' + f() + vim.o.eventignore = ei + end end --- Preview the hunk at the cursor position in a floating --- window. If the preview is already open, calling this --- will cause the window to get focus. M.preview_hunk = noautocmd(function() - -- Wrap in noautocmd so vim-repeat continues to work + -- Wrap in noautocmd so vim-repeat continues to work - if popup.focus_open('hunk') then - return - end + if popup.focus_open('hunk') then + return + end - local bufnr = current_buf() + local bufnr = current_buf() - local hunk, index = get_cursor_hunk(bufnr) + local hunk, index = get_cursor_hunk(bufnr) - if not hunk then - return - end + if not hunk then + return + end - local lines_fmt = { - { { 'Hunk of ', 'Title' } }, - { { '', 'NormalFloat' } }, - } + local lines_fmt = { + { { 'Hunk of ', 'Title' } }, + { { '', 'NormalFloat' } }, + } - insert_hunk_hlmarks(lines_fmt, hunk) + insert_hunk_hlmarks(lines_fmt, hunk) - local lines_spec = lines_format(lines_fmt, { - hunk_no = index, - num_hunks = #cache[bufnr].hunks, - hunk = gs_hunks.patch_lines(hunk, vim.bo[bufnr].fileformat), - }) + local lines_spec = lines_format(lines_fmt, { + hunk_no = index, + num_hunks = #cache[bufnr].hunks, + hunk = gs_hunks.patch_lines(hunk, vim.bo[bufnr].fileformat), + }) - popup.create(lines_spec, config.preview_config, 'hunk') + popup.create(lines_spec, config.preview_config, 'hunk') end) local function clear_preview_inline(bufnr) - api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) + api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) end --- Preview the hunk at the cursor position inline in the buffer. M.preview_hunk_inline = function() - local bufnr = current_buf() + local bufnr = current_buf() - local hunk = get_cursor_hunk(bufnr) + local hunk = get_cursor_hunk(bufnr) - if not hunk then - return - end + if not hunk then + return + end - clear_preview_inline(bufnr) + clear_preview_inline(bufnr) - local winid - manager.show_added(bufnr, ns_inline, hunk) - if config._inline2 then - if hunk.removed.count > 0 then - winid = manager.show_deleted_in_float(bufnr, ns_inline, hunk) + local winid + manager.show_added(bufnr, ns_inline, hunk) + if config._inline2 then + if hunk.removed.count > 0 then + winid = manager.show_deleted_in_float(bufnr, ns_inline, hunk) + end + else + manager.show_deleted(bufnr, ns_inline, hunk) + end + + api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter' }, { + buffer = bufnr, + desc = 'Clear gitsigns inline preview', + callback = function() + if winid then + pcall(api.nvim_win_close, winid, true) end - else - manager.show_deleted(bufnr, ns_inline, hunk) - end - - api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter' }, { - buffer = bufnr, - desc = 'Clear gitsigns inline preview', - callback = function() - if winid then - pcall(api.nvim_win_close, winid, true) - end - clear_preview_inline(bufnr) - end, - once = true, - }) - - -- Virtual lines will be hidden if cursor is on the top row, so automatically - -- scroll the viewport. - if api.nvim_win_get_cursor(0)[1] == 1 then - local keys = hunk.removed.count .. '' - local cy = api.nvim_replace_termcodes(keys, true, false, true) - api.nvim_feedkeys(cy, 'n', false) - end + clear_preview_inline(bufnr) + end, + once = true, + }) + -- Virtual lines will be hidden if cursor is on the top row, so automatically + -- scroll the viewport. + if api.nvim_win_get_cursor(0)[1] == 1 then + local keys = hunk.removed.count .. '' + local cy = api.nvim_replace_termcodes(keys, true, false, true) + api.nvim_feedkeys(cy, 'n', false) + end end --- Select the hunk under the cursor. M.select_hunk = function() - local hunk = get_cursor_hunk() - if not hunk then return end + local hunk = get_cursor_hunk() + if not hunk then + return + end - vim.cmd('normal! ' .. hunk.added.start .. 'GV' .. hunk.vend .. 'G') + vim.cmd('normal! ' .. hunk.added.start .. 'GV' .. hunk.vend .. 'G') end --- Get hunk array for specified buffer. --- --- Parameters: ~ ---- {bufnr} integer: Buffer number, if not provided (or 0) ---- will use current buffer. +--- {bufnr} integer: Buffer number, if not provided (or 0) +--- will use current buffer. --- --- Return: ~ ---- Array of hunk objects. Each hunk object has keys: ---- • `"type"`: String with possible values: "add", "change", ---- "delete" ---- • `"head"`: Header that appears in the unified diff ---- output. ---- • `"lines"`: Line contents of the hunks prefixed with ---- either `"-"` or `"+"`. ---- • `"removed"`: Sub-table with fields: ---- • `"start"`: Line number (1-based) ---- • `"count"`: Line count ---- • `"added"`: Sub-table with fields: ---- • `"start"`: Line number (1-based) ---- • `"count"`: Line count +--- Array of hunk objects. Each hunk object has keys: +--- • `"type"`: String with possible values: "add", "change", +--- "delete" +--- • `"head"`: Header that appears in the unified diff +--- output. +--- • `"lines"`: Line contents of the hunks prefixed with +--- either `"-"` or `"+"`. +--- • `"removed"`: Sub-table with fields: +--- • `"start"`: Line number (1-based) +--- • `"count"`: Line count +--- • `"added"`: Sub-table with fields: +--- • `"start"`: Line number (1-based) +--- • `"count"`: Line count M.get_hunks = function(bufnr) - bufnr = bufnr or current_buf() - if not cache[bufnr] then return end - local ret = {} - -- TODO(lewis6991): allow this to accept a greedy option - for _, h in ipairs(cache[bufnr].hunks or {}) do - ret[#ret + 1] = { - head = h.head, - lines = gs_hunks.patch_lines(h, vim.bo[bufnr].fileformat), - type = h.type, - added = h.added, - removed = h.removed, - } - end - return ret + bufnr = bufnr or current_buf() + if not cache[bufnr] then + return + end + local ret = {} + -- TODO(lewis6991): allow this to accept a greedy option + for _, h in ipairs(cache[bufnr].hunks or {}) do + ret[#ret + 1] = { + head = h.head, + lines = gs_hunks.patch_lines(h, vim.bo[bufnr].fileformat), + type = h.type, + added = h.added, + removed = h.removed, + } + end + return ret end local function get_blame_hunk(repo, info) - local a = {} - -- If no previous so sha of blame added the file - if info.previous then - a = repo:get_show_text(info.previous_sha .. ':' .. info.previous_filename) - end - local b = repo:get_show_text(info.sha .. ':' .. info.filename) - local hunks = run_diff(a, b, false) - local hunk, i = gs_hunks.find_hunk(info.orig_lnum, hunks) - return hunk, i, #hunks + local a = {} + -- If no previous so sha of blame added the file + if info.previous then + a = repo:get_show_text(info.previous_sha .. ':' .. info.previous_filename) + end + local b = repo:get_show_text(info.sha .. ':' .. info.filename) + local hunks = run_diff(a, b, false) + local hunk, i = gs_hunks.find_hunk(info.orig_lnum, hunks) + return hunk, i, #hunks end local function create_blame_fmt(is_committed, full) - if not is_committed then - return { - { { '', 'Label' } }, - } - end + if not is_committed then + return { + { { '', 'Label' } }, + } + end - local header = { - { ' ', 'Directory' }, - { ' ', 'MoreMsg' }, - { '()', 'Label' }, - { ':', 'NormalFloat' }, - } + local header = { + { ' ', 'Directory' }, + { ' ', 'MoreMsg' }, + { '()', 'Label' }, + { ':', 'NormalFloat' }, + } - if full then - return { - header, - { { '', 'NormalFloat' } }, - { { 'Hunk of ', 'Title' }, { ' ', 'LineNr' } }, - { { '', 'NormalFloat' } }, - } - end - - return { + if full then + return { header, - { { '', 'NormalFloat' } }, - } + { { '', 'NormalFloat' } }, + { { 'Hunk of ', 'Title' }, { ' ', 'LineNr' } }, + { { '', 'NormalFloat' } }, + } + end + + return { + header, + { { '', 'NormalFloat' } }, + } end --- Run git blame on the current line and show the results in a @@ -886,66 +812,68 @@ end --- window to get focus. --- --- Parameters: ~ ---- {opts} (table|nil): ---- Additional options: ---- • {full}: (boolean) ---- Display full commit message with hunk. ---- • {ignore_whitespace}: (boolean) ---- Ignore whitespace when running blame. +--- {opts} (table|nil): +--- Additional options: +--- • {full}: (boolean) +--- Display full commit message with hunk. +--- • {ignore_whitespace}: (boolean) +--- Ignore whitespace when running blame. --- --- Attributes: ~ ---- {async} +--- {async} M.blame_line = void(function(opts) - if popup.focus_open('blame') then - return - end + if popup.focus_open('blame') then + return + end - opts = opts or {} + opts = opts or {} - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then return end + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - local loading = vim.defer_fn(function() - popup.create({ { { 'Loading...', 'Title' } } }, config.preview_config) - end, 1000) + local loading = vim.defer_fn(function() + popup.create({ { { 'Loading...', 'Title' } } }, config.preview_config) + end, 1000) - scheduler() - local buftext = util.buf_lines(bufnr) - local fileformat = vim.bo[bufnr].fileformat - local lnum = api.nvim_win_get_cursor(0)[1] - local result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) - pcall(function() - loading:close() - end) + scheduler() + local buftext = util.buf_lines(bufnr) + local fileformat = vim.bo[bufnr].fileformat + local lnum = api.nvim_win_get_cursor(0)[1] + local result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + pcall(function() + loading:close() + end) - local is_committed = result.sha and tonumber('0x' .. result.sha) ~= 0 + local is_committed = result.sha and tonumber('0x' .. result.sha) ~= 0 - local blame_fmt = create_blame_fmt(is_committed, opts.full) + local blame_fmt = create_blame_fmt(is_committed, opts.full) - local info = result + local info = result - if is_committed and opts.full then - info.body = bcache.git_obj:command({ 'show', '-s', '--format=%B', result.sha }) + if is_committed and opts.full then + info.body = bcache.git_obj:command({ 'show', '-s', '--format=%B', result.sha }) - local hunk + local hunk - hunk, info.hunk_no, info.num_hunks = get_blame_hunk(bcache.git_obj.repo, result) + hunk, info.hunk_no, info.num_hunks = get_blame_hunk(bcache.git_obj.repo, result) - info.hunk = gs_hunks.patch_lines(hunk, fileformat) - info.hunk_head = hunk.head - insert_hunk_hlmarks(blame_fmt, hunk) - end + info.hunk = gs_hunks.patch_lines(hunk, fileformat) + info.hunk_head = hunk.head + insert_hunk_hlmarks(blame_fmt, hunk) + end - scheduler() + scheduler() - popup.create(lines_format(blame_fmt, info), config.preview_config, 'blame') + popup.create(lines_format(blame_fmt, info), config.preview_config, 'blame') end) local function update_buf_base(buf, bcache, base) - bcache.base = base - bcache:invalidate() - update(buf) + bcache.base = base + bcache:invalidate() + update(buf) end --- Change the base revision to diff against. If {base} is not @@ -954,53 +882,55 @@ end --- including any new buffers. --- --- Attributes: ~ ---- {async} +--- {async} --- --- Parameters:~ ---- {base} string|nil The object/revision to diff against. ---- {global} boolean|nil Change the base of all buffers. +--- {base} string|nil The object/revision to diff against. +--- {global} boolean|nil Change the base of all buffers. --- --- Examples: > ---- " Change base to 1 commit behind head ---- :lua require('gitsigns').change_base('HEAD~1') +--- " Change base to 1 commit behind head +--- :lua require('gitsigns').change_base('HEAD~1') --- ---- " Also works using the Gitsigns command ---- :Gitsigns change_base HEAD~1 +--- " Also works using the Gitsigns command +--- :Gitsigns change_base HEAD~1 --- ---- " Other variations ---- :Gitsigns change_base ~1 ---- :Gitsigns change_base ~ ---- :Gitsigns change_base ^ +--- " Other variations +--- :Gitsigns change_base ~1 +--- :Gitsigns change_base ~ +--- :Gitsigns change_base ^ --- ---- " Commits work too ---- :Gitsigns change_base 92eb3dd +--- " Commits work too +--- :Gitsigns change_base 92eb3dd --- ---- " Revert to original base ---- :Gitsigns change_base +--- " Revert to original base +--- :Gitsigns change_base --- < --- --- For a more complete list of ways to specify bases, see --- |gitsigns-revision|. M.change_base = void(function(base, global) - base = util.calc_base(base) + base = util.calc_base(base) - if global then - config.base = base - - for bufnr, bcache in pairs(cache) do - update_buf_base(bufnr, bcache, base) - end - else - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then return end + if global then + config.base = base + for bufnr, bcache in pairs(cache) do update_buf_base(bufnr, bcache, base) - end + end + else + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + + update_buf_base(bufnr, bcache, base) + end end) C.change_base = function(args, _) - M.change_base(args[1], (args[2] or args.global)) + M.change_base(args[1], (args[2] or args.global)) end CP.change_base = complete_heads @@ -1010,11 +940,11 @@ CP.change_base = complete_heads --- --- Alias for `change_base(nil, {global})` . M.reset_base = function(global) - M.change_base(nil, global) + M.change_base(nil, global) end C.reset_base = function(args, _) - M.change_base(nil, (args[1] or args.global)) + M.change_base(nil, (args[1] or args.global)) end --- Perform a |vimdiff| on the given file with {base} if it is @@ -1024,61 +954,61 @@ end --- any written changes will update the index accordingly. --- --- Parameters: ~ ---- {base} (string|nil): Revision to diff against. Defaults ---- to index. ---- {opts} (table|nil): ---- Additional options: ---- • {vertical}: {boolean}. Split window vertically. Defaults to ---- config.diff_opts.vertical. If running via command line, then ---- this is taken from the command modifiers. ---- • {split}: {string}. One of: 'aboveleft', 'belowright', ---- 'botright', 'rightbelow', 'leftabove', 'topleft'. Defaults to ---- 'aboveleft'. If running via command line, then this is taken ---- from the command modifiers. +--- {base} (string|nil): Revision to diff against. Defaults +--- to index. +--- {opts} (table|nil): +--- Additional options: +--- • {vertical}: {boolean}. Split window vertically. Defaults to +--- config.diff_opts.vertical. If running via command line, then +--- this is taken from the command modifiers. +--- • {split}: {string}. One of: 'aboveleft', 'belowright', +--- 'botright', 'rightbelow', 'leftabove', 'topleft'. Defaults to +--- 'aboveleft'. If running via command line, then this is taken +--- from the command modifiers. --- --- Examples: > ---- " Diff against the index ---- :Gitsigns diffthis +--- " Diff against the index +--- :Gitsigns diffthis --- ---- " Diff against the last commit ---- :Gitsigns diffthis ~1 +--- " Diff against the last commit +--- :Gitsigns diffthis ~1 --- < --- --- For a more complete list of ways to specify bases, see --- |gitsigns-revision|. --- --- Attributes: ~ ---- {async} +--- {async} M.diffthis = function(base, opts) - -- TODO(lewis6991): can't pass numbers as strings from the command line - if base ~= nil then - base = tostring(base) - end - opts = opts or {} - local diffthis = require('gitsigns.diffthis') - if not opts.vertical then - opts.vertical = config.diff_opts.vertical - end - diffthis.diffthis(base, opts) + -- TODO(lewis6991): can't pass numbers as strings from the command line + if base ~= nil then + base = tostring(base) + end + opts = opts or {} + local diffthis = require('gitsigns.diffthis') + if not opts.vertical then + opts.vertical = config.diff_opts.vertical + end + diffthis.diffthis(base, opts) end C.diffthis = function(args, params) - -- TODO(lewis6991): validate these - local opts = { - vertical = args.vertical, - split = args.split, - } + -- TODO(lewis6991): validate these + local opts = { + vertical = args.vertical, + split = args.split, + } - if params.smods then - if params.smods.split ~= '' and opts.split == nil then - opts.split = params.smods.split - end - if opts.vertical == nil then - opts.vertical = params.smods.vertical - end - end + if params.smods then + if params.smods.split ~= '' and opts.split == nil then + opts.split = params.smods.split + end + if opts.vertical == nil then + opts.vertical = params.smods.vertical + end + end - M.diffthis(args[1], opts) + M.diffthis(args[1], opts) end CP.diffthis = complete_heads @@ -1096,139 +1026,141 @@ CP.diffthis = complete_heads --- any written changes will update the index accordingly. --- --- Examples: > ---- " View the index version of the file ---- :Gitsigns show +--- " View the index version of the file +--- :Gitsigns show --- ---- " View revision of file in the last commit ---- :Gitsigns show ~1 +--- " View revision of file in the last commit +--- :Gitsigns show ~1 --- < --- --- For a more complete list of ways to specify bases, see --- |gitsigns-revision|. --- --- Attributes: ~ ---- {async} +--- {async} M.show = function(revision) - local diffthis = require('gitsigns.diffthis') - diffthis.show(revision) + local diffthis = require('gitsigns.diffthis') + diffthis.show(revision) end CP.show = complete_heads local function hunks_to_qflist(buf_or_filename, hunks, qflist) - for i, hunk in ipairs(hunks) do - qflist[#qflist + 1] = { - bufnr = type(buf_or_filename) == "number" and (buf_or_filename) or nil, - filename = type(buf_or_filename) == "string" and buf_or_filename or nil, - lnum = hunk.added.start, - text = string.format('Lines %d-%d (%d/%d)', - hunk.added.start, hunk.vend, i, #hunks), - } - end + for i, hunk in ipairs(hunks) do + qflist[#qflist + 1] = { + bufnr = type(buf_or_filename) == 'number' and buf_or_filename or nil, + filename = type(buf_or_filename) == 'string' and buf_or_filename or nil, + lnum = hunk.added.start, + text = string.format('Lines %d-%d (%d/%d)', hunk.added.start, hunk.vend, i, #hunks), + } + end end local function buildqflist(target) - target = target or current_buf() - if target == 0 then target = current_buf() end - local qflist = {} + target = target or current_buf() + if target == 0 then + target = current_buf() + end + local qflist = {} - if type(target) == 'number' then - local bufnr = target - if not cache[bufnr] then return end - hunks_to_qflist(bufnr, cache[bufnr].hunks, qflist) - elseif target == 'attached' then - for bufnr, bcache in pairs(cache) do - hunks_to_qflist(bufnr, bcache.hunks, qflist) - end - elseif target == 'all' then - local repos = {} - for _, bcache in pairs(cache) do - local repo = bcache.git_obj.repo - if not repos[repo.gitdir] then - repos[repo.gitdir] = repo - end - end - - local repo = git.Repo.new(vim.loop.cwd()) + if type(target) == 'number' then + local bufnr = target + if not cache[bufnr] then + return + end + hunks_to_qflist(bufnr, cache[bufnr].hunks, qflist) + elseif target == 'attached' then + for bufnr, bcache in pairs(cache) do + hunks_to_qflist(bufnr, bcache.hunks, qflist) + end + elseif target == 'all' then + local repos = {} + for _, bcache in pairs(cache) do + local repo = bcache.git_obj.repo if not repos[repo.gitdir] then - repos[repo.gitdir] = repo + repos[repo.gitdir] = repo end + end - for _, r in pairs(repos) do - for _, f in ipairs(r:files_changed()) do - local f_abs = r.toplevel .. '/' .. f - local stat = vim.loop.fs_stat(f_abs) - if stat and stat.type == 'file' then - local a = r:get_show_text(':0:' .. f) - scheduler() - local hunks = run_diff(a, util.file_lines(f_abs)) - hunks_to_qflist(f_abs, hunks, qflist) - end - end + local repo = git.Repo.new(vim.loop.cwd()) + if not repos[repo.gitdir] then + repos[repo.gitdir] = repo + end + + for _, r in pairs(repos) do + for _, f in ipairs(r:files_changed()) do + local f_abs = r.toplevel .. '/' .. f + local stat = vim.loop.fs_stat(f_abs) + if stat and stat.type == 'file' then + local a = r:get_show_text(':0:' .. f) + scheduler() + local hunks = run_diff(a, util.file_lines(f_abs)) + hunks_to_qflist(f_abs, hunks, qflist) + end end - - end - return qflist + end + end + return qflist end --- Populate the quickfix list with hunks. Automatically opens the --- quickfix window. --- --- Attributes: ~ ---- {async} +--- {async} --- --- Parameters: ~ ---- {target} (integer or string): ---- Specifies which files hunks are collected from. ---- Possible values. ---- • [integer]: The buffer with the matching buffer ---- number. `0` for current buffer (default). ---- • `"attached"`: All attached buffers. ---- • `"all"`: All modified files for each git ---- directory of all attached buffers in addition ---- to the current working directory. ---- {opts} (table|nil): ---- Additional options: ---- • {use_location_list}: (boolean) ---- Populate the location list instead of the ---- quickfix list. Default to `false`. ---- • {nr}: (integer) ---- Window number or ID when using location list. ---- Expand folds when navigating to a hunk which is ---- inside a fold. Defaults to `0`. ---- • {open}: (boolean) ---- Open the quickfix/location list viewer. ---- Defaults to `true`. +--- {target} (integer or string): +--- Specifies which files hunks are collected from. +--- Possible values. +--- • [integer]: The buffer with the matching buffer +--- number. `0` for current buffer (default). +--- • `"attached"`: All attached buffers. +--- • `"all"`: All modified files for each git +--- directory of all attached buffers in addition +--- to the current working directory. +--- {opts} (table|nil): +--- Additional options: +--- • {use_location_list}: (boolean) +--- Populate the location list instead of the +--- quickfix list. Default to `false`. +--- • {nr}: (integer) +--- Window number or ID when using location list. +--- Expand folds when navigating to a hunk which is +--- inside a fold. Defaults to `0`. +--- • {open}: (boolean) +--- Open the quickfix/location list viewer. +--- Defaults to `true`. M.setqflist = void(function(target, opts) - opts = opts or {} - if opts.open == nil then - opts.open = true - end - local qfopts = { - items = buildqflist(target), - title = 'Hunks', - } - scheduler() - if opts.use_location_list then - local nr = opts.nr or 0 - vim.fn.setloclist(nr, {}, ' ', qfopts) - if opts.open then - if config.trouble then - require('trouble').open("loclist") - else - vim.cmd([[lopen]]) - end + opts = opts or {} + if opts.open == nil then + opts.open = true + end + local qfopts = { + items = buildqflist(target), + title = 'Hunks', + } + scheduler() + if opts.use_location_list then + local nr = opts.nr or 0 + vim.fn.setloclist(nr, {}, ' ', qfopts) + if opts.open then + if config.trouble then + require('trouble').open('loclist') + else + vim.cmd([[lopen]]) end - else - vim.fn.setqflist({}, ' ', qfopts) - if opts.open then - if config.trouble then - require('trouble').open("quickfix") - else - vim.cmd([[copen]]) - end + end + else + vim.fn.setqflist({}, ' ', qfopts) + if opts.open then + if config.trouble then + require('trouble').open('quickfix') + else + vim.cmd([[copen]]) end - end + end + end end) --- Populate the location list with hunks. Automatically opens the @@ -1237,80 +1169,80 @@ end) --- Alias for: `setqflist({target}, { use_location_list = true, nr = {nr} }` --- --- Attributes: ~ ---- {async} +--- {async} --- --- Parameters: ~ ---- {nr} (integer): Window number or the |window-ID|. ---- `0` for the current window (default). ---- {target} (integer or string): See |gitsigns.setqflist()|. +--- {nr} (integer): Window number or the |window-ID|. +--- `0` for the current window (default). +--- {target} (integer or string): See |gitsigns.setqflist()|. M.setloclist = function(nr, target) - M.setqflist(target, { - nr = nr, - use_location_list = true, - }) + M.setqflist(target, { + nr = nr, + use_location_list = true, + }) end --- Get all the available line specific actions for the current --- buffer at the cursor position. --- --- Return: ~ ---- Dictionary of action name to function which when called ---- performs action. +--- Dictionary of action name to function which when called +--- performs action. M.get_actions = function() - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - local hunk = get_cursor_hunk() + local bufnr = current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end + local hunk = get_cursor_hunk() - local actions_l = {} + local actions_l = {} - local function add_action(action) - actions_l[#actions_l + 1] = action - end + local function add_action(action) + actions_l[#actions_l + 1] = action + end - if hunk then - add_action('stage_hunk') - add_action('reset_hunk') - add_action('preview_hunk') - add_action('select_hunk') - else - add_action('blame_line') - end + if hunk then + add_action('stage_hunk') + add_action('reset_hunk') + add_action('preview_hunk') + add_action('select_hunk') + else + add_action('blame_line') + end - if not vim.tbl_isempty(bcache.staged_diffs) then - add_action('undo_stage_hunk') - end + if not vim.tbl_isempty(bcache.staged_diffs) then + add_action('undo_stage_hunk') + end - local actions = {} - for _, a in ipairs(actions_l) do - actions[a] = (M)[a] - end + local actions = {} + for _, a in ipairs(actions_l) do + actions[a] = (M)[a] + end - return actions + return actions end --- Refresh all buffers. --- --- Attributes: ~ ---- {async} +--- {async} M.refresh = void(function() - manager.reset_signs() - require('gitsigns.highlight').setup_highlights() - require('gitsigns.current_line_blame').setup() - for k, v in pairs(cache) do - v:invalidate() - manager.update(k, v) - end + manager.reset_signs() + require('gitsigns.highlight').setup_highlights() + require('gitsigns.current_line_blame').setup() + for k, v in pairs(cache) do + v:invalidate() + manager.update(k, v) + end end) function M._get_cmd_func(name) - return C[name] + return C[name] end function M._get_cmp_func(name) - return CP[name] + return CP[name] end return M diff --git a/lua/gitsigns/async.lua b/lua/gitsigns/async.lua index 379ca93..0d83e71 100644 --- a/lua/gitsigns/async.lua +++ b/lua/gitsigns/async.lua @@ -1,42 +1,15 @@ - - -- Order by highest number of return types - - - - - - - - - - - - - - - - - - - +-- Order by highest number of return types local M = {} - - local Async_T = {} - -- Handle for an object currently running on the event loop. - -- The coroutine is paused while this is active. - -- Must provide methods cancel() and is_cancelled() - -- - -- Handle gets updated on each call to a wrapped functions, so provide access - -- to it via a proxy - - - - - +-- Handle for an object currently running on the event loop. +-- The coroutine is paused while this is active. +-- Must provide methods cancel() and is_cancelled() +-- +-- Handle gets updated on each call to a wrapped functions, so provide access +-- to it via a proxy -- Coroutine.running() was changed between Lua 5.1 and 5.2: -- - 5.1: Returns the running coroutine, or nil when called by the main thread. @@ -53,106 +26,104 @@ local handles = setmetatable({}, { __mode = 'k' }) --- Returns whether the current execution context is async. function M.running() - local current = coroutine.running() - if current and handles[current] then - return true - end -end - --- hack: teal doesn't know table.maxn exists -local function maxn(x) - return ((table).maxn)(x) + local current = coroutine.running() + if current and handles[current] then + return true + end end local function is_Async_T(handle) - if handle and - type(handle) == 'table' and - vim.is_callable(handle.cancel) and - vim.is_callable(handle.is_cancelled) then - return true - end + if + handle + and type(handle) == 'table' + and vim.is_callable(handle.cancel) + and vim.is_callable(handle.is_cancelled) + then + return true + end end -- Analogous to uv.close function Async_T:cancel(cb) - -- Cancel anything running on the event loop - if self._current and not self._current:is_cancelled() then - self._current:cancel(cb) - end + -- Cancel anything running on the event loop + if self._current and not self._current:is_cancelled() then + self._current:cancel(cb) + end end function Async_T.new(co) - local handle = setmetatable({}, { __index = Async_T }) - handles[co] = handle - return handle + local handle = setmetatable({}, { __index = Async_T }) + handles[co] = handle + return handle end -- Analogous to uv.is_closing function Async_T:is_cancelled() - return self._current and self._current:is_cancelled() + return self._current and self._current:is_cancelled() end local function run(func, callback, ...) - local co = coroutine.create(func) - local handle = Async_T.new(co) + local co = coroutine.create(func) + local handle = Async_T.new(co) - local function step(...) - local ret = { coroutine.resume(co, ...) } - local stat = ret[1] + local function step(...) + local ret = { coroutine.resume(co, ...) } + local stat = ret[1] - if not stat then - local err = ret[2] - error(string.format("The coroutine failed with this message: %s\n%s", - err, debug.traceback(co))) + if not stat then + local err = ret[2] + error( + string.format('The coroutine failed with this message: %s\n%s', err, debug.traceback(co)) + ) + end + + if coroutine.status(co) == 'dead' then + if callback then + callback(unpack(ret, 4, table.maxn(ret))) end + return + end - if coroutine.status(co) == 'dead' then - if callback then - callback(unpack(ret, 4, maxn(ret))) - end - return - end + local _, nargs, fn = unpack(ret) - local _, nargs, fn = unpack(ret) + assert(type(fn) == 'function', 'type error :: expected func') - assert(type(fn) == 'function', "type error :: expected func") + local args = { select(4, unpack(ret)) } + args[nargs] = step - local args = { select(4, unpack(ret)) } - args[nargs] = step + local r = fn(unpack(args, 1, nargs)) + if is_Async_T(r) then + handle._current = r + end + end - local r = fn(unpack(args, 1, nargs)) - if is_Async_T(r) then - handle._current = r - end - end - - step(...) - return handle + step(...) + return handle end function M.wait(argc, func, ...) - -- Always run the wrapped functions in xpcall and re-raise the error in the - -- coroutine. This makes pcall work as normal. - local function pfunc(...) - local args = { ... } - local cb = args[argc] - args[argc] = function(...) - cb(true, ...) - end - xpcall(func, function(err) - cb(false, err, debug.traceback()) - end, unpack(args, 1, argc)) - end + -- Always run the wrapped functions in xpcall and re-raise the error in the + -- coroutine. This makes pcall work as normal. + local function pfunc(...) + local args = { ... } + local cb = args[argc] + args[argc] = function(...) + cb(true, ...) + end + xpcall(func, function(err) + cb(false, err, debug.traceback()) + end, unpack(args, 1, argc)) + end - local ret = { coroutine.yield(argc, pfunc, ...) } + local ret = { coroutine.yield(argc, pfunc, ...) } - local ok = ret[1] - if not ok then - local _, err, traceback = unpack(ret) - error(string.format("Wrapped function failed: %s\n%s", err, traceback)) - end + local ok = ret[1] + if not ok then + local _, err, traceback = unpack(ret) + error(string.format('Wrapped function failed: %s\n%s', err, traceback)) + end - return unpack(ret, 2, maxn(ret)) + return unpack(ret, 2, table.maxn(ret)) end ---Creates an async function with a callback style function. @@ -160,13 +131,13 @@ end ---@param argc number: The number of arguments of func. Must be included. ---@return function: Returns an async function function M.wrap(func, argc) - assert(argc) - return function(...) - if not M.running() then - return func(...) - end - return M.wait(argc, func, ...) - end + assert(argc) + return function(...) + if not M.running() then + return func(...) + end + return M.wait(argc, func, ...) + end end ---Use this to create a function which executes in an async context but @@ -174,14 +145,14 @@ end ---since it is non-blocking ---@param func function function M.create(func, argc) - argc = argc or 0 - return function(...) - if M.running() then - return func(...) - end - local callback = select(argc + 1, ...) - return run(func, callback, unpack({ ... }, 1, argc)) - end + argc = argc or 0 + return function(...) + if M.running() then + return func(...) + end + local callback = select(argc + 1, ...) + return run(func, callback, unpack({ ... }, 1, argc)) + end end ---Use this to create a function which executes in an async context but @@ -189,12 +160,12 @@ end ---since it is non-blocking ---@param func function function M.void(func) - return function(...) - if M.running() then - return func(...) - end - return run(func, nil, ...) - end + return function(...) + if M.running() then + return func(...) + end + return run(func, nil, ...) + end end ---An async function that when called will yield to the Neovim scheduler to be diff --git a/lua/gitsigns/attach.lua b/lua/gitsigns/attach.lua index daa72d9..de22d5d 100644 --- a/lua/gitsigns/attach.lua +++ b/lua/gitsigns/attach.lua @@ -1,7 +1,7 @@ local async = require('gitsigns.async') local git = require('gitsigns.git') -local log = require("gitsigns.debug.log") +local log = require('gitsigns.debug.log') local dprintf = log.dprintf local dprint = log.dprint @@ -11,7 +11,7 @@ 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 Status = require('gitsigns.status') local gs_config = require('gitsigns.config') local config = gs_config.config @@ -19,393 +19,376 @@ 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 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 + 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 + 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 + -- 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() + local file = uv.fs_realpath(api.nvim_buf_get_name(bufnr)) + or api.nvim_buf_call(bufnr, function() return vim.fn.expand('%:p') - end) + 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 + 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 + 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 + end - return file + 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) + 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) + local __FUNC__ = 'on_reload' + dprint('Reload') + manager.update_debounced(bufnr) end local function on_detach(_, bufnr) - M.detach(bufnr, true) + 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 + 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 - end - return gitdir, toplevel + 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 + 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 + 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 + if done_setup then + return + end - done_setup = true + done_setup = true - manager.setup() + manager.setup() - hl.setup_highlights() - api.nvim_create_autocmd('ColorScheme', { - group = 'gitsigns', - callback = hl.setup_highlights, - }) + 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, }) + 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, + }) - -- 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, + }) - 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, - }) + 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' + local __FUNC__ = 'attach' - M._setup() + M._setup() - if vimgrep_running then - dprint('attaching is disabled') + 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 + end - if cache[cbuf] then - dprint('Already attached') + if vim.bo[cbuf].buftype ~= '' then + dprint('Non-normal buffer') return - end + end - if aucmd then - dprintf('Attaching (trigger=%s)', aucmd) - else - dprint('Attaching') - end + file, commit = get_buf_path(cbuf) + local file_dir = util.dirname(file) - if not api.nvim_buf_is_loaded(cbuf) then - dprint('Non-loaded buffer') + if not file_dir or not util.path_exists(file_dir) then + dprint('Not a path') return - end + end - local encoding = vim.bo[cbuf].fileencoding - if encoding == '' then - encoding = 'utf-8' - end - local file - local commit - local gitdir_oap - local toplevel_oap + gitdir_oap, toplevel_oap = on_attach_pre(cbuf) + end - if ctx then - gitdir_oap = ctx.gitdir - toplevel_oap = ctx.toplevel - file = ctx.toplevel .. util.path_sep .. ctx.file - commit = ctx.commit - else - if api.nvim_buf_line_count(cbuf) > config.max_file_length then - dprint('Exceeds max_file_length') - return - end + local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) - if vim.bo[cbuf].buftype ~= '' then - dprint('Non-normal buffer') - return - end + if not git_obj and not ctx then + git_obj = try_worktrees(cbuf, file, encoding) + async.scheduler() + end - file, commit = get_buf_path(cbuf) - local file_dir = util.dirname(file) + if not git_obj then + dprint('Empty git obj') + return + end + local repo = git_obj.repo - if not file_dir or not util.path_exists(file_dir) then - dprint('Not a path') - return - end + async.scheduler() + Status:update(cbuf, { + head = repo.abbrev_head, + root = repo.toplevel, + gitdir = repo.gitdir, + }) - gitdir_oap, toplevel_oap = on_attach_pre(cbuf) - end + if vim.startswith(file, repo.gitdir .. util.path_sep) then + dprint('In non-standard git dir') + return + end - local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) + 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 and not ctx then - git_obj = try_worktrees(cbuf, file, encoding) - async.scheduler() - end + if not git_obj.relpath then + dprint('Cannot resolve file in repo') + return + end - if not git_obj then - dprint('Empty git obj') - return - end - local repo = git_obj.repo + if not config.attach_to_untracked and git_obj.object_name == nil then + dprint('File is untracked') + return + end - async.scheduler() - Status:update(cbuf, { - head = repo.abbrev_head, - root = repo.toplevel, - gitdir = repo.gitdir, - }) + -- On windows os.tmpname() crashes in callback threads so initialise this + -- variable on the main thread. + async.scheduler() - if vim.startswith(file, repo.gitdir .. util.path_sep) then - dprint('In non-standard git dir') - return - end + if config.on_attach and config.on_attach(cbuf) == false then + dprint('User on_attach() returned false') + 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 + 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 git_obj.relpath then - dprint('Cannot resolve file in repo') - return - end + if not api.nvim_buf_is_loaded(cbuf) then + dprint('Un-loaded buffer') + return + end - if not config.attach_to_untracked and git_obj.object_name == nil then - dprint('File is untracked') - 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, + }) - -- On windows os.tmpname() crashes in callback threads so initialise this - -- variable on the main thread. - async.scheduler() + -- Initial update + manager.update(cbuf, cache[cbuf]) - 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 + 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 + 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 +--- {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 + -- 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) + manager.detach(bufnr, _keep_signs) - -- Clear status variables - Status:clear(bufnr) + -- Clear status variables + Status:clear(bufnr) - cache:destroy(bufnr) + cache:destroy(bufnr) end - --- Attach Gitsigns to the buffer. --- --- Attributes: ~ ---- {async} +--- {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. +--- {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) + attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger) end) return M diff --git a/lua/gitsigns/cache.lua b/lua/gitsigns/cache.lua index 74e466c..6753d2f 100644 --- a/lua/gitsigns/cache.lua +++ b/lua/gitsigns/cache.lua @@ -1,101 +1,67 @@ -local Hunk = require("gitsigns.hunks").Hunk +local Hunk = require('gitsigns.hunks').Hunk local GitObj = require('gitsigns.git').Obj local config = require('gitsigns.config').config -local M = {CacheEntry = {}, CacheObj = {}, } - - - - - - - - - - - - -- Timer object watching the gitdir - - - - - - - - - - - - - - - - - - +local M = { CacheEntry = {}, CacheObj = {} } +-- Timer object watching the gitdir local CacheEntry = M.CacheEntry function CacheEntry:get_compare_rev(base) - base = base or self.base - if base then - return base - end + base = base or self.base + if base then + return base + end - if self.commit then - -- Buffer is a fugitive commit so compare against the parent of the commit - if config._signs_staged_enable then - return self.commit - else - return string.format('%s^', self.commit) - end - end + if self.commit then + -- Buffer is a fugitive commit so compare against the parent of the commit + if config._signs_staged_enable then + return self.commit + else + return string.format('%s^', self.commit) + end + end - local stage = self.git_obj.has_conflicts and 1 or 0 - return string.format(':%d', stage) + local stage = self.git_obj.has_conflicts and 1 or 0 + return string.format(':%d', stage) end function CacheEntry:get_staged_compare_rev() - return self.commit and string.format('%s^', self.commit) or 'HEAD' + return self.commit and string.format('%s^', self.commit) or 'HEAD' end function CacheEntry:get_rev_bufname(rev) - rev = rev or self:get_compare_rev() - return string.format( - 'gitsigns://%s/%s:%s', - self.git_obj.repo.gitdir, - rev, - self.git_obj.relpath) - + rev = rev or self:get_compare_rev() + return string.format('gitsigns://%s/%s:%s', self.git_obj.repo.gitdir, rev, self.git_obj.relpath) end function CacheEntry:invalidate() - self.compare_text = nil - self.compare_text_head = nil - self.hunks = nil - self.hunks_staged = nil + self.compare_text = nil + self.compare_text_head = nil + self.hunks = nil + self.hunks_staged = nil end function CacheEntry.new(o) - o.staged_diffs = o.staged_diffs or {} - return setmetatable(o, { __index = CacheEntry }) + o.staged_diffs = o.staged_diffs or {} + return setmetatable(o, { __index = CacheEntry }) end function CacheEntry:destroy() - local w = self.gitdir_watcher - if w and not w:is_closing() then - w:close() - end + local w = self.gitdir_watcher + if w and not w:is_closing() then + w:close() + end end function M.CacheObj:destroy(bufnr) - self[bufnr]:destroy() - self[bufnr] = nil + self[bufnr]:destroy() + self[bufnr] = nil end M.cache = setmetatable({}, { - __index = M.CacheObj, + __index = M.CacheObj, }) return M diff --git a/lua/gitsigns/cli.lua b/lua/gitsigns/cli.lua index cd90fef..89fb17a 100644 --- a/lua/gitsigns/cli.lua +++ b/lua/gitsigns/cli.lua @@ -12,9 +12,9 @@ local attach = require('gitsigns.attach') local gs_debug = require('gitsigns.debug') local sources = { - [actions] = true, - [attach] = false, - [gs_debug] = false, + [actions] = true, + [attach] = false, + [gs_debug] = false, } -- try to parse each argument as a lua boolean, nil or number, if fails then @@ -25,85 +25,87 @@ local sources = { -- '100' -> 100 -- 'HEAD~300' -> 'HEAD~300' local function parse_to_lua(a) - if tonumber(a) then - return tonumber(a) - elseif a == 'false' or a == 'true' then - return a == 'true' - elseif a == 'nil' then - return nil - end - return a + if tonumber(a) then + return tonumber(a) + elseif a == 'false' or a == 'true' then + return a == 'true' + elseif a == 'nil' then + return nil + end + return a end local M = {} - - function M.complete(arglead, line) - local words = vim.split(line, '%s+') - local n = #words + local words = vim.split(line, '%s+') + local n = #words - local matches = {} - if n == 2 then - for m, _ in pairs(sources) do - for func, _ in pairs(m) do - if not func:match('^[a-z]') then - -- exclude - elseif vim.startswith(func, arglead) then - table.insert(matches, func) - end - end + local matches = {} + if n == 2 then + for m, _ in pairs(sources) do + for func, _ in pairs(m) do + if not func:match('^[a-z]') then + -- exclude + elseif vim.startswith(func, arglead) then + table.insert(matches, func) + end end - elseif n > 2 then - -- Subcommand completion - local cmp_func = actions._get_cmp_func(words[2]) - if cmp_func then - return cmp_func(arglead) - end - end - return matches + end + elseif n > 2 then + -- Subcommand completion + local cmp_func = actions._get_cmp_func(words[2]) + if cmp_func then + return cmp_func(arglead) + end + end + return matches end local function print_nonnil(x) - if x ~= nil then - print(vim.inspect(x)) - end + if x ~= nil then + print(vim.inspect(x)) + end end M.run = void(function(params) - local __FUNC__ = 'cli.run' - local pos_args_raw, named_args_raw = parse_args(params.args) + local __FUNC__ = 'cli.run' + local pos_args_raw, named_args_raw = parse_args(params.args) - local func = pos_args_raw[1] + local func = pos_args_raw[1] - if not func then - func = async.wrap(vim.ui.select, 3)(M.complete('', 'Gitsigns '), {}) - end + if not func then + 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 pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2)) + local named_args = vim.tbl_map(parse_to_lua, named_args_raw) + local args = vim.tbl_extend('error', pos_args, named_args) - dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, { newline = ' ', indent = '' })) + dprintf( + "Running action '%s' with arguments %s", + func, + vim.inspect(args, { newline = ' ', indent = '' }) + ) - local cmd_func = actions._get_cmd_func(func) - if cmd_func then - -- Action has a specialised mapping function from command form to lua - -- function - print_nonnil(cmd_func(args, params)) + local cmd_func = actions._get_cmd_func(func) + if cmd_func then + -- Action has a specialised mapping function from command form to lua + -- function + print_nonnil(cmd_func(args, params)) + return + end + + for m, has_named in pairs(sources) do + local f = (m)[func] + if type(f) == 'function' then + -- Note functions here do not have named arguments + print_nonnil(f(unpack(pos_args), has_named and named_args or nil)) return - end + end + end - for m, has_named in pairs(sources) do - local f = (m)[func] - if type(f) == "function" then - -- Note functions here do not have named arguments - print_nonnil(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) + message.error('%s is not a valid function or action', func) end) return M diff --git a/lua/gitsigns/cli/argparse.lua b/lua/gitsigns/cli/argparse.lua index 3da753d..8b465b5 100644 --- a/lua/gitsigns/cli/argparse.lua +++ b/lua/gitsigns/cli/argparse.lua @@ -1,115 +1,105 @@ local M = {} - - local function is_char(x) - return x:match('[^=\'"%s]') ~= nil + return x:match('[^=\'"%s]') ~= nil end - - - - - - - - -- Return positional arguments and named arguments function M.parse_args(x) - local pos_args, named_args = {}, {} + local pos_args, named_args = {}, {} - local state = 'in_arg' - local cur_arg = '' - local cur_val = '' - local cur_quote = '' + local state = 'in_arg' + local cur_arg = '' + local cur_val = '' + local cur_quote = '' - local function peek(idx) - return x:sub(idx + 1, idx + 1) - end + local function peek(idx) + return x:sub(idx + 1, idx + 1) + end - local i = 1 - while i <= #x do - local ch = x:sub(i, i) - -- dprintf('L(%d)(%s): cur_arg="%s" ch="%s"', i, state, cur_arg, ch) + local i = 1 + while i <= #x do + local ch = x:sub(i, i) + -- dprintf('L(%d)(%s): cur_arg="%s" ch="%s"', i, state, cur_arg, ch) - if state == 'in_arg' then - if is_char(ch) then - if ch == '-' and peek(i) == '-' then - state = 'in_flag' - cur_arg = '' - i = i + 1 - else - cur_arg = cur_arg .. ch - end - elseif ch:match('%s') then - pos_args[#pos_args + 1] = cur_arg - state = 'in_ws' - elseif ch == '=' then - cur_val = '' - local next_ch = peek(i) - if next_ch == "'" or next_ch == '"' then - cur_quote = next_ch - i = i + 1 - state = 'in_quote' - else - state = 'in_value' - end - end - elseif state == 'in_flag' then - if ch:match('%s') then - named_args[cur_arg] = true - state = 'in_ws' - else - cur_arg = cur_arg .. ch - end - elseif state == 'in_ws' then - if is_char(ch) then - if ch == '-' and peek(i) == '-' then - state = 'in_flag' - cur_arg = '' - i = i + 1 - else - state = 'in_arg' - cur_arg = ch - end - end - elseif state == 'in_value' then - if is_char(ch) then - cur_val = cur_val .. ch - elseif ch:match('%s') then - named_args[cur_arg] = cur_val - cur_arg = '' - state = 'in_ws' - end - elseif state == 'in_quote' then - local next_ch = peek(i) - if ch == "\\" and next_ch == cur_quote then - cur_val = cur_val .. next_ch - i = i + 1 - elseif ch == cur_quote then - named_args[cur_arg] = cur_val - state = 'in_ws' - if next_ch ~= '' and not next_ch:match('%s') then - error('malformed argument: ' .. next_ch) - end - else - cur_val = cur_val .. ch - end + if state == 'in_arg' then + if is_char(ch) then + if ch == '-' and peek(i) == '-' then + state = 'in_flag' + cur_arg = '' + i = i + 1 + else + cur_arg = cur_arg .. ch + end + elseif ch:match('%s') then + pos_args[#pos_args + 1] = cur_arg + state = 'in_ws' + elseif ch == '=' then + cur_val = '' + local next_ch = peek(i) + if next_ch == "'" or next_ch == '"' then + cur_quote = next_ch + i = i + 1 + state = 'in_quote' + else + state = 'in_value' + end end - i = i + 1 - end - - if #cur_arg > 0 then - if state == 'in_arg' then - pos_args[#pos_args + 1] = cur_arg - elseif state == 'in_flag' then - named_args[cur_arg] = true - elseif state == 'in_value' then - named_args[cur_arg] = cur_val + elseif state == 'in_flag' then + if ch:match('%s') then + named_args[cur_arg] = true + state = 'in_ws' + else + cur_arg = cur_arg .. ch end - end + elseif state == 'in_ws' then + if is_char(ch) then + if ch == '-' and peek(i) == '-' then + state = 'in_flag' + cur_arg = '' + i = i + 1 + else + state = 'in_arg' + cur_arg = ch + end + end + elseif state == 'in_value' then + if is_char(ch) then + cur_val = cur_val .. ch + elseif ch:match('%s') then + named_args[cur_arg] = cur_val + cur_arg = '' + state = 'in_ws' + end + elseif state == 'in_quote' then + local next_ch = peek(i) + if ch == '\\' and next_ch == cur_quote then + cur_val = cur_val .. next_ch + i = i + 1 + elseif ch == cur_quote then + named_args[cur_arg] = cur_val + state = 'in_ws' + if next_ch ~= '' and not next_ch:match('%s') then + error('malformed argument: ' .. next_ch) + end + else + cur_val = cur_val .. ch + end + end + i = i + 1 + end - return pos_args, named_args + if #cur_arg > 0 then + if state == 'in_arg' then + pos_args[#pos_args + 1] = cur_arg + elseif state == 'in_flag' then + named_args[cur_arg] = true + elseif state == 'in_value' then + named_args[cur_arg] = cur_val + end + end + + return pos_args, named_args end return M diff --git a/lua/gitsigns/config.lua b/lua/gitsigns/config.lua index 29f9569..0d5e585 100644 --- a/lua/gitsigns/config.lua +++ b/lua/gitsigns/config.lua @@ -1,10 +1,10 @@ local warn do - -- this is included in gen_help.lua so don't error if requires fail - local ok, ret = pcall(require, 'gitsigns.message') - if ok then - warn = ret.warn - end + -- this is included in gen_help.lua so don't error if requires fail + local ok, ret = pcall(require, 'gitsigns.message') + if ok then + warn = ret.warn + end end --- @class Gitsigns.SchemaElem @@ -87,153 +87,60 @@ end --- @field _verbose boolean --- @field _test_mode boolean - - - - - - - - - - - - - - - - - -local M = {Config = {DiffOpts = {}, SignConfig = {}, watch_gitdir = {}, current_line_blame_formatter_opts = {}, current_line_blame_opts = {}, yadm = {}, Worktree = {}, }, } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- Undocumented - - - - - - - - - - - - - - - +local M = { + Config = { + DiffOpts = {}, + SignConfig = {}, + watch_gitdir = {}, + current_line_blame_formatter_opts = {}, + current_line_blame_opts = {}, + yadm = {}, + Worktree = {}, + }, +} --- @type Gitsigns.Config M.config = {} --- @type table M.schema = { - signs = { - type = 'table', - deep_extend = true, - default = { - add = { hl = 'GitSignsAdd', text = '┃', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn' }, - change = { hl = 'GitSignsChange', text = '┃', numhl = 'GitSignsChangeNr', linehl = 'GitSignsChangeLn' }, - delete = { hl = 'GitSignsDelete', text = '▁', numhl = 'GitSignsDeleteNr', linehl = 'GitSignsDeleteLn' }, - topdelete = { hl = 'GitSignsTopdelete', text = '▔', numhl = 'GitSignsTopdeleteNr', linehl = 'GitSignsTopdeleteLn' }, - changedelete = { hl = 'GitSignsChangedelete', text = '~', numhl = 'GitSignsChangedeleteNr', linehl = 'GitSignsChangedeleteLn' }, - untracked = { hl = 'GitSignsUntracked', text = '┆', numhl = 'GitSignsUntrackedNr', linehl = 'GitSignsUntrackedLn' }, + signs = { + type = 'table', + deep_extend = true, + default = { + add = { hl = 'GitSignsAdd', text = '┃', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn' }, + change = { + hl = 'GitSignsChange', + text = '┃', + numhl = 'GitSignsChangeNr', + linehl = 'GitSignsChangeLn', }, - default_help = [[{ + delete = { + hl = 'GitSignsDelete', + text = '▁', + numhl = 'GitSignsDeleteNr', + linehl = 'GitSignsDeleteLn', + }, + topdelete = { + hl = 'GitSignsTopdelete', + text = '▔', + numhl = 'GitSignsTopdeleteNr', + linehl = 'GitSignsTopdeleteLn', + }, + changedelete = { + hl = 'GitSignsChangedelete', + text = '~', + numhl = 'GitSignsChangedeleteNr', + linehl = 'GitSignsChangedeleteLn', + }, + untracked = { + hl = 'GitSignsUntracked', + text = '┆', + numhl = 'GitSignsUntrackedNr', + linehl = 'GitSignsUntrackedLn', + }, + }, + default_help = [[{ add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, @@ -241,7 +148,7 @@ M.schema = { changedelete = { text = '~' }, untracked = { text = '┆' }, }]], - description = [[ + description = [[ Configuration for signs: • `text` specifies the character to use for the sign. • `show_count` to enable showing count of hunk, e.g. number of deleted @@ -255,49 +162,74 @@ M.schema = { See |gitsigns-highlight-groups|. ]], - }, + }, - _signs_staged = { - type = 'table', - deep_extend = true, - default = { - add = { hl = 'GitSignsStagedAdd', text = '┃', numhl = 'GitSignsStagedAddNr', linehl = 'GitSignsStagedAddLn' }, - change = { hl = 'GitSignsStagedChange', text = '┃', numhl = 'GitSignsStagedChangeNr', linehl = 'GitSignsStagedChangeLn' }, - delete = { hl = 'GitSignsStagedDelete', text = '▁', numhl = 'GitSignsStagedDeleteNr', linehl = 'GitSignsStagedDeleteLn' }, - topdelete = { hl = 'GitSignsStagedTopdelete', text = '▔', numhl = 'GitSignsStagedTopdeleteNr', linehl = 'GitSignsStagedTopdeleteLn' }, - changedelete = { hl = 'GitSignsStagedChangedelete', text = '~', numhl = 'GitSignsStagedChangedeleteNr', linehl = 'GitSignsStagedChangedeleteLn' }, + _signs_staged = { + type = 'table', + deep_extend = true, + default = { + add = { + hl = 'GitSignsStagedAdd', + text = '┃', + numhl = 'GitSignsStagedAddNr', + linehl = 'GitSignsStagedAddLn', }, - default_help = [[{ + change = { + hl = 'GitSignsStagedChange', + text = '┃', + numhl = 'GitSignsStagedChangeNr', + linehl = 'GitSignsStagedChangeLn', + }, + delete = { + hl = 'GitSignsStagedDelete', + text = '▁', + numhl = 'GitSignsStagedDeleteNr', + linehl = 'GitSignsStagedDeleteLn', + }, + topdelete = { + hl = 'GitSignsStagedTopdelete', + text = '▔', + numhl = 'GitSignsStagedTopdeleteNr', + linehl = 'GitSignsStagedTopdeleteLn', + }, + changedelete = { + hl = 'GitSignsStagedChangedelete', + text = '~', + numhl = 'GitSignsStagedChangedeleteNr', + linehl = 'GitSignsStagedChangedeleteLn', + }, + }, + default_help = [[{ add = { text = '┃' }, change = { text = '┃' }, delete = { text = '▁' }, topdelete = { text = '▔' }, changedelete = { text = '~' }, }]], - description = [[ - Configuration for signs of staged hunks. + description = [[ + Configuration for signs of staged hunks. - See |gitsigns-config-signs|. + See |gitsigns-config-signs|. ]], - }, + }, - _signs_staged_enable = { - type = 'boolean', - default = false, - description = [[ - Show signs for staged hunks. + _signs_staged_enable = { + type = 'boolean', + default = false, + description = [[ + Show signs for staged hunks. - When enabled the signs defined in |git-config-signs_staged|` are used. + When enabled the signs defined in |git-config-signs_staged|` are used. ]], - }, + }, - keymaps = { - deprecated = { - message = "config.keymaps is now deprecated. Please define mappings in config.on_attach() instead.", - }, - type = 'table', - default = {}, - description = [[ + keymaps = { + deprecated = { + message = 'config.keymaps is now deprecated. Please define mappings in config.on_attach() instead.', + }, + type = 'table', + default = {}, + description = [[ Keymaps to set up when attaching to a buffer. Each key in the table defines the mode and key (whitespace delimited) @@ -311,13 +243,13 @@ M.schema = { mappings defined in this field can be disabled by setting the whole field to `{}`, and |gitsigns-config-on_attach| can instead be used to define mappings. - ]], - }, + ]], + }, - worktrees = { - type = 'table', - default = nil, - description = [[ + worktrees = { + type = 'table', + default = nil, + description = [[ Detached working trees. Array of tables with the keys `gitdir` and `toplevel`. @@ -333,12 +265,12 @@ M.schema = { } } ]], - }, + }, - _on_attach_pre = { - type = 'function', - default = nil, - description = [[ + _on_attach_pre = { + type = 'function', + default = nil, + description = [[ Asynchronous hook called before attaching to a buffer. Mainly used to configure detached worktrees. @@ -346,21 +278,21 @@ M.schema = { accept an optional table argument with the keys: 'gitdir' and 'toplevel'. Example: > - on_attach_pre = function(bufnr, callback) - ... - callback { - gitdir = ..., - toplevel = ... - } - end -< + on_attach_pre = function(bufnr, callback) + ... + callback { + gitdir = ..., + toplevel = ... + } + end + < ]], - }, + }, - on_attach = { - type = 'function', - default = nil, - description = [[ + on_attach = { + type = 'function', + default = nil, + description = [[ Callback called when attaching to a buffer. Mainly used to setup keymaps when `config.keymaps` is empty. The buffer number is passed as the first argument. @@ -380,17 +312,17 @@ M.schema = { end < ]], - }, + }, - watch_gitdir = { - type = 'table', - deep_extend = true, - default = { - enable = true, - interval = 1000, - follow_files = true, - }, - description = [[ + watch_gitdir = { + type = 'table', + deep_extend = true, + default = { + enable = true, + interval = 1000, + follow_files = true, + }, + description = [[ When opening a file, a libuv watcher is placed on the respective `.git` directory to detect when changes happen to use as a trigger to update signs. @@ -405,91 +337,91 @@ M.schema = { • `follow_files`: If a file is moved with `git mv`, switch the buffer to the new location. ]], - }, + }, - sign_priority = { - type = 'number', - default = 6, - description = [[ + sign_priority = { + type = 'number', + default = 6, + description = [[ Priority to use for signs. ]], - }, + }, - signcolumn = { - type = 'boolean', - default = true, - description = [[ + signcolumn = { + type = 'boolean', + default = true, + description = [[ Enable/disable symbols in the sign column. When enabled the highlights defined in `signs.*.hl` and symbols defined in `signs.*.text` are used. ]], - }, + }, - numhl = { - type = 'boolean', - default = false, - description = [[ + numhl = { + type = 'boolean', + default = false, + description = [[ Enable/disable line number highlights. When enabled the highlights defined in `signs.*.numhl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. ]], - }, + }, - linehl = { - type = 'boolean', - default = false, - description = [[ + linehl = { + type = 'boolean', + default = false, + description = [[ Enable/disable line highlights. When enabled the highlights defined in `signs.*.linehl` are used. If the highlight group does not exist, then it is automatically defined and linked to the corresponding highlight group in `signs.*.hl`. ]], - }, + }, - show_deleted = { - type = 'boolean', - default = false, - description = [[ + show_deleted = { + type = 'boolean', + default = false, + description = [[ Show the old version of hunks inline in the buffer (via virtual lines). Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`. ]], - }, + }, - diff_opts = { - type = 'table', - deep_extend = true, - default = function() - local r = { - algorithm = 'myers', - internal = false, - indent_heuristic = false, - vertical = true, - linematch = nil, - } - for _, o in ipairs(vim.opt.diffopt:get()) do - if o == 'indent-heuristic' then - r.indent_heuristic = true - elseif o == 'internal' then - if vim.diff then - r.internal = true - end - elseif o == 'horizontal' then - r.vertical = false - elseif vim.startswith(o, 'algorithm:') then - r.algorithm = string.sub(o, ('algorithm:'):len() + 1) - elseif vim.startswith(o, 'linematch:') then - r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1)) - end - end - return r - end, - default_help = "derived from 'diffopt'", - description = [[ + diff_opts = { + type = 'table', + deep_extend = true, + default = function() + local r = { + algorithm = 'myers', + internal = false, + indent_heuristic = false, + vertical = true, + linematch = nil, + } + for _, o in ipairs(vim.opt.diffopt:get()) do + if o == 'indent-heuristic' then + r.indent_heuristic = true + elseif o == 'internal' then + if vim.diff then + r.internal = true + end + elseif o == 'horizontal' then + r.vertical = false + elseif vim.startswith(o, 'algorithm:') then + r.algorithm = string.sub(o, ('algorithm:'):len() + 1) + elseif vim.startswith(o, 'linematch:') then + r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1)) + end + end + return r + end, + default_help = "derived from 'diffopt'", + description = [[ Diff options. Fields: ~ @@ -511,33 +443,33 @@ M.schema = { Enable second-stage diff on hunks to align lines. Requires `internal=true`. ]], - }, + }, - base = { - type = 'string', - default = nil, - default_help = 'index', - description = [[ + base = { + type = 'string', + default = nil, + default_help = 'index', + description = [[ The object/revision to diff against. See |gitsigns-revision|. ]], - }, + }, - count_chars = { - type = 'table', - default = { - [1] = '1', -- '₁', - [2] = '2', -- '₂', - [3] = '3', -- '₃', - [4] = '4', -- '₄', - [5] = '5', -- '₅', - [6] = '6', -- '₆', - [7] = '7', -- '₇', - [8] = '8', -- '₈', - [9] = '9', -- '₉', - ['+'] = '>', -- '₊', - }, - description = [[ + count_chars = { + type = 'table', + default = { + [1] = '1', -- '₁', + [2] = '2', -- '₂', + [3] = '3', -- '₃', + [4] = '4', -- '₄', + [5] = '5', -- '₅', + [6] = '6', -- '₆', + [7] = '7', -- '₇', + [8] = '8', -- '₈', + [9] = '9', -- '₉', + ['+'] = '>', -- '₊', + }, + description = [[ The count characters used when `signs.*.show_count` is enabled. The `+` entry is used as a fallback. With the default, any count outside of 1-9 uses the `>` character in the sign. @@ -546,19 +478,25 @@ M.schema = { • to specify unicode characters for the counts instead of 1-9. • to define characters to be used for counts greater than 9. ]], - }, + }, - status_formatter = { - type = 'function', - default = function(status) - local added, changed, removed = status.added, status.changed, status.removed - local status_txt = {} - if added and added > 0 then table.insert(status_txt, '+' .. added) end - if changed and changed > 0 then table.insert(status_txt, '~' .. changed) end - if removed and removed > 0 then table.insert(status_txt, '-' .. removed) end - return table.concat(status_txt, ' ') - end, - default_help = [[function(status) + status_formatter = { + type = 'function', + default = function(status) + local added, changed, removed = status.added, status.changed, status.removed + local status_txt = {} + if added and added > 0 then + table.insert(status_txt, '+' .. added) + end + if changed and changed > 0 then + table.insert(status_txt, '~' .. changed) + end + if removed and removed > 0 then + table.insert(status_txt, '-' .. removed) + end + return table.concat(status_txt, ' ') + end, + default_help = [[function(status) local added, changed, removed = status.added, status.changed, status.removed local status_txt = {} if added and added > 0 then table.insert(status_txt, '+'..added ) end @@ -566,72 +504,72 @@ M.schema = { if removed and removed > 0 then table.insert(status_txt, '-'..removed) end return table.concat(status_txt, ' ') end]], - description = [[ + description = [[ Function used to format `b:gitsigns_status`. ]], - }, + }, - max_file_length = { - type = 'number', - default = 40000, - description = [[ + max_file_length = { + type = 'number', + default = 40000, + description = [[ Max file length (in lines) to attach to. ]], - }, + }, - preview_config = { - type = 'table', - deep_extend = true, - default = { - border = 'single', - style = 'minimal', - relative = 'cursor', - row = 0, - col = 1, - }, - description = [[ + preview_config = { + type = 'table', + deep_extend = true, + default = { + border = 'single', + style = 'minimal', + relative = 'cursor', + row = 0, + col = 1, + }, + description = [[ Option overrides for the Gitsigns preview window. Table is passed directly to `nvim_open_win`. ]], - }, + }, - attach_to_untracked = { - type = 'boolean', - default = true, - description = [[ + attach_to_untracked = { + type = 'boolean', + default = true, + description = [[ Attach to untracked files. ]], - }, + }, - update_debounce = { - type = 'number', - default = 100, - description = [[ + update_debounce = { + type = 'number', + default = 100, + description = [[ Debounce time for updates (in milliseconds). ]], - }, + }, - current_line_blame = { - type = 'boolean', - default = false, - description = [[ + current_line_blame = { + type = 'boolean', + default = false, + description = [[ Adds an unobtrusive and customisable blame annotation at the end of the current line. The highlight group used for the text is `GitSignsCurrentLineBlame`. ]], - }, + }, - current_line_blame_opts = { - type = 'table', - deep_extend = true, - default = { - virt_text = true, - virt_text_pos = 'eol', - virt_text_priority = 100, - delay = 1000, - }, - description = [[ + current_line_blame_opts = { + type = 'table', + deep_extend = true, + default = { + virt_text = true, + virt_text_pos = 'eol', + virt_text_priority = 100, + delay = 1000, + }, + description = [[ Options for the current line blame annotation. Fields: ~ @@ -651,27 +589,27 @@ M.schema = { • virt_text_priority: integer Priority of virtual text. ]], - }, + }, - current_line_blame_formatter_opts = { - type = 'table', - deep_extend = true, - deprecated = true, - default = { - relative_time = false, - }, - description = [[ + current_line_blame_formatter_opts = { + type = 'table', + deep_extend = true, + deprecated = true, + default = { + relative_time = false, + }, + description = [[ Options for the current line blame annotation formatter. Fields: ~ • relative_time: boolean ]], - }, + }, - current_line_blame_formatter = { - type = { 'string', 'function' }, - default = ' , - ', - description = [[ + current_line_blame_formatter = { + type = { 'string', 'function' }, + default = ' , - ', + description = [[ String or function used to format the virtual text of |gitsigns-config-current_line_blame|. @@ -746,65 +684,65 @@ M.schema = { field of |nvim_buf_set_extmark| and thus must be a list of [text, highlight] tuples. ]], - }, + }, - current_line_blame_formatter_nc = { - type = { 'string', 'function' }, - default = ' ', - description = [[ + current_line_blame_formatter_nc = { + type = { 'string', 'function' }, + default = ' ', + description = [[ String or function used to format the virtual text of |gitsigns-config-current_line_blame| for lines that aren't committed. See |gitsigns-config-current_line_blame_formatter| for more information. ]], - }, + }, - trouble = { - type = 'boolean', - default = function() - local has_trouble = pcall(require, 'trouble') - return has_trouble - end, - default_help = "true if installed", - description = [[ + trouble = { + type = 'boolean', + default = function() + local has_trouble = pcall(require, 'trouble') + return has_trouble + end, + default_help = 'true if installed', + description = [[ When using setqflist() or setloclist(), open Trouble instead of the quickfix/location list window. ]], - }, + }, - yadm = { - type = 'table', - default = { enable = false }, - description = [[ + yadm = { + type = 'table', + default = { enable = false }, + description = [[ yadm configuration. ]], - }, + }, - _git_version = { - type = 'string', - default = 'auto', - description = [[ + _git_version = { + type = 'string', + default = 'auto', + description = [[ Version of git available. Set to 'auto' to automatically detect. ]], - }, + }, - _verbose = { - type = 'boolean', - default = false, - description = [[ + _verbose = { + type = 'boolean', + default = false, + description = [[ More verbose debug message. Requires debug_mode=true. ]], - }, + }, - _test_mode = { - type = 'boolean', - default = false, - }, + _test_mode = { + type = 'boolean', + default = false, + }, - word_diff = { - type = 'boolean', - default = false, - description = [[ + word_diff = { + type = 'boolean', + default = false, + description = [[ Highlight intra-line word differences in the buffer. Requires `config.diff_opts.internal = true` . @@ -822,142 +760,141 @@ M.schema = { • `GitSignsChangeVirtLnInline` • `GitSignsDeleteVirtLnInline` ]], - }, + }, - _refresh_staged_on_update = { - type = 'boolean', - default = false, - description = [[ + _refresh_staged_on_update = { + type = 'boolean', + default = false, + description = [[ Always refresh the staged file on each update. Disabling this will cause the staged file to only be refreshed when an update to the index is detected. ]], - }, + }, - _blame_cache = { - type = 'boolean', - default = true, - description = [[ + _blame_cache = { + type = 'boolean', + default = true, + description = [[ Cache blame results for current_line_blame ]], - }, + }, - _threaded_diff = { - type = 'boolean', - default = false, - description = [[ + _threaded_diff = { + type = 'boolean', + default = false, + description = [[ Run diffs on a separate thread ]], - }, + }, - _inline2 = { - type = 'boolean', - default = false, - description = [[ + _inline2 = { + type = 'boolean', + default = false, + description = [[ Enable enhanced version of preview_hunk_inline() ]], - }, + }, - _extmark_signs = { - type = 'boolean', - default = false, - description = [[ + _extmark_signs = { + type = 'boolean', + default = false, + description = [[ Use extmarks for placing signs. ]], - }, + }, - debug_mode = { - type = 'boolean', - default = false, - description = [[ + debug_mode = { + type = 'boolean', + default = false, + description = [[ Enables debug logging and makes the following functions available: `dump_cache`, `debug_messages`, `clear_debug`. ]], - }, - + }, } warn = function(s, ...) - vim.notify(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' }) + vim.notify(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' }) end --- @param config Gitsigns.Config local function validate_config(config) - for k, v in pairs(config) do - local kschema = M.schema[k] - if kschema == nil then - warn("gitsigns: Ignoring invalid configuration field '%s'", k) - elseif kschema.type then - if type(kschema.type) == 'string' then - vim.validate({ - [k] = { v, kschema.type }, - }) - end + for k, v in pairs(config) do + local kschema = M.schema[k] + if kschema == nil then + warn("gitsigns: Ignoring invalid configuration field '%s'", k) + elseif kschema.type then + if type(kschema.type) == 'string' then + vim.validate({ + [k] = { v, kschema.type }, + }) end - end + end + end end local function resolve_default(v) - if type(v.default) == 'function' and v.type ~= 'function' then - return (v.default)() - else - return v.default - end + if type(v.default) == 'function' and v.type ~= 'function' then + return (v.default)() + else + return v.default + end end local function handle_deprecated(cfg) - for k, v in pairs(M.schema) do - local dep = v.deprecated - if dep and cfg[k] ~= nil then - if type(dep) == "table" then - if dep.new_field then - local opts_key, field = dep.new_field:match('(.*)%.(.*)') - if opts_key and field then - -- Field moved to an options table - local opts = (cfg[opts_key] or {}) - opts[field] = cfg[k] - cfg[opts_key] = opts - else - -- Field renamed - cfg[dep.new_field] = cfg[k] - end - end + for k, v in pairs(M.schema) do + local dep = v.deprecated + if dep and cfg[k] ~= nil then + if type(dep) == 'table' then + if dep.new_field then + local opts_key, field = dep.new_field:match('(.*)%.(.*)') + if opts_key and field then + -- Field moved to an options table + local opts = (cfg[opts_key] or {}) + opts[field] = cfg[k] + cfg[opts_key] = opts + else + -- Field renamed + cfg[dep.new_field] = cfg[k] + end + end - if dep.hard then - if dep.message then - warn(dep.message) - elseif dep.new_field then - warn('%s is now deprecated, please use %s', k, dep.new_field) - else - warn('%s is now deprecated; ignoring', k) - end - end - end + if dep.hard then + if dep.message then + warn(dep.message) + elseif dep.new_field then + warn('%s is now deprecated, please use %s', k, dep.new_field) + else + warn('%s is now deprecated; ignoring', k) + end + end end - end + end + end end --- @param user_config Gitsigns.Config function M.build(user_config) - user_config = user_config or {} + user_config = user_config or {} - handle_deprecated(user_config) + handle_deprecated(user_config) - validate_config(user_config) + validate_config(user_config) - local config = M.config - for k, v in pairs(M.schema) do - if user_config[k] ~= nil then - if v.deep_extend then - local d = resolve_default(v) - config[k] = vim.tbl_deep_extend('force', d, user_config[k]) - else - config[k] = user_config[k] - end + local config = M.config + for k, v in pairs(M.schema) do + if user_config[k] ~= nil then + if v.deep_extend then + local d = resolve_default(v) + config[k] = vim.tbl_deep_extend('force', d, user_config[k]) else - config[k] = resolve_default(v) + config[k] = user_config[k] end - end + else + config[k] = resolve_default(v) + end + end end return M diff --git a/lua/gitsigns/current_line_blame.lua b/lua/gitsigns/current_line_blame.lua index f9ca2ce..c53fc40 100644 --- a/lua/gitsigns/current_line_blame.lua +++ b/lua/gitsigns/current_line_blame.lua @@ -19,196 +19,198 @@ local timer = uv.new_timer(true) local M = {} - - local wait_timer = wrap(vim.loop.timer_start, 4) local function set_extmark(bufnr, row, opts) - opts = opts or {} - opts.id = 1 - api.nvim_buf_set_extmark(bufnr, namespace, row - 1, 0, opts) + opts = opts or {} + opts.id = 1 + api.nvim_buf_set_extmark(bufnr, namespace, row - 1, 0, opts) end local function get_extmark(bufnr) - local pos = api.nvim_buf_get_extmark_by_id(bufnr, namespace, 1, {}) - if pos[1] then - return pos[1] + 1 - end - return + local pos = api.nvim_buf_get_extmark_by_id(bufnr, namespace, 1, {}) + if pos[1] then + return pos[1] + 1 + end end local function reset(bufnr) - bufnr = bufnr or current_buf() - if not api.nvim_buf_is_valid(bufnr) then - return - end - api.nvim_buf_del_extmark(bufnr, namespace, 1) - vim.b[bufnr].gitsigns_blame_line_dict = nil + bufnr = bufnr or current_buf() + if not api.nvim_buf_is_valid(bufnr) then + return + end + api.nvim_buf_del_extmark(bufnr, namespace, 1) + vim.b[bufnr].gitsigns_blame_line_dict = nil end -- TODO: expose as config local max_cache_size = 1000 -local BlameCache = {Elem = {}, } - - - - - - - +local BlameCache = { Elem = {} } BlameCache.contents = {} function BlameCache:add(bufnr, lnum, x) - if not config._blame_cache then return end - local scache = self.contents[bufnr] - if scache.size <= max_cache_size then - scache.cache[lnum] = x - scache.size = scache.size + 1 - end + if not config._blame_cache then + return + end + local scache = self.contents[bufnr] + if scache.size <= max_cache_size then + scache.cache[lnum] = x + scache.size = scache.size + 1 + end end function BlameCache:get(bufnr, lnum) - if not config._blame_cache then return end + if not config._blame_cache then + return + end - -- init and invalidate - local tick = vim.b[bufnr].changedtick - if not self.contents[bufnr] or self.contents[bufnr].tick ~= tick then - self.contents[bufnr] = { tick = tick, cache = {}, size = 0 } - end + -- init and invalidate + local tick = vim.b[bufnr].changedtick + if not self.contents[bufnr] or self.contents[bufnr].tick ~= tick then + self.contents[bufnr] = { tick = tick, cache = {}, size = 0 } + end - return self.contents[bufnr].cache[lnum] + return self.contents[bufnr].cache[lnum] end local function expand_blame_format(fmt, name, info) - if info.author == name then - info.author = 'You' - end - return util.expand_format(fmt, info, config.current_line_blame_formatter_opts.relative_time) + if info.author == name then + info.author = 'You' + end + return util.expand_format(fmt, info, config.current_line_blame_formatter_opts.relative_time) end local function flatten_virt_text(virt_text) - local res = {} - for _, part in ipairs(virt_text) do - res[#res + 1] = part[1] - end - return table.concat(res) + local res = {} + for _, part in ipairs(virt_text) do + res[#res + 1] = part[1] + end + return table.concat(res) end -- Update function, must be called in async context local update = void(function() - local bufnr = current_buf() - local lnum = api.nvim_win_get_cursor(0)[1] + local bufnr = current_buf() + local lnum = api.nvim_win_get_cursor(0)[1] - local old_lnum = get_extmark(bufnr) - if old_lnum and lnum == old_lnum and BlameCache:get(bufnr, lnum) then - -- Don't update if on the same line and we already have results - return - end + local old_lnum = get_extmark(bufnr) + if old_lnum and lnum == old_lnum and BlameCache:get(bufnr, lnum) then + -- Don't update if on the same line and we already have results + return + end - if api.nvim_get_mode().mode == 'i' then - reset(bufnr) - return - end + if api.nvim_get_mode().mode == 'i' then + reset(bufnr) + return + end - -- Set an empty extmark to save the line number. - -- This will also clear virt_text. - -- Only do this if there was already an extmark to avoid clearing the intro - -- text. - if get_extmark(bufnr) then - reset(bufnr) - set_extmark(bufnr, lnum) - end + -- Set an empty extmark to save the line number. + -- This will also clear virt_text. + -- Only do this if there was already an extmark to avoid clearing the intro + -- text. + if get_extmark(bufnr) then + reset(bufnr) + set_extmark(bufnr, lnum) + end - -- Can't show extmarks on folded lines so skip - if vim.fn.foldclosed(lnum) ~= -1 then - return - end + -- Can't show extmarks on folded lines so skip + if vim.fn.foldclosed(lnum) ~= -1 then + return + end - local opts = config.current_line_blame_opts + local opts = config.current_line_blame_opts - -- Note because the same timer is re-used, this call has a debouncing effect. - wait_timer(timer, opts.delay, 0) - scheduler() + -- Note because the same timer is re-used, this call has a debouncing effect. + wait_timer(timer, opts.delay, 0) + scheduler() - local bcache = cache[bufnr] - if not bcache or not bcache.git_obj.object_name then - return - end + local bcache = cache[bufnr] + if not bcache or not bcache.git_obj.object_name then + return + end - local result = BlameCache:get(bufnr, lnum) - if not result then - local buftext = util.buf_lines(bufnr) - result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) - BlameCache:add(bufnr, lnum, result) - scheduler() - end + local result = BlameCache:get(bufnr, lnum) + if not result then + local buftext = util.buf_lines(bufnr) + result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + BlameCache:add(bufnr, lnum, result) + scheduler() + end - local lnum1 = api.nvim_win_get_cursor(0)[1] - if bufnr == current_buf() and lnum ~= lnum1 then - -- Cursor has moved during events; abort - return - end + local lnum1 = api.nvim_win_get_cursor(0)[1] + if bufnr == current_buf() and lnum ~= lnum1 then + -- Cursor has moved during events; abort + return + end - if not api.nvim_buf_is_loaded(bufnr) then - -- Buffer is no longer loaded; abort - return - end + if not api.nvim_buf_is_loaded(bufnr) then + -- Buffer is no longer loaded; abort + return + end - vim.b[bufnr].gitsigns_blame_line_dict = result + vim.b[bufnr].gitsigns_blame_line_dict = result - if result then - local virt_text - local clb_formatter = result.author == 'Not Committed Yet' and - config.current_line_blame_formatter_nc or - config.current_line_blame_formatter - if type(clb_formatter) == "string" then - virt_text = { { - expand_blame_format(clb_formatter, bcache.git_obj.repo.username, result), - 'GitSignsCurrentLineBlame', - }, } - else -- function - virt_text = clb_formatter( - bcache.git_obj.repo.username, - result, - config.current_line_blame_formatter_opts) + if result then + local virt_text + local clb_formatter = result.author == 'Not Committed Yet' + and config.current_line_blame_formatter_nc + or config.current_line_blame_formatter + if type(clb_formatter) == 'string' then + virt_text = { + { + expand_blame_format(clb_formatter, bcache.git_obj.repo.username, result), + 'GitSignsCurrentLineBlame', + }, + } + else -- function + virt_text = clb_formatter( + bcache.git_obj.repo.username, + result, + config.current_line_blame_formatter_opts + ) + end - end + vim.b[bufnr].gitsigns_blame_line = flatten_virt_text(virt_text) - vim.b[bufnr].gitsigns_blame_line = flatten_virt_text(virt_text) - - if opts.virt_text then - set_extmark(bufnr, lnum, { - virt_text = virt_text, - virt_text_pos = opts.virt_text_pos, - priority = opts.virt_text_priority, - hl_mode = 'combine', - }) - end - end + if opts.virt_text then + set_extmark(bufnr, lnum, { + virt_text = virt_text, + virt_text_pos = opts.virt_text_pos, + priority = opts.virt_text_priority, + hl_mode = 'combine', + }) + end + end end) M.setup = function() - local group = api.nvim_create_augroup('gitsigns_blame', {}) + local group = api.nvim_create_augroup('gitsigns_blame', {}) - for k, _ in pairs(cache) do - reset(k) - end + for k, _ in pairs(cache) do + reset(k) + end - if config.current_line_blame then - api.nvim_create_autocmd({ 'FocusGained', 'BufEnter', 'CursorMoved', 'CursorMovedI' }, { - group = group, callback = function() update() end, - }) + if config.current_line_blame then + api.nvim_create_autocmd({ 'FocusGained', 'BufEnter', 'CursorMoved', 'CursorMovedI' }, { + group = group, + callback = function() + update() + end, + }) - api.nvim_create_autocmd({ 'InsertEnter', 'FocusLost', 'BufLeave' }, { - group = group, callback = function() reset() end, - }) + api.nvim_create_autocmd({ 'InsertEnter', 'FocusLost', 'BufLeave' }, { + group = group, + callback = function() + reset() + end, + }) - -- Call via vim.schedule to avoid the debounce timer killing the async - -- coroutine - vim.schedule(update) - end + -- Call via vim.schedule to avoid the debounce timer killing the async + -- coroutine + vim.schedule(update) + end end return M diff --git a/lua/gitsigns/debounce.lua b/lua/gitsigns/debounce.lua index 9346f0a..0102e37 100644 --- a/lua/gitsigns/debounce.lua +++ b/lua/gitsigns/debounce.lua @@ -2,9 +2,6 @@ local uv = require('gitsigns.uv') local M = {} - - - --- Debounces a function on the trailing edge. --- --- @generic F: function @@ -12,17 +9,16 @@ local M = {} --- @param fn F Function to debounce --- @return F Debounced function. function M.debounce_trailing(ms, fn) - local timer = uv.new_timer(true) - return function(...) - local argv = { ... } - timer:start(ms, 0, function() - timer:stop() - fn(unpack(argv)) - end) - end + local timer = uv.new_timer(true) + return function(...) + local argv = { ... } + timer:start(ms, 0, function() + timer:stop() + fn(unpack(argv)) + end) + end end - --- Throttles a function on the leading edge. --- --- @generic F: function @@ -30,18 +26,18 @@ end --- @param fn F Function to throttle --- @return F throttled function. function M.throttle_leading(ms, fn) - local timer = uv.new_timer(true) - local running = false - return function(...) - if not running then - timer:start(ms, 0, function() - running = false - timer:stop() - end) - running = true - fn(...) - end - end + local timer = uv.new_timer(true) + local running = false + return function(...) + if not running then + timer:start(ms, 0, function() + running = false + timer:stop() + end) + running = true + fn(...) + end + end end --- Throttles a function using the first argument as an ID @@ -61,26 +57,26 @@ end --- @param schedule boolean --- @return F throttled function. function M.throttle_by_id(fn, schedule) - local scheduled = {} --- @type table - local running = {} --- @type table - return function(id, ...) - if scheduled[id] then - -- If fn is already scheduled, then drop - return - end - if not running[id] or schedule then - scheduled[id] = true - end - if running[id] then - return - end - while scheduled[id] do - scheduled[id] = nil - running[id] = true - fn(id, ...) - running[id] = nil - end - end + local scheduled = {} --- @type table + local running = {} --- @type table + return function(id, ...) + if scheduled[id] then + -- If fn is already scheduled, then drop + return + end + if not running[id] or schedule then + scheduled[id] = true + end + if running[id] then + return + end + while scheduled[id] do + scheduled[id] = nil + running[id] = true + fn(id, ...) + running[id] = nil + end + end end return M diff --git a/lua/gitsigns/debug.lua b/lua/gitsigns/debug.lua index 3ca804d..073d087 100644 --- a/lua/gitsigns/debug.lua +++ b/lua/gitsigns/debug.lua @@ -6,45 +6,45 @@ local M = {} --- @param path string[] --- @return any local function process(raw_item, path) - if path[#path] == vim.inspect.METATABLE then - return nil - elseif type(raw_item) == "function" then - return nil - elseif type(raw_item) == "table" then - local key = path[#path] - if key == 'compare_text' or key == 'compare_text_head' then - local item = raw_item - return { '...', length = #item, head = item[1] } - elseif not vim.tbl_isempty(raw_item) and key == 'staged_diffs' then - return { '...', length = #vim.tbl_keys(raw_item) } - end - end - return raw_item + if path[#path] == vim.inspect.METATABLE then + return nil + elseif type(raw_item) == 'function' then + return nil + elseif type(raw_item) == 'table' then + local key = path[#path] + if key == 'compare_text' or key == 'compare_text_head' then + local item = raw_item + return { '...', length = #item, head = item[1] } + elseif not vim.tbl_isempty(raw_item) and key == 'staged_diffs' then + return { '...', length = #vim.tbl_keys(raw_item) } + end + end + return raw_item end --- @return any function M.dump_cache() - -- TODO(lewis6991): hack: use package.loaded to avoid circular deps - local cache = (require('gitsigns.cache')).cache - local text = vim.inspect(cache, { process = process }) - vim.api.nvim_echo({ { text } }, false, {}) - return cache + -- TODO(lewis6991): hack: use package.loaded to avoid circular deps + local cache = (require('gitsigns.cache')).cache + local text = vim.inspect(cache, { process = process }) + vim.api.nvim_echo({ { text } }, false, {}) + return cache end --- @param noecho boolean --- @return string[] function M.debug_messages(noecho) - if noecho then - return log.messages - else - for _, m in ipairs(log.messages) do - vim.api.nvim_echo({ { m } }, false, {}) - end - end + if noecho then + return log.messages + else + for _, m in ipairs(log.messages) do + vim.api.nvim_echo({ { m } }, false, {}) + end + end end function M.clear_debug() - log.messages = {} + log.messages = {} end return M diff --git a/lua/gitsigns/debug/log.lua b/lua/gitsigns/debug/log.lua index 3ca2246..157f726 100644 --- a/lua/gitsigns/debug/log.lua +++ b/lua/gitsigns/debug/log.lua @@ -1,109 +1,125 @@ local M = { - debug_mode = false, - verbose = false, - messages = {}, + debug_mode = false, + verbose = false, + messages = {}, } local function getvarvalue(name, lvl) - lvl = lvl + 1 - local value - local found + 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 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 + -- 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] + -- 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) + 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 + 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) + 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) + 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) + 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) + 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) + 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 + 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) + eprint(msg, 1) end function M.eprintf(fmt, ...) - eprint(fmt:format(...), 1) + eprint(fmt:format(...), 1) end return M diff --git a/lua/gitsigns/diff.lua b/lua/gitsigns/diff.lua index 68970c2..8f6c291 100644 --- a/lua/gitsigns/diff.lua +++ b/lua/gitsigns/diff.lua @@ -1,18 +1,17 @@ local config = require('gitsigns.config').config -local Hunk = require('gitsigns.hunks').Hunk return function(a, b, linematch) - local diff_opts = config.diff_opts - local f - if diff_opts.internal then - f = require('gitsigns.diff_int').run_diff - else - f = require('gitsigns.diff_ext').run_diff - end + local diff_opts = config.diff_opts + local f + if diff_opts.internal then + f = require('gitsigns.diff_int').run_diff + else + f = require('gitsigns.diff_ext').run_diff + end - local linematch0 - if linematch ~= false then - linematch0 = diff_opts.linematch - end - return f(a, b, diff_opts.algorithm, diff_opts.indent_heuristic, linematch0) + local linematch0 --- @type boolean? + if linematch ~= false then + linematch0 = diff_opts.linematch + end + return f(a, b, diff_opts.algorithm, diff_opts.indent_heuristic, linematch0) end diff --git a/lua/gitsigns/diff_ext.lua b/lua/gitsigns/diff_ext.lua index e1106d5..dd7abcd 100644 --- a/lua/gitsigns/diff_ext.lua +++ b/lua/gitsigns/diff_ext.lua @@ -1,80 +1,73 @@ local git_diff = require('gitsigns.git').diff -local gs_hunks = require("gitsigns.hunks") +local gs_hunks = require('gitsigns.hunks') local Hunk = gs_hunks.Hunk local util = require('gitsigns.util') local scheduler = require('gitsigns.async').scheduler local M = {} - -- Async function - - +-- Async function local function write_to_file(path, text) - local f, err = io.open(path, 'wb') - if f == nil then - error(err) - end - for _, l in ipairs(text) do - f:write(l) - f:write('\n') - end - f:close() + local f, err = io.open(path, 'wb') + if f == nil then + error(err) + end + for _, l in ipairs(text) do + f:write(l) + f:write('\n') + end + f:close() end -M.run_diff = function( - text_cmp, - text_buf, - diff_algo, - indent_heuristic) +M.run_diff = function(text_cmp, text_buf, diff_algo, indent_heuristic) + local results = {} - local results = {} + -- tmpname must not be called in a callback + if vim.in_fast_event() then + scheduler() + end - -- tmpname must not be called in a callback - if vim.in_fast_event() then - scheduler() - end + local file_buf = util.tmpname() + local file_cmp = util.tmpname() - local file_buf = util.tmpname() - local file_cmp = util.tmpname() + write_to_file(file_buf, text_buf) + write_to_file(file_cmp, text_cmp) - write_to_file(file_buf, text_buf) - write_to_file(file_cmp, text_cmp) + -- Taken from gitgutter, diff.vim: + -- + -- If a file has CRLF line endings and git's core.autocrlf is true, the file + -- in git's object store will have LF line endings. Writing it out via + -- git-show will produce a file with LF line endings. + -- + -- If this last file is one of the files passed to git-diff, git-diff will + -- convert its line endings to CRLF before diffing -- which is what we want + -- but also by default outputs a warning on stderr. + -- + -- warning: LF will be replace by CRLF in . + -- The file will have its original line endings in your working directory. + -- + -- We can safely ignore the warning, we turn it off by passing the '-c + -- "core.safecrlf=false"' argument to git-diff. - -- Taken from gitgutter, diff.vim: - -- - -- If a file has CRLF line endings and git's core.autocrlf is true, the file - -- in git's object store will have LF line endings. Writing it out via - -- git-show will produce a file with LF line endings. - -- - -- If this last file is one of the files passed to git-diff, git-diff will - -- convert its line endings to CRLF before diffing -- which is what we want - -- but also by default outputs a warning on stderr. - -- - -- warning: LF will be replace by CRLF in . - -- The file will have its original line endings in your working directory. - -- - -- We can safely ignore the warning, we turn it off by passing the '-c - -- "core.safecrlf=false"' argument to git-diff. + local out = git_diff(file_cmp, file_buf, indent_heuristic, diff_algo) - local out = git_diff(file_cmp, file_buf, indent_heuristic, diff_algo) - - for _, line in ipairs(out) do - if vim.startswith(line, '@@') then - results[#results + 1] = gs_hunks.parse_diff_line(line) - elseif #results > 0 then - local r = results[#results] - if line:sub(1, 1) == '-' then - r.removed.lines[#r.removed.lines + 1] = line:sub(2) - elseif line:sub(1, 1) == '+' then - r.added.lines[#r.added.lines + 1] = line:sub(2) - end + for _, line in ipairs(out) do + if vim.startswith(line, '@@') then + results[#results + 1] = gs_hunks.parse_diff_line(line) + elseif #results > 0 then + local r = results[#results] + if line:sub(1, 1) == '-' then + r.removed.lines[#r.removed.lines + 1] = line:sub(2) + elseif line:sub(1, 1) == '+' then + r.added.lines[#r.added.lines + 1] = line:sub(2) end - end + end + end - os.remove(file_buf) - os.remove(file_cmp) - return results + os.remove(file_buf) + os.remove(file_cmp) + return results end return M diff --git a/lua/gitsigns/diff_int.lua b/lua/gitsigns/diff_int.lua index 4319543..1367505 100644 --- a/lua/gitsigns/diff_int.lua +++ b/lua/gitsigns/diff_int.lua @@ -1,149 +1,133 @@ -local create_hunk = require("gitsigns.hunks").create_hunk +local create_hunk = require('gitsigns.hunks').create_hunk local Hunk = require('gitsigns.hunks').Hunk local config = require('gitsigns.config').config local async = require('gitsigns.async') local M = {} +local run_diff_xdl = function(fa, fb, algorithm, indent_heuristic, linematch) + local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n') .. '\n' + local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n') .. '\n' - - - - - -local run_diff_xdl = function( - fa, fb, - algorithm, indent_heuristic, - linematch) - - - local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n') .. '\n' - local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n') .. '\n' - - return vim.diff(a, b, { - result_type = 'indices', - algorithm = algorithm, - indent_heuristic = indent_heuristic, - linematch = linematch, - }) + return vim.diff(a, b, { + result_type = 'indices', + algorithm = algorithm, + indent_heuristic = indent_heuristic, + linematch = linematch, + }) end -local run_diff_xdl_async = async.wrap(function( - fa, fb, - algorithm, indent_heuristic, - linematch, - callback) +local run_diff_xdl_async = async.wrap( + function(fa, fb, algorithm, indent_heuristic, linematch, callback) + local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n') .. '\n' + local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n') .. '\n' + vim.loop + .new_work(function(a0, b0, algorithm0, indent_heuristic0, linematch0) + return vim.mpack.encode(vim.diff(a0, b0, { + result_type = 'indices', + algorithm = algorithm0, + indent_heuristic = indent_heuristic0, + linematch = linematch0, + })) + end, function(r) + callback(vim.mpack.decode(r)) + end) + :queue(a, b, algorithm, indent_heuristic, linematch) + end, + 6 +) - local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n') .. '\n' - local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n') .. '\n' +M.run_diff = async.void(function(fa, fb, diff_algo, indent_heuristic, linematch) + local run_diff0 + if config._threaded_diff and vim.is_thread then + run_diff0 = run_diff_xdl_async + else + run_diff0 = run_diff_xdl + end - vim.loop.new_work(function( - a0, b0, - algorithm0, indent_heuristic0, - linematch0) + local results = run_diff0(fa, fb, diff_algo, indent_heuristic, linematch) - return vim.mpack.encode(vim.diff(a0, b0, { - result_type = 'indices', - algorithm = algorithm0, - indent_heuristic = indent_heuristic0, - linematch = linematch0, - })) - end, function(r) - callback(vim.mpack.decode(r)) - end):queue(a, b, algorithm, indent_heuristic, linematch) -end, 6) + local hunks = {} -M.run_diff = async.void(function( - fa, fb, - diff_algo, indent_heuristic, - linematch) - - local run_diff0 - if config._threaded_diff and vim.is_thread then - run_diff0 = run_diff_xdl_async - else - run_diff0 = run_diff_xdl - end - - local results = run_diff0(fa, fb, diff_algo, indent_heuristic, linematch) - - local hunks = {} - - for _, r in ipairs(results) do - local rs, rc, as, ac = unpack(r) - local hunk = create_hunk(rs, rc, as, ac) - if rc > 0 then - for i = rs, rs + rc - 1 do - hunk.removed.lines[#hunk.removed.lines + 1] = fa[i] or '' - end + for _, r in ipairs(results) do + local rs, rc, as, ac = unpack(r) + local hunk = create_hunk(rs, rc, as, ac) + if rc > 0 then + for i = rs, rs + rc - 1 do + hunk.removed.lines[#hunk.removed.lines + 1] = fa[i] or '' end - if ac > 0 then - for i = as, as + ac - 1 do - hunk.added.lines[#hunk.added.lines + 1] = fb[i] or '' - end + end + if ac > 0 then + for i = as, as + ac - 1 do + hunk.added.lines[#hunk.added.lines + 1] = fb[i] or '' end - hunks[#hunks + 1] = hunk - end + end + hunks[#hunks + 1] = hunk + end - return hunks + return hunks end) - - local gaps_between_regions = 5 local function denoise_hunks(hunks) - -- Denoise the hunks - local ret = { hunks[1] } - for j = 2, #hunks do - local h, n = ret[#ret], hunks[j] - if not h or not n then break end - if n.added.start - h.added.start - h.added.count < gaps_between_regions then - h.added.count = n.added.start + n.added.count - h.added.start - h.removed.count = n.removed.start + n.removed.count - h.removed.start + -- Denoise the hunks + local ret = { hunks[1] } + for j = 2, #hunks do + local h, n = ret[#ret], hunks[j] + if not h or not n then + break + end + if n.added.start - h.added.start - h.added.count < gaps_between_regions then + h.added.count = n.added.start + n.added.count - h.added.start + h.removed.count = n.removed.start + n.removed.count - h.removed.start - if h.added.count > 0 or h.removed.count > 0 then - h.type = 'change' - end - else - ret[#ret + 1] = n + if h.added.count > 0 or h.removed.count > 0 then + h.type = 'change' end - end - return ret + else + ret[#ret + 1] = n + end + end + return ret end function M.run_word_diff(removed, added) - local adds = {} - local rems = {} + local adds = {} + local rems = {} - if #removed ~= #added then - return rems, adds - end + if #removed ~= #added then + return rems, adds + end - for i = 1, #removed do - -- pair lines by position - local a, b = vim.split(removed[i], ''), vim.split(added[i], '') + for i = 1, #removed do + -- pair lines by position + local a, b = vim.split(removed[i], ''), vim.split(added[i], '') - local hunks = {} - for _, r in ipairs(run_diff_xdl(a, b)) do - local rs, rc, as, ac = unpack(r) + local hunks = {} + for _, r in ipairs(run_diff_xdl(a, b)) do + local rs, rc, as, ac = unpack(r) - -- Balance of the unknown offset done in hunk_func - if rc == 0 then rs = rs + 1 end - if ac == 0 then as = as + 1 end - - hunks[#hunks + 1] = create_hunk(rs, rc, as, ac) + -- Balance of the unknown offset done in hunk_func + if rc == 0 then + rs = rs + 1 + end + if ac == 0 then + as = as + 1 end - hunks = denoise_hunks(hunks) + hunks[#hunks + 1] = create_hunk(rs, rc, as, ac) + end - for _, h in ipairs(hunks) do - adds[#adds + 1] = { i, h.type, h.added.start, h.added.start + h.added.count } - rems[#rems + 1] = { i, h.type, h.removed.start, h.removed.start + h.removed.count } - end - end - return rems, adds + hunks = denoise_hunks(hunks) + + for _, h in ipairs(hunks) do + adds[#adds + 1] = { i, h.type, h.added.start, h.added.start + h.added.count } + rems[#rems + 1] = { i, h.type, h.removed.start, h.removed.start + h.removed.count } + end + end + return rems, adds end return M diff --git a/lua/gitsigns/diffthis.lua b/lua/gitsigns/diffthis.lua index 2befee0..9de4a49 100644 --- a/lua/gitsigns/diffthis.lua +++ b/lua/gitsigns/diffthis.lua @@ -16,186 +16,179 @@ local throttle_by_id = require('gitsigns.debounce').throttle_by_id local input = awrap(vim.ui.input, 2) -local M = {DiffthisOpts = {}, } - - - - - - - - - +local M = { DiffthisOpts = {} } local bufread = void(function(bufnr, dbufnr, base, bcache) - local comp_rev = bcache:get_compare_rev(util.calc_base(base)) - local text - if util.calc_base(base) == util.calc_base(bcache.base) then - text = bcache.compare_text - else - local err - text, err = bcache.git_obj:get_show_text(comp_rev) - if err then - error(err, 2) - end - scheduler() - if vim.bo[bufnr].fileformat == 'dos' then - text = util.strip_cr(text) - end - end + local comp_rev = bcache:get_compare_rev(util.calc_base(base)) + local text + if util.calc_base(base) == util.calc_base(bcache.base) then + text = bcache.compare_text + else + local err + text, err = bcache.git_obj:get_show_text(comp_rev) + if err then + error(err, 2) + end + scheduler() + if vim.bo[bufnr].fileformat == 'dos' then + text = util.strip_cr(text) + end + end - local modifiable = vim.bo[dbufnr].modifiable - vim.bo[dbufnr].modifiable = true - util.set_lines(dbufnr, 0, -1, text) + local modifiable = vim.bo[dbufnr].modifiable + vim.bo[dbufnr].modifiable = true + util.set_lines(dbufnr, 0, -1, text) - vim.bo[dbufnr].modifiable = modifiable - vim.bo[dbufnr].modified = false - vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype - vim.bo[dbufnr].bufhidden = 'wipe' + vim.bo[dbufnr].modifiable = modifiable + vim.bo[dbufnr].modified = false + vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype + vim.bo[dbufnr].bufhidden = 'wipe' end) local bufwrite = void(function(bufnr, dbufnr, base, bcache) - local buftext = util.buf_lines(dbufnr) - bcache.git_obj:stage_lines(buftext) - scheduler() - vim.bo[dbufnr].modified = false - -- If diff buffer base matches the bcache base then also update the - -- signs. - if util.calc_base(base) == util.calc_base(bcache.base) then - bcache.compare_text = buftext - manager.update(bufnr, bcache) - end + local buftext = util.buf_lines(dbufnr) + bcache.git_obj:stage_lines(buftext) + scheduler() + vim.bo[dbufnr].modified = false + -- If diff buffer base matches the bcache base then also update the + -- signs. + if util.calc_base(base) == util.calc_base(bcache.base) then + bcache.compare_text = buftext + manager.update(bufnr, bcache) + end end) local function run(base, diffthis, opts) - local bufnr = vim.api.nvim_get_current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + local bufnr = vim.api.nvim_get_current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - opts = opts or {} + opts = opts or {} - local comp_rev = bcache:get_compare_rev(util.calc_base(base)) - local bufname = bcache:get_rev_bufname(comp_rev) + local comp_rev = bcache:get_compare_rev(util.calc_base(base)) + local bufname = bcache:get_rev_bufname(comp_rev) - local dbuf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(dbuf, bufname) + local dbuf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(dbuf, bufname) - local ok, err = pcall(bufread, bufnr, dbuf, base, bcache) - if not ok then - message.error(err) - scheduler() - vim.cmd('bdelete') - if diffthis then - vim.cmd('diffoff') - end - return - end + local ok, err = pcall(bufread, bufnr, dbuf, base, bcache) + if not ok then + message.error(err) + scheduler() + vim.cmd('bdelete') + if diffthis then + vim.cmd('diffoff') + end + return + end - if comp_rev == ':0' then - vim.bo[dbuf].buftype = 'acwrite' + if comp_rev == ':0' then + vim.bo[dbuf].buftype = 'acwrite' - api.nvim_create_autocmd('BufReadCmd', { - group = 'gitsigns', - buffer = dbuf, - callback = function() - bufread(bufnr, dbuf, base, bcache) - if diffthis then - vim.cmd('diffthis') - end - end, - }) + api.nvim_create_autocmd('BufReadCmd', { + group = 'gitsigns', + buffer = dbuf, + callback = function() + bufread(bufnr, dbuf, base, bcache) + if diffthis then + vim.cmd('diffthis') + end + end, + }) - api.nvim_create_autocmd('BufWriteCmd', { - group = 'gitsigns', - buffer = dbuf, - callback = function() - bufwrite(bufnr, dbuf, base, bcache) - end, - }) - else - vim.bo[dbuf].buftype = 'nowrite' - vim.bo[dbuf].modifiable = false - end + api.nvim_create_autocmd('BufWriteCmd', { + group = 'gitsigns', + buffer = dbuf, + callback = function() + bufwrite(bufnr, dbuf, base, bcache) + end, + }) + else + vim.bo[dbuf].buftype = 'nowrite' + vim.bo[dbuf].modifiable = false + end - if diffthis then - vim.cmd(table.concat({ - 'keepalt', opts.split or 'aboveleft', - opts.vertical and 'vertical' or '', - 'diffsplit', bufname, - }, ' ')) - else - vim.cmd('edit ' .. bufname) - end + if diffthis then + vim.cmd(table.concat({ + 'keepalt', + opts.split or 'aboveleft', + opts.vertical and 'vertical' or '', + 'diffsplit', + bufname, + }, ' ')) + else + vim.cmd('edit ' .. bufname) + end end M.diffthis = void(function(base, opts) - if vim.wo.diff then - return - end + if vim.wo.diff then + return + end - local bufnr = vim.api.nvim_get_current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end + local bufnr = vim.api.nvim_get_current_buf() + local bcache = cache[bufnr] + if not bcache then + return + end - local cwin = api.nvim_get_current_win() - if not base and bcache.git_obj.has_conflicts then - run(':2', true, opts) - api.nvim_set_current_win(cwin) - opts.split = 'belowright' - run(':3', true, opts) - else - run(base, true, opts) - end - api.nvim_set_current_win(cwin) + local cwin = api.nvim_get_current_win() + if not base and bcache.git_obj.has_conflicts then + run(':2', true, opts) + api.nvim_set_current_win(cwin) + opts.split = 'belowright' + run(':3', true, opts) + else + run(base, true, opts) + end + api.nvim_set_current_win(cwin) end) M.show = void(function(base) - run(base, false) + run(base, false) end) local function should_reload(bufnr) - if not vim.bo[bufnr].modified then - return true - end - local response - while not vim.tbl_contains({ 'O', 'L' }, response) do - response = input({ - prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:', - }) - end - return response == 'L' + if not vim.bo[bufnr].modified then + return true + end + local response + while not vim.tbl_contains({ 'O', 'L' }, response) do + response = input({ + prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:', + }) + end + return response == 'L' end -- This function needs to be throttled as there is a call to vim.ui.input M.update = throttle_by_id(void(function(bufnr) - if not vim.wo.diff then - return - end + if not vim.wo.diff then + return + end - local bcache = cache[bufnr] + local bcache = cache[bufnr] - -- Note this will be the bufname for the currently set base - -- which are the only ones we want to update - local bufname = bcache:get_rev_bufname() + -- Note this will be the bufname for the currently set base + -- which are the only ones we want to update + local bufname = bcache:get_rev_bufname() - for _, w in ipairs(api.nvim_list_wins()) do - if api.nvim_win_is_valid(w) then - local b = api.nvim_win_get_buf(w) - local bname = api.nvim_buf_get_name(b) - if bname == bufname or vim.startswith(bname, 'fugitive://') then - if should_reload(b) then - api.nvim_buf_call(b, function() - vim.cmd('doautocmd BufReadCmd') - vim.cmd('diffthis') - end) - end - end + for _, w in ipairs(api.nvim_list_wins()) do + if api.nvim_win_is_valid(w) then + local b = api.nvim_win_get_buf(w) + local bname = api.nvim_buf_get_name(b) + if bname == bufname or vim.startswith(bname, 'fugitive://') then + if should_reload(b) then + api.nvim_buf_call(b, function() + vim.cmd('doautocmd BufReadCmd') + vim.cmd('diffthis') + end) + end end - end + end + end end)) return M diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index 85b665f..16d2a86 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -1,14 +1,14 @@ local async = require('gitsigns.async') local scheduler = require('gitsigns.async').scheduler -local log = require("gitsigns.debug.log") +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 gs_hunks = require('gitsigns.hunks') local Hunk = gs_hunks.Hunk local uv = vim.loop @@ -18,259 +18,180 @@ local dprint = require('gitsigns.debug.log').dprint local eprint = require('gitsigns.debug.log').eprint local err = require('gitsigns.message').error +-- local extensions +local M = { BlameInfo = {}, Version = {}, RepoInfo = {}, Repo = {}, FileProps = {}, Obj = {} } +-- Info in header +-- Porcelain fields +-- Use for tracking moved files +-- Object has crlf +-- Working copy has crlf - -- local extensions - - - -local M = {BlameInfo = {}, Version = {}, RepoInfo = {}, Repo = {}, FileProps = {}, Obj = {}, } - - -- Info in header - - - - - - -- Porcelain fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- Use for tracking moved files - - - - -- Object has crlf - -- Working copy has crlf - - - - - - - -- Use for tracking moved files - - - - -- Object has crlf - -- Working copy has crlf - - - - - - - - - - - - - - - +-- Use for tracking moved files +-- Object has crlf +-- Working copy has crlf local in_git_dir = function(file) - for _, p in ipairs(vim.split(file, util.path_sep)) do - if p == '.git' then - return true - end - end - return false + for _, p in ipairs(vim.split(file, util.path_sep)) do + if p == '.git' then + return true + end + end + return false end local Obj = M.Obj local Repo = M.Repo local function parse_version(version) - assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: ' .. version) - local ret = {} - local parts = vim.split(version, '%.') - ret.major = tonumber(parts[1]) - ret.minor = tonumber(parts[2]) + assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: ' .. version) + local ret = {} + local parts = vim.split(version, '%.') + ret.major = tonumber(parts[1]) + ret.minor = tonumber(parts[2]) - if parts[3] == 'GIT' then - ret.patch = 0 - else - ret.patch = tonumber(parts[3]) - end + if parts[3] == 'GIT' then + ret.patch = 0 + else + ret.patch = tonumber(parts[3]) + end - return ret + return ret end -- Usage: check_version{2,3} local function check_version(version) - if not M.version then - return false - end - if M.version.major < version[1] then - return false - end - if version[2] and M.version.minor < version[2] then - return false - end - if version[3] and M.version.patch < version[3] then - return false - end - return true + if not M.version then + return false + end + if M.version.major < version[1] then + return false + end + if version[2] and M.version.minor < version[2] then + return false + end + if version[3] and M.version.patch < version[3] then + return false + end + return true end --- @async function M._set_version(version) - if version ~= 'auto' then - M.version = parse_version(version) - return - end + 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 _, _, 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]) + 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 { - '--no-pager', - '--literal-pathspecs', - '-c', 'gc.auto=0', -- Disable auto-packing which emits messages to stderr - unpack(args), - } or args + 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 { + '--no-pager', + '--literal-pathspecs', + '-c', + 'gc.auto=0', -- Disable auto-packing which emits messages to stderr + unpack(args), + } + or args - if not spec.cwd and not uv.cwd() then - spec.cwd = vim.env.HOME - end + if not spec.cwd and not uv.cwd() then + spec.cwd = vim.env.HOME + end - local _, _, stdout, stderr = async.wait(2, subprocess.run_job, spec) + local _, _, stdout, stderr = async.wait(2, subprocess.run_job, spec) - if not spec.suppress_stderr then - if stderr then - local cmd_str = table.concat({ spec.command, unpack(args) }, ' ') - log.eprintf("Received stderr when running command\n'%s':\n%s", cmd_str, stderr) - end - end + if not spec.suppress_stderr then + if stderr then + local cmd_str = table.concat({ spec.command, unpack(args) }, ' ') + log.eprintf("Received stderr when running command\n'%s':\n%s", cmd_str, stderr) + end + end - local stdout_lines = vim.split(stdout or '', '\n', true) + local stdout_lines = vim.split(stdout or '', '\n', true) - -- If stdout ends with a newline, then remove the final empty string after - -- the split - if stdout_lines[#stdout_lines] == '' then - stdout_lines[#stdout_lines] = nil - end + -- If stdout ends with a newline, then remove the final empty string after + -- the split + if stdout_lines[#stdout_lines] == '' then + stdout_lines[#stdout_lines] = nil + end - if log.verbose then - log.vprintf('%d lines:', #stdout_lines) - for i = 1, math.min(10, #stdout_lines) do - log.vprintf('\t%s', stdout_lines[i]) - end - end + if log.verbose then + log.vprintf('%d lines:', #stdout_lines) + for i = 1, math.min(10, #stdout_lines) do + log.vprintf('\t%s', stdout_lines[i]) + end + end - return stdout_lines, stderr + return stdout_lines, stderr end, 2) --- @async function M.diff(file_cmp, file_buf, indent_heuristic, diff_algo) - return git_command({ - '-c', 'core.safecrlf=false', - 'diff', - '--color=never', - '--' .. (indent_heuristic and '' or 'no-') .. 'indent-heuristic', - '--diff-algorithm=' .. diff_algo, - '--patch-with-raw', - '--unified=0', - file_cmp, - file_buf, - }) + return git_command({ + '-c', + 'core.safecrlf=false', + 'diff', + '--color=never', + '--' .. (indent_heuristic and '' or 'no-') .. 'indent-heuristic', + '--diff-algorithm=' .. diff_algo, + '--patch-with-raw', + '--unified=0', + file_cmp, + file_buf, + }) end --- @async local function process_abbrev_head(gitdir, head_str, path, cmd) - if not gitdir then - return head_str - end - if head_str == 'HEAD' then - local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, { - command = cmd or 'git', - suppress_stderr = true, - cwd = path, - })[1] or '' - if log.debug_mode and short_sha ~= '' then - short_sha = 'HEAD' - end - if util.path_exists(gitdir .. '/rebase-merge') or - util.path_exists(gitdir .. '/rebase-apply') then - return short_sha .. '(rebasing)' - end - return short_sha - end - return head_str + if not gitdir then + return head_str + end + if head_str == 'HEAD' then + local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, { + command = cmd or 'git', + suppress_stderr = true, + cwd = path, + })[1] or '' + if log.debug_mode and short_sha ~= '' then + short_sha = 'HEAD' + end + if + util.path_exists(gitdir .. '/rebase-merge') + or util.path_exists(gitdir .. '/rebase-apply') + then + return short_sha .. '(rebasing)' + end + return short_sha + end + return head_str end local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1 @@ -279,61 +200,65 @@ local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') local cygpath_convert if has_cygpath then - cygpath_convert = function(path) - return git_command({ '-aw', path }, { command = 'cygpath' })[1] - end + cygpath_convert = function(path) + return git_command({ '-aw', path }, { command = 'cygpath' })[1] + end end local function normalize_path(path) - if path and has_cygpath and not uv.fs_stat(path) then - -- If on windows and path isn't recognizable as a file, try passing it - -- through cygpath - path = cygpath_convert(path) - end - return path + if path and has_cygpath and not uv.fs_stat(path) then + -- If on windows and path isn't recognizable as a file, try passing it + -- through cygpath + path = cygpath_convert(path) + end + return path end --- @async function M.get_repo_info(path, cmd, gitdir, toplevel) - -- Does git rev-parse have --absolute-git-dir, added in 2.13: - -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ - local has_abs_gd = check_version({ 2, 13 }) - local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' + -- Does git rev-parse have --absolute-git-dir, added in 2.13: + -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ + local has_abs_gd = check_version({ 2, 13 }) + local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' - -- Wait for internal scheduler to settle before running command - -- https://github.com/lewis6991/gitsigns.nvim/pull/215 - scheduler() + -- Wait for internal scheduler to settle before running command + -- https://github.com/lewis6991/gitsigns.nvim/pull/215 + scheduler() - local args = {} + local args = {} - if gitdir then - vim.list_extend(args, { '--git-dir', gitdir }) - end + if gitdir then + vim.list_extend(args, { '--git-dir', gitdir }) + end - if toplevel then - vim.list_extend(args, { '--work-tree', toplevel }) - end + if toplevel then + vim.list_extend(args, { '--work-tree', toplevel }) + end - vim.list_extend(args, { - 'rev-parse', '--show-toplevel', git_dir_opt, '--abbrev-ref', 'HEAD', - }) + vim.list_extend(args, { + 'rev-parse', + '--show-toplevel', + git_dir_opt, + '--abbrev-ref', + 'HEAD', + }) - local results = git_command(args, { - command = cmd or 'git', - suppress_stderr = true, - cwd = toplevel or path, - }) + local results = git_command(args, { + command = cmd or 'git', + suppress_stderr = true, + cwd = toplevel or path, + }) - local ret = { - toplevel = normalize_path(results[1]), - gitdir = normalize_path(results[2]), - } - ret.abbrev_head = process_abbrev_head(ret.gitdir, results[3], path, cmd) - if ret.gitdir and not has_abs_gd then - ret.gitdir = uv.fs_realpath(ret.gitdir) - end - ret.detached = ret.toplevel and ret.gitdir ~= ret.toplevel .. '/.git' - return ret + local ret = { + toplevel = normalize_path(results[1]), + gitdir = normalize_path(results[2]), + } + ret.abbrev_head = process_abbrev_head(ret.gitdir, results[3], path, cmd) + if ret.gitdir and not has_abs_gd then + ret.gitdir = uv.fs_realpath(ret.gitdir) + end + ret.detached = ret.toplevel and ret.gitdir ~= ret.toplevel .. '/.git' + return ret end -------------------------------------------------------------------------------- @@ -343,126 +268,129 @@ end --- Run git command the with the objects gitdir and toplevel --- @async function Repo:command(args, spec) - spec = spec or {} - spec.cwd = self.toplevel + spec = spec or {} + spec.cwd = self.toplevel - local args1 = { - '--git-dir', self.gitdir, - } + local args1 = { + '--git-dir', + self.gitdir, + } - if self.detached then - vim.list_extend(args1, { '--work-tree', self.toplevel }) - end + if self.detached then + vim.list_extend(args1, { '--work-tree', self.toplevel }) + end - vim.list_extend(args1, args) + vim.list_extend(args1, args) - return git_command(args1, spec) + return git_command(args1, spec) end --- @async function Repo:files_changed() - local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) + local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) - local ret = {} - for _, line in ipairs(results) do - if line:sub(1, 2):match('^.M') then - ret[#ret + 1] = line:sub(4, -1) - end - end - return ret + local ret = {} + for _, line in ipairs(results) do + if line:sub(1, 2):match('^.M') then + ret[#ret + 1] = line:sub(4, -1) + end + end + return ret end local function make_bom(...) - local r = {} - for i, a in ipairs({ ... }) do - r[i] = string.char(a) - end - return table.concat(r) + local r = {} + for i, a in ipairs({ ... }) do + r[i] = string.char(a) + end + return table.concat(r) end local BOM_TABLE = { - ['utf-8'] = make_bom(0xef, 0xbb, 0xbf), - ['utf-16le'] = make_bom(0xff, 0xfe), - ['utf-16'] = make_bom(0xfe, 0xff), - ['utf-16be'] = make_bom(0xfe, 0xff), - ['utf-32le'] = make_bom(0xff, 0xfe, 0x00, 0x00), - ['utf-32'] = make_bom(0xff, 0xfe, 0x00, 0x00), - ['utf-32be'] = make_bom(0x00, 0x00, 0xfe, 0xff), - ['utf-7'] = make_bom(0x2b, 0x2f, 0x76), - ['utf-1'] = make_bom(0xf7, 0x54, 0x4c), + ['utf-8'] = make_bom(0xef, 0xbb, 0xbf), + ['utf-16le'] = make_bom(0xff, 0xfe), + ['utf-16'] = make_bom(0xfe, 0xff), + ['utf-16be'] = make_bom(0xfe, 0xff), + ['utf-32le'] = make_bom(0xff, 0xfe, 0x00, 0x00), + ['utf-32'] = make_bom(0xff, 0xfe, 0x00, 0x00), + ['utf-32be'] = make_bom(0x00, 0x00, 0xfe, 0xff), + ['utf-7'] = make_bom(0x2b, 0x2f, 0x76), + ['utf-1'] = make_bom(0xf7, 0x54, 0x4c), } local function strip_bom(x, encoding) - local bom = BOM_TABLE[encoding] - if bom and vim.startswith(x, bom) then - return x:sub(bom:len() + 1) - end - return x + local bom = BOM_TABLE[encoding] + if bom and vim.startswith(x, bom) then + return x:sub(bom:len() + 1) + end + return x end local function iconv_supported(encoding) - -- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924 - if vim.startswith(encoding, 'utf-16') then - return false - elseif vim.startswith(encoding, 'utf-32') then - return false - end - return true + -- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924 + if vim.startswith(encoding, 'utf-16') then + return false + elseif vim.startswith(encoding, 'utf-32') then + return false + end + return true end --- Get version of file in the index, return array lines --- @async function Repo:get_show_text(object, encoding) - local stdout, stderr = self:command({ 'show', object }, { suppress_stderr = true }) + local stdout, stderr = self:command({ 'show', object }, { suppress_stderr = true }) - if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then - stdout[1] = strip_bom(stdout[1], encoding) - if vim.iconv then - for i, l in ipairs(stdout) do - stdout[i] = vim.iconv(l, encoding, 'utf-8') - end - else - scheduler() - for i, l in ipairs(stdout) do - -- vimscript will interpret strings containing NUL as blob type - if vim.fn.type(l) == vim.v.t_string then - stdout[i] = vim.fn.iconv(l, encoding, 'utf-8') - end - end + if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then + stdout[1] = strip_bom(stdout[1], encoding) + if vim.iconv then + for i, l in ipairs(stdout) do + stdout[i] = vim.iconv(l, encoding, 'utf-8') end - end + else + scheduler() + for i, l in ipairs(stdout) do + -- vimscript will interpret strings containing NUL as blob type + if vim.fn.type(l) == vim.v.t_string then + stdout[i] = vim.fn.iconv(l, encoding, 'utf-8') + end + end + end + end - return stdout, stderr + return stdout, stderr end --- @async function Repo:update_abbrev_head() - self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head + self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head end --- @async function Repo.new(dir, gitdir, toplevel) - local self = setmetatable({}, { __index = Repo }) + local self = setmetatable({}, { __index = Repo }) - self.username = git_command({ 'config', 'user.name' })[1] - local info = M.get_repo_info(dir, nil, gitdir, toplevel) - for k, v in pairs(info) do - (self)[k] = v - end + self.username = git_command({ 'config', 'user.name' })[1] + local info = M.get_repo_info(dir, nil, gitdir, toplevel) + for k, v in pairs(info) do + (self)[k] = v + end - -- Try yadm - 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) - local yadm_info = M.get_repo_info(dir, 'yadm', gitdir, toplevel) - for k, v in pairs(yadm_info) do - (self)[k] = v - end + -- Try yadm + 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) + local yadm_info = M.get_repo_info(dir, 'yadm', gitdir, toplevel) + for k, v in pairs(yadm_info) do + (self)[k] = v end - end + end + end - return self + return self end -------------------------------------------------------------------------------- @@ -472,254 +400,267 @@ end --- Run git command the with the objects gitdir and toplevel --- @async function Obj:command(args, spec) - return self.repo:command(args, spec) + return self.repo:command(args, spec) end --- @async function Obj:update_file_info(update_relpath, silent) - local old_object_name = self.object_name - local props = self:file_info(self.file, silent) + local old_object_name = self.object_name + local props = self:file_info(self.file, silent) - if update_relpath then - self.relpath = props.relpath - end - self.object_name = props.object_name - self.mode_bits = props.mode_bits - self.has_conflicts = props.has_conflicts - self.i_crlf = props.i_crlf - self.w_crlf = props.w_crlf + if update_relpath then + self.relpath = props.relpath + end + self.object_name = props.object_name + self.mode_bits = props.mode_bits + self.has_conflicts = props.has_conflicts + self.i_crlf = props.i_crlf + self.w_crlf = props.w_crlf - return old_object_name ~= self.object_name + return old_object_name ~= self.object_name end --- @async function Obj:file_info(file, silent) - local results, stderr = self:command({ - '-c', 'core.quotepath=off', - 'ls-files', - '--stage', - '--others', - '--exclude-standard', - '--eol', - file or self.file, - }, { suppress_stderr = true }) + local results, stderr = self:command({ + '-c', + 'core.quotepath=off', + 'ls-files', + '--stage', + '--others', + '--exclude-standard', + '--eol', + file or self.file, + }, { suppress_stderr = true }) - if stderr and not silent then - -- 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 - log.eprint(stderr) - end - end + if stderr and not silent then + -- 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 + log.eprint(stderr) + end + end - local result = {} - for _, line in ipairs(results) do - local parts = vim.split(line, '\t') - if #parts > 2 then -- tracked file - local eol = vim.split(parts[2], '%s+') - result.i_crlf = eol[1] == 'i/crlf' - result.w_crlf = eol[2] == 'w/crlf' - result.relpath = parts[3] - local attrs = vim.split(parts[1], '%s+') - local stage = tonumber(attrs[3]) - if stage <= 1 then - result.mode_bits = attrs[1] - result.object_name = attrs[2] - else - result.has_conflicts = true - end - else -- untracked file - result.relpath = parts[2] + local result = {} + for _, line in ipairs(results) do + local parts = vim.split(line, '\t') + if #parts > 2 then -- tracked file + local eol = vim.split(parts[2], '%s+') + result.i_crlf = eol[1] == 'i/crlf' + result.w_crlf = eol[2] == 'w/crlf' + result.relpath = parts[3] + local attrs = vim.split(parts[1], '%s+') + local stage = tonumber(attrs[3]) + if stage <= 1 then + result.mode_bits = attrs[1] + result.object_name = attrs[2] + else + result.has_conflicts = true end - end - return result + else -- untracked file + result.relpath = parts[2] + end + end + return result end --- @async function Obj:get_show_text(revision) - if not self.relpath then - return {} - end + if not self.relpath then + return {} + end - local stdout, stderr = self.repo:get_show_text(revision .. ':' .. self.relpath, self.encoding) + local stdout, stderr = self.repo:get_show_text(revision .. ':' .. self.relpath, self.encoding) - if not self.i_crlf and self.w_crlf then - -- Add cr - for i = 1, #stdout do - stdout[i] = stdout[i] .. '\r' - end - end + if not self.i_crlf and self.w_crlf then + -- Add cr + for i = 1, #stdout do + stdout[i] = stdout[i] .. '\r' + end + end - return stdout, stderr + return stdout, stderr end --- @async Obj.unstage_file = function(self) - self:command({ 'reset', self.file }) + self:command({ 'reset', self.file }) end --- @async function Obj:run_blame(lines, lnum, ignore_whitespace) - local not_committed = { - author = 'Not Committed Yet', - ['author_mail'] = '', - committer = 'Not Committed Yet', - ['committer_mail'] = '', - } + local not_committed = { + author = 'Not Committed Yet', + ['author_mail'] = '', + committer = 'Not Committed Yet', + ['committer_mail'] = '', + } - if not self.object_name or self.repo.abbrev_head == '' then - -- As we support attaching to untracked files we need to return something if - -- the file isn't isn't tracked in git. - -- If abbrev_head is empty, then assume the repo has no commits - return not_committed - end + if not self.object_name or self.repo.abbrev_head == '' then + -- As we support attaching to untracked files we need to return something if + -- the file isn't isn't tracked in git. + -- If abbrev_head is empty, then assume the repo has no commits + return not_committed + end - local args = { - 'blame', - '--contents', '-', - '-L', lnum .. ',+1', - '--line-porcelain', - self.file, - } + local args = { + 'blame', + '--contents', + '-', + '-L', + lnum .. ',+1', + '--line-porcelain', + self.file, + } - if ignore_whitespace then - args[#args + 1] = '-w' - end + if ignore_whitespace then + args[#args + 1] = '-w' + end - local ignore_file = self.repo.toplevel .. '/.git-blame-ignore-revs' - if uv.fs_stat(ignore_file) then - vim.list_extend(args, { '--ignore-revs-file', ignore_file }) - end + local ignore_file = self.repo.toplevel .. '/.git-blame-ignore-revs' + if uv.fs_stat(ignore_file) then + vim.list_extend(args, { '--ignore-revs-file', ignore_file }) + end - local results = self:command(args, { writer = lines }) - if #results == 0 then - return - end - local header = vim.split(table.remove(results, 1), ' ') + local results = self:command(args, { writer = lines }) + if #results == 0 then + return + end + local header = vim.split(table.remove(results, 1), ' ') - local ret = {} - ret.sha = header[1] - ret.orig_lnum = tonumber(header[2]) - ret.final_lnum = tonumber(header[3]) - ret.abbrev_sha = string.sub(ret.sha, 1, 8) - for _, l in ipairs(results) do - if not startswith(l, '\t') then - local cols = vim.split(l, ' ') - local key = table.remove(cols, 1):gsub('-', '_') - ret[key] = table.concat(cols, ' ') - if key == 'previous' then - ret.previous_sha = cols[1] - ret.previous_filename = cols[2] - end + local ret = {} + ret.sha = header[1] + ret.orig_lnum = tonumber(header[2]) + ret.final_lnum = tonumber(header[3]) + ret.abbrev_sha = string.sub(ret.sha, 1, 8) + for _, l in ipairs(results) do + if not startswith(l, '\t') then + local cols = vim.split(l, ' ') + local key = table.remove(cols, 1):gsub('-', '_') + ret[key] = table.concat(cols, ' ') + if key == 'previous' then + ret.previous_sha = cols[1] + ret.previous_filename = cols[2] end - end + end + end - -- New in git 2.41: - -- The output given by "git blame" that attributes a line to contents - -- taken from the file specified by the "--contents" option shows it - -- differently from a line attributed to the working tree file. - if ret.author_mail == '' then - ret = vim.tbl_extend('force', ret, not_committed) - end + -- New in git 2.41: + -- The output given by "git blame" that attributes a line to contents + -- taken from the file specified by the "--contents" option shows it + -- differently from a line attributed to the working tree file. + if ret.author_mail == '' then + ret = vim.tbl_extend('force', ret, not_committed) + end - return ret + return ret end --- @async local function ensure_file_in_index(obj) - if obj.object_name and not obj.has_conflicts then - return - end + if obj.object_name and not obj.has_conflicts then + return + end - if not obj.object_name then - -- If there is no object_name then it is not yet in the index so add it - obj:command({ 'add', '--intent-to-add', obj.file }) - else - -- Update the index with the common ancestor (stage 1) which is what bcache - -- stores - local info = string.format('%s,%s,%s', obj.mode_bits, obj.object_name, obj.relpath) - obj:command({ 'update-index', '--add', '--cacheinfo', info }) - end + if not obj.object_name then + -- If there is no object_name then it is not yet in the index so add it + obj:command({ 'add', '--intent-to-add', obj.file }) + else + -- Update the index with the common ancestor (stage 1) which is what bcache + -- stores + local info = string.format('%s,%s,%s', obj.mode_bits, obj.object_name, obj.relpath) + obj:command({ 'update-index', '--add', '--cacheinfo', info }) + end - obj:update_file_info() + obj:update_file_info() end -- Stage 'lines' as the entire contents of the file --- @async ---- @param lines +--- @param lines string[] function Obj:stage_lines(lines) - local stdout = self:command({ - 'hash-object', '-w', '--path', self.relpath, '--stdin', - }, { writer = lines }) + local stdout = self:command({ + 'hash-object', + '-w', + '--path', + self.relpath, + '--stdin', + }, { writer = lines }) - local new_object = stdout[1] + local new_object = stdout[1] - self:command({ - 'update-index', '--cacheinfo', string.format('%s,%s,%s', self.mode_bits, new_object, self.relpath), - }) + self:command({ + 'update-index', + '--cacheinfo', + string.format('%s,%s,%s', self.mode_bits, new_object, self.relpath), + }) end --- @async Obj.stage_hunks = function(self, hunks, invert) - ensure_file_in_index(self) + ensure_file_in_index(self) - local patch = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert) + local patch = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert) - if not self.i_crlf and self.w_crlf then - -- Remove cr - for i = 1, #patch do - patch[i] = patch[i]:gsub('\r$', '') - end - end + if not self.i_crlf and self.w_crlf then + -- Remove cr + for i = 1, #patch do + patch[i] = patch[i]:gsub('\r$', '') + end + end - self:command({ - 'apply', '--whitespace=nowarn', '--cached', '--unidiff-zero', '-', - }, { - writer = patch, - }) + self:command({ + 'apply', + '--whitespace=nowarn', + '--cached', + '--unidiff-zero', + '-', + }, { + writer = patch, + }) end --- @async function Obj:has_moved() - local out = self:command({ 'diff', '--name-status', '-C', '--cached' }) - local orig_relpath = self.orig_relpath or self.relpath - for _, l in ipairs(out) do - local parts = vim.split(l, '%s+') - if #parts == 3 then - local orig, new = parts[2], parts[3] - if orig_relpath == orig then - self.orig_relpath = orig_relpath - self.relpath = new - self.file = self.repo.toplevel .. '/' .. new - return new - end + local out = self:command({ 'diff', '--name-status', '-C', '--cached' }) + local orig_relpath = self.orig_relpath or self.relpath + for _, l in ipairs(out) do + local parts = vim.split(l, '%s+') + if #parts == 3 then + local orig, new = parts[2], parts[3] + if orig_relpath == orig then + self.orig_relpath = orig_relpath + self.relpath = new + self.file = self.repo.toplevel .. '/' .. new + return new end - end + end + end end --- @async function Obj.new(file, encoding, gitdir, toplevel) - if in_git_dir(file) then - dprint('In git dir') - return nil - end - local self = setmetatable({}, { __index = Obj }) + if in_git_dir(file) then + dprint('In git dir') + return nil + end + local self = setmetatable({}, { __index = Obj }) - self.file = file - self.encoding = encoding - self.repo = Repo.new(util.dirname(file), gitdir, toplevel) + self.file = file + self.encoding = encoding + self.repo = Repo.new(util.dirname(file), gitdir, toplevel) - if not self.repo.gitdir then - dprint('Not in git repo') - return nil - end + if not self.repo.gitdir then + dprint('Not in git repo') + return nil + end - -- When passing gitdir and toplevel, suppress stderr when resolving the file - local silent = gitdir ~= nil and toplevel ~= nil + -- When passing gitdir and toplevel, suppress stderr when resolving the file + local silent = gitdir ~= nil and toplevel ~= nil - self:update_file_info(true, silent) + self:update_file_info(true, silent) - return self + return self end return M diff --git a/lua/gitsigns/highlight.lua b/lua/gitsigns/highlight.lua index 7cc594d..1c7d6d6 100644 --- a/lua/gitsigns/highlight.lua +++ b/lua/gitsigns/highlight.lua @@ -1,223 +1,307 @@ - - - - - - - - local M = {} - - - -- Use array of dict so we can iterate deterministically -- Export for docgen M.hls = { - { GitSignsAdd = { 'GitGutterAdd', 'SignifySignAdd', 'DiffAddedGutter', 'diffAdded', 'DiffAdd', -desc = "Used for the text of 'add' signs.", -}, }, + { + GitSignsAdd = { + 'GitGutterAdd', + 'SignifySignAdd', + 'DiffAddedGutter', + 'diffAdded', + 'DiffAdd', + desc = "Used for the text of 'add' signs.", + }, + }, - { GitSignsChange = { 'GitGutterChange', 'SignifySignChange', 'DiffModifiedGutter', 'diffChanged', 'DiffChange', -desc = "Used for the text of 'change' signs.", -}, }, + { + GitSignsChange = { + 'GitGutterChange', + 'SignifySignChange', + 'DiffModifiedGutter', + 'diffChanged', + 'DiffChange', + desc = "Used for the text of 'change' signs.", + }, + }, - { GitSignsDelete = { 'GitGutterDelete', 'SignifySignDelete', 'DiffRemovedGutter', 'diffRemoved', 'DiffDelete', -desc = "Used for the text of 'delete' signs.", -}, }, + { + GitSignsDelete = { + 'GitGutterDelete', + 'SignifySignDelete', + 'DiffRemovedGutter', + 'diffRemoved', + 'DiffDelete', + desc = "Used for the text of 'delete' signs.", + }, + }, - { GitSignsChangedelete = { 'GitSignsChange', -desc = "Used for the text of 'changedelete' signs.", -}, }, + { + GitSignsChangedelete = { + 'GitSignsChange', + desc = "Used for the text of 'changedelete' signs.", + }, + }, - { GitSignsTopdelete = { 'GitSignsDelete', -desc = "Used for the text of 'topdelete' signs.", -}, }, + { GitSignsTopdelete = { 'GitSignsDelete', desc = "Used for the text of 'topdelete' signs." } }, - { GitSignsUntracked = { 'GitSignsAdd', -desc = "Used for the text of 'untracked' signs.", -}, }, + { GitSignsUntracked = { 'GitSignsAdd', desc = "Used for the text of 'untracked' signs." } }, - { GitSignsAddNr = { 'GitGutterAddLineNr', 'GitSignsAdd', -desc = "Used for number column (when `config.numhl == true`) of 'add' signs.", -}, }, + { + GitSignsAddNr = { + 'GitGutterAddLineNr', + 'GitSignsAdd', + desc = "Used for number column (when `config.numhl == true`) of 'add' signs.", + }, + }, - { GitSignsChangeNr = { 'GitGutterChangeLineNr', 'GitSignsChange', -desc = "Used for number column (when `config.numhl == true`) of 'change' signs.", -}, }, + { + GitSignsChangeNr = { + 'GitGutterChangeLineNr', + 'GitSignsChange', + desc = "Used for number column (when `config.numhl == true`) of 'change' signs.", + }, + }, - { GitSignsDeleteNr = { 'GitGutterDeleteLineNr', 'GitSignsDelete', -desc = "Used for number column (when `config.numhl == true`) of 'delete' signs.", -}, }, + { + GitSignsDeleteNr = { + 'GitGutterDeleteLineNr', + 'GitSignsDelete', + desc = "Used for number column (when `config.numhl == true`) of 'delete' signs.", + }, + }, - { GitSignsChangedeleteNr = { 'GitSignsChangeNr', -desc = "Used for number column (when `config.numhl == true`) of 'changedelete' signs.", -}, }, + { + GitSignsChangedeleteNr = { + 'GitSignsChangeNr', + desc = "Used for number column (when `config.numhl == true`) of 'changedelete' signs.", + }, + }, - { GitSignsTopdeleteNr = { 'GitSignsDeleteNr', -desc = "Used for number column (when `config.numhl == true`) of 'topdelete' signs.", -}, }, + { + GitSignsTopdeleteNr = { + 'GitSignsDeleteNr', + desc = "Used for number column (when `config.numhl == true`) of 'topdelete' signs.", + }, + }, - { GitSignsUntrackedNr = { 'GitSignsAddNr', -desc = "Used for number column (when `config.numhl == true`) of 'untracked' signs.", -}, }, + { + GitSignsUntrackedNr = { + 'GitSignsAddNr', + desc = "Used for number column (when `config.numhl == true`) of 'untracked' signs.", + }, + }, - { GitSignsAddLn = { 'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd', -desc = "Used for buffer line (when `config.linehl == true`) of 'add' signs.", -}, }, + { + GitSignsAddLn = { + 'GitGutterAddLine', + 'SignifyLineAdd', + 'DiffAdd', + desc = "Used for buffer line (when `config.linehl == true`) of 'add' signs.", + }, + }, - { GitSignsChangeLn = { 'GitGutterChangeLine', 'SignifyLineChange', 'DiffChange', -desc = "Used for buffer line (when `config.linehl == true`) of 'change' signs.", -}, }, + { + GitSignsChangeLn = { + 'GitGutterChangeLine', + 'SignifyLineChange', + 'DiffChange', + desc = "Used for buffer line (when `config.linehl == true`) of 'change' signs.", + }, + }, - { GitSignsChangedeleteLn = { 'GitSignsChangeLn', -desc = "Used for buffer line (when `config.linehl == true`) of 'changedelete' signs.", -}, }, + { + GitSignsChangedeleteLn = { + 'GitSignsChangeLn', + desc = "Used for buffer line (when `config.linehl == true`) of 'changedelete' signs.", + }, + }, - { GitSignsUntrackedLn = { 'GitSignsAddLn', -desc = "Used for buffer line (when `config.linehl == true`) of 'untracked' signs.", -}, }, + { + GitSignsUntrackedLn = { + 'GitSignsAddLn', + desc = "Used for buffer line (when `config.linehl == true`) of 'untracked' signs.", + }, + }, - -- Don't set GitSignsDeleteLn by default - -- {GitSignsDeleteLn = {}}, + -- Don't set GitSignsDeleteLn by default + -- {GitSignsDeleteLn = {}}, - { GitSignsStagedAdd = { 'GitSignsAdd', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedChange = { 'GitSignsChange', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedDelete = { 'GitSignsDelete', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedChangedelete = { 'GitSignsChangedelete', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedTopdelete = { 'GitSignsTopdelete', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedAddNr = { 'GitSignsAddNr', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedChangeNr = { 'GitSignsChangeNr', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedDeleteNr = { 'GitSignsDeleteNr', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedChangedeleteNr = { 'GitSignsChangedeleteNr', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedTopdeleteNr = { 'GitSignsTopdeleteNr', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedAddLn = { 'GitSignsAddLn', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedChangeLn = { 'GitSignsChangeLn', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedDeleteLn = { 'GitSignsDeleteLn', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedChangedeleteLn = { 'GitSignsChangedeleteLn', fg_factor = 0.5, hidden = true } }, - { GitSignsStagedTopdeleteLn = { 'GitSignsTopdeleteLn', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedAdd = { 'GitSignsAdd', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedChange = { 'GitSignsChange', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedDelete = { 'GitSignsDelete', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedChangedelete = { 'GitSignsChangedelete', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedTopdelete = { 'GitSignsTopdelete', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedAddNr = { 'GitSignsAddNr', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedChangeNr = { 'GitSignsChangeNr', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedDeleteNr = { 'GitSignsDeleteNr', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedChangedeleteNr = { 'GitSignsChangedeleteNr', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedTopdeleteNr = { 'GitSignsTopdeleteNr', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedAddLn = { 'GitSignsAddLn', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedChangeLn = { 'GitSignsChangeLn', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedDeleteLn = { 'GitSignsDeleteLn', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedChangedeleteLn = { 'GitSignsChangedeleteLn', fg_factor = 0.5, hidden = true } }, + { GitSignsStagedTopdeleteLn = { 'GitSignsTopdeleteLn', fg_factor = 0.5, hidden = true } }, - { GitSignsAddPreview = { 'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd', -desc = "Used for added lines in previews.", -}, }, + { + GitSignsAddPreview = { + 'GitGutterAddLine', + 'SignifyLineAdd', + 'DiffAdd', + desc = 'Used for added lines in previews.', + }, + }, - { GitSignsDeletePreview = { 'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete', -desc = "Used for deleted lines in previews.", -}, }, + { + GitSignsDeletePreview = { + 'GitGutterDeleteLine', + 'SignifyLineDelete', + 'DiffDelete', + desc = 'Used for deleted lines in previews.', + }, + }, - { GitSignsCurrentLineBlame = { 'NonText', -desc = "Used for current line blame.", -}, }, + { GitSignsCurrentLineBlame = { 'NonText', desc = 'Used for current line blame.' } }, - { GitSignsAddInline = { 'TermCursor', -desc = "Used for added word diff regions in inline previews.", -}, }, + { + GitSignsAddInline = { + 'TermCursor', + desc = 'Used for added word diff regions in inline previews.', + }, + }, - { GitSignsDeleteInline = { 'TermCursor', -desc = "Used for deleted word diff regions in inline previews.", -}, }, + { + GitSignsDeleteInline = { + 'TermCursor', + desc = 'Used for deleted word diff regions in inline previews.', + }, + }, - { GitSignsChangeInline = { 'TermCursor', -desc = "Used for changed word diff regions in inline previews.", -}, }, + { + GitSignsChangeInline = { + 'TermCursor', + desc = 'Used for changed word diff regions in inline previews.', + }, + }, - { GitSignsAddLnInline = { 'GitSignsAddInline', -desc = "Used for added word diff regions when `config.word_diff == true`.", -}, }, + { + GitSignsAddLnInline = { + 'GitSignsAddInline', + desc = 'Used for added word diff regions when `config.word_diff == true`.', + }, + }, - { GitSignsChangeLnInline = { 'GitSignsChangeInline', -desc = "Used for changed word diff regions when `config.word_diff == true`.", -}, }, + { + GitSignsChangeLnInline = { + 'GitSignsChangeInline', + desc = 'Used for changed word diff regions when `config.word_diff == true`.', + }, + }, - { GitSignsDeleteLnInline = { 'GitSignsDeleteInline', -desc = "Used for deleted word diff regions when `config.word_diff == true`.", -}, }, + { + GitSignsDeleteLnInline = { + 'GitSignsDeleteInline', + desc = 'Used for deleted word diff regions when `config.word_diff == true`.', + }, + }, - -- Currently unused - -- {GitSignsAddLnVirtLn = {'GitSignsAddLn'}}, - -- {GitSignsChangeVirtLn = {'GitSignsChangeLn'}}, - -- {GitSignsAddLnVirtLnInLine = {'GitSignsAddLnInline', }}, - -- {GitSignsChangeVirtLnInLine = {'GitSignsChangeLnInline', }}, + -- Currently unused + -- {GitSignsAddLnVirtLn = {'GitSignsAddLn'}}, + -- {GitSignsChangeVirtLn = {'GitSignsChangeLn'}}, + -- {GitSignsAddLnVirtLnInLine = {'GitSignsAddLnInline', }}, + -- {GitSignsChangeVirtLnInLine = {'GitSignsChangeLnInline', }}, - { GitSignsDeleteVirtLn = { 'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete', -desc = "Used for deleted lines shown by inline `preview_hunk_inline()` or `show_deleted()`.", -}, }, + { + GitSignsDeleteVirtLn = { + 'GitGutterDeleteLine', + 'SignifyLineDelete', + 'DiffDelete', + desc = 'Used for deleted lines shown by inline `preview_hunk_inline()` or `show_deleted()`.', + }, + }, - { GitSignsDeleteVirtLnInLine = { 'GitSignsDeleteLnInline', -desc = "Used for word diff regions in lines shown by inline `preview_hunk_inline()` or `show_deleted()`.", -}, }, - - { GitSignsVirtLnum = { 'GitSignsDeleteVirtLn', -desc = 'Used for line numbers in inline hunks previews.', -}, }, + { + GitSignsDeleteVirtLnInLine = { + 'GitSignsDeleteLnInline', + desc = 'Used for word diff regions in lines shown by inline `preview_hunk_inline()` or `show_deleted()`.', + }, + }, + { + GitSignsVirtLnum = { + 'GitSignsDeleteVirtLn', + desc = 'Used for line numbers in inline hunks previews.', + }, + }, } local function is_hl_set(hl_name) - -- TODO: this only works with `set termguicolors` - local exists, hl = pcall(vim.api.nvim_get_hl_by_name, hl_name, true) - local color = hl.foreground or hl.background or hl.reverse - return exists and color ~= nil + -- TODO: this only works with `set termguicolors` + local exists, hl = pcall(vim.api.nvim_get_hl_by_name, hl_name, true) + local color = hl.foreground or hl.background or hl.reverse + return exists and color ~= nil end local function cmul(x, factor) - if not x or factor == 1 then - return x - end + if not x or factor == 1 then + return x + end - local r = math.floor(x / 2 ^ 16) - local x1 = x - (r * 2 ^ 16) - local g = math.floor(x1 / 2 ^ 8) - local b = math.floor(x1 - (g * 2 ^ 8)) - return math.floor(math.floor(r * factor) * 2 ^ 16 + math.floor(g * factor) * 2 ^ 8 + math.floor(b * factor)) + local r = math.floor(x / 2 ^ 16) + local x1 = x - (r * 2 ^ 16) + local g = math.floor(x1 / 2 ^ 8) + local b = math.floor(x1 - (g * 2 ^ 8)) + return math.floor( + math.floor(r * factor) * 2 ^ 16 + math.floor(g * factor) * 2 ^ 8 + math.floor(b * factor) + ) end local function dprintf(fmt, ...) - require('gitsigns.debug.log').dprintf(fmt, ...) + require('gitsigns.debug.log').dprintf(fmt, ...) end local function derive(hl, hldef) - for _, d in ipairs(hldef) do - if is_hl_set(d) then - dprintf('Deriving %s from %s', hl, d) - if hldef.fg_factor or hldef.bg_factor then - hldef.fg_factor = hldef.fg_factor or 1 - hldef.bg_factor = hldef.bg_factor or 1 - local dh = vim.api.nvim_get_hl_by_name(d, true) - vim.api.nvim_set_hl(0, hl, { - default = true, - fg = cmul(dh.foreground, hldef.fg_factor), - bg = cmul(dh.background, hldef.bg_factor), - }) - else - vim.api.nvim_set_hl(0, hl, { default = true, link = d }) - end - return + for _, d in ipairs(hldef) do + if is_hl_set(d) then + dprintf('Deriving %s from %s', hl, d) + if hldef.fg_factor or hldef.bg_factor then + hldef.fg_factor = hldef.fg_factor or 1 + hldef.bg_factor = hldef.bg_factor or 1 + local dh = vim.api.nvim_get_hl_by_name(d, true) + vim.api.nvim_set_hl(0, hl, { + default = true, + fg = cmul(dh.foreground, hldef.fg_factor), + bg = cmul(dh.background, hldef.bg_factor), + }) + else + vim.api.nvim_set_hl(0, hl, { default = true, link = d }) end - end - if hldef[1] and not hldef.bg_factor and not hldef.fg_factor then - -- No fallback found which is set. Just link to the first fallback - -- if there are no modifiers - dprintf('Deriving %s from %s', hl, hldef[1]) - vim.api.nvim_set_hl(0, hl, { default = true, link = hldef[1] }) - else - dprintf('Could not derive %s', hl) - end + return + end + end + if hldef[1] and not hldef.bg_factor and not hldef.fg_factor then + -- No fallback found which is set. Just link to the first fallback + -- if there are no modifiers + dprintf('Deriving %s from %s', hl, hldef[1]) + vim.api.nvim_set_hl(0, hl, { default = true, link = hldef[1] }) + else + dprintf('Could not derive %s', hl) + end end -- Setup a GitSign* highlight by deriving it from other potentially present -- highlights. M.setup_highlights = function() - for _, hlg in ipairs(M.hls) do - for hl, hldef in pairs(hlg) do - if is_hl_set(hl) then - -- Already defined - dprintf('Highlight %s is already defined', hl) - else - derive(hl, hldef) - end + for _, hlg in ipairs(M.hls) do + for hl, hldef in pairs(hlg) do + if is_hl_set(hl) then + -- Already defined + dprintf('Highlight %s is already defined', hl) + else + derive(hl, hldef) end - end + end + end end return M diff --git a/lua/gitsigns/hunks.lua b/lua/gitsigns/hunks.lua index de9d163..c45b896 100644 --- a/lua/gitsigns/hunks.lua +++ b/lua/gitsigns/hunks.lua @@ -29,36 +29,9 @@ local min, max = math.min, math.max --- @field added Gitsigns.Hunk.Node --- @field removed Gitsigns.Hunk.Node -local M = {Node = {}, Hunk = {}, Hunk_Public = {}, } - - - - - - - - - - - - - -- For internal use - - - - - - - - - - - - - - - +local M = { Node = {}, Hunk = {}, Hunk_Public = {} } +-- For internal use local Hunk = M.Hunk @@ -68,18 +41,19 @@ local Hunk = M.Hunk --- @param new_count integer --- @return Gitsigns.Hunk.Hunk function M.create_hunk(old_start, old_count, new_start, new_count) - return { - removed = { start = old_start, count = old_count, lines = {} }, - added = { start = new_start, count = new_count, lines = {} }, - head = ('@@ -%d%s +%d%s @@'):format( - old_start, old_count > 0 and ',' .. old_count or '', - new_start, new_count > 0 and ',' .. new_count or ''), + return { + removed = { start = old_start, count = old_count, lines = {} }, + added = { start = new_start, count = new_count, lines = {} }, + head = ('@@ -%d%s +%d%s @@'):format( + old_start, + old_count > 0 and ',' .. old_count or '', + new_start, + new_count > 0 and ',' .. new_count or '' + ), - vend = new_start + math.max(new_count - 1, 0), - type = new_count == 0 and 'delete' or - old_count == 0 and 'add' or - 'change', - } + vend = new_start + math.max(new_count - 1, 0), + type = new_count == 0 and 'delete' or old_count == 0 and 'add' or 'change', + } end --- @param hunks Gitsigns.Hunk.Hunk[] @@ -87,97 +61,100 @@ end --- @param bot integer --- @return Gitsigns.Hunk.Hunk function M.create_partial_hunk(hunks, top, bot) - local pretop, precount = top, bot - top + 1 - for _, h in ipairs(hunks) do - local added_in_hunk = h.added.count - h.removed.count + local pretop, precount = top, bot - top + 1 + for _, h in ipairs(hunks) do + local added_in_hunk = h.added.count - h.removed.count - local added_in_range = 0 - if h.added.start >= top and h.vend <= bot then - -- Range contains hunk - added_in_range = added_in_hunk - else - local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count)) - local added_above_top = max(0, top - (h.added.start + h.removed.count)) + local added_in_range = 0 + if h.added.start >= top and h.vend <= bot then + -- Range contains hunk + added_in_range = added_in_hunk + else + local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count)) + local added_above_top = max(0, top - (h.added.start + h.removed.count)) - if h.added.start >= top and h.added.start <= bot then - -- Range top intersects hunk - added_in_range = added_above_bot - elseif h.vend >= top and h.vend <= bot then - -- Range bottom intersects hunk - added_in_range = added_in_hunk - added_above_top - pretop = pretop - added_above_top - elseif h.added.start <= top and h.vend >= bot then - -- Range within hunk - added_in_range = added_above_bot - added_above_top - pretop = pretop - added_above_top - end - - if top > h.vend then - pretop = pretop - added_in_hunk - end + if h.added.start >= top and h.added.start <= bot then + -- Range top intersects hunk + added_in_range = added_above_bot + elseif h.vend >= top and h.vend <= bot then + -- Range bottom intersects hunk + added_in_range = added_in_hunk - added_above_top + pretop = pretop - added_above_top + elseif h.added.start <= top and h.vend >= bot then + -- Range within hunk + added_in_range = added_above_bot - added_above_top + pretop = pretop - added_above_top end - precount = precount - added_in_range - end + if top > h.vend then + pretop = pretop - added_in_hunk + end + end - if precount == 0 then - pretop = pretop - 1 - end + precount = precount - added_in_range + end - return M.create_hunk(pretop, precount, top, bot - top + 1) + if precount == 0 then + pretop = pretop - 1 + end + + return M.create_hunk(pretop, precount, top, bot - top + 1) end --- @param hunk Gitsigns.Hunk.Hunk --- @param fileformat string --- @return string[] function M.patch_lines(hunk, fileformat) - local lines = {} --- @type string[] - for _, l in ipairs(hunk.removed.lines) do - lines[#lines + 1] = '-' .. l - end - for _, l in ipairs(hunk.added.lines) do - lines[#lines + 1] = '+' .. l - end + local lines = {} --- @type string[] + for _, l in ipairs(hunk.removed.lines) do + lines[#lines + 1] = '-' .. l + end + for _, l in ipairs(hunk.added.lines) do + lines[#lines + 1] = '+' .. l + end - if fileformat == 'dos' then - lines = util.strip_cr(lines) - end - return lines + if fileformat == 'dos' then + lines = util.strip_cr(lines) + end + return lines end --- @param line string --- @return Gitsigns.Hunk.Hunk function M.parse_diff_line(line) - local diffkey = vim.trim(vim.split(line, '@@', true)[2]) + local diffkey = vim.trim(vim.split(line, '@@', true)[2]) - -- diffKey: "-xx,n +yy" - -- pre: {xx, n}, now: {yy} - local pre, now = unpack(vim.tbl_map(function(s) - return vim.split(string.sub(s, 2), ',') - end, vim.split(diffkey, ' '))) + -- diffKey: "-xx,n +yy" + -- pre: {xx, n}, now: {yy} + local pre, now = unpack(vim.tbl_map(function(s) + return vim.split(string.sub(s, 2), ',') + end, vim.split(diffkey, ' '))) - local hunk = M.create_hunk( - tonumber(pre[1]), (tonumber(pre[2]) or 1), - tonumber(now[1]), (tonumber(now[2]) or 1)) + local hunk = M.create_hunk( + tonumber(pre[1]), + (tonumber(pre[2]) or 1), + tonumber(now[1]), + (tonumber(now[2]) or 1) + ) - hunk.head = line + hunk.head = line - return hunk + return hunk end --- @param hunk Gitsigns.Hunk.Hunk --- @return integer local function change_end(hunk) - if hunk.added.count == 0 then - -- delete - return hunk.added.start - elseif hunk.removed.count == 0 then - -- add - return hunk.added.start + hunk.added.count - 1 - else - -- change - return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1 - end + if hunk.added.count == 0 then + -- delete + return hunk.added.start + elseif hunk.removed.count == 0 then + -- add + return hunk.added.start + hunk.added.count - 1 + else + -- change + return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1 + end end --- Calculate signs needed to be applied from a hunk for a specified line range. @@ -187,47 +164,45 @@ end --- @param untracked boolean --- @return Gitsigns.Sign[] function M.calc_signs(hunk, min_lnum, max_lnum, untracked) - assert(not untracked or hunk.type == 'add') - min_lnum = min_lnum or 1 - max_lnum = max_lnum or math.huge - local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count + assert(not untracked or hunk.type == 'add') + min_lnum = min_lnum or 1 + max_lnum = max_lnum or math.huge + local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count - if hunk.type == 'delete' and start == 0 then - if min_lnum <= 1 then - -- topdelete signs get placed one row lower - return { { type = 'topdelete', count = removed, lnum = 1 } } - else - return {} - end - end + if hunk.type == 'delete' and start == 0 then + if min_lnum <= 1 then + -- topdelete signs get placed one row lower + return { { type = 'topdelete', count = removed, lnum = 1 } } + else + return {} + end + end - local signs = {} + local signs = {} - local cend = change_end(hunk) + local cend = change_end(hunk) - for lnum = max(start, min_lnum), min(cend, max_lnum) do - local changedelete = hunk.type == 'change' and removed > added and lnum == cend + for lnum = max(start, min_lnum), min(cend, max_lnum) do + local changedelete = hunk.type == 'change' and removed > added and lnum == cend + signs[#signs + 1] = { + type = changedelete and 'changedelete' or untracked and 'untracked' or hunk.type, + count = lnum == start and (hunk.type == 'add' and added or removed), + lnum = lnum, + } + end + + if hunk.type == 'change' and added > removed and hunk.vend >= min_lnum and cend <= max_lnum then + for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do signs[#signs + 1] = { - type = changedelete and 'changedelete' or - untracked and 'untracked' or hunk.type, - count = lnum == start and (hunk.type == 'add' and added or removed), - lnum = lnum, + type = 'add', + count = lnum == hunk.vend and (added - removed), + lnum = lnum, } - end + end + end - if hunk.type == "change" and added > removed and - hunk.vend >= min_lnum and cend <= max_lnum then - for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do - signs[#signs + 1] = { - type = 'add', - count = lnum == hunk.vend and (added - removed), - lnum = lnum, - } - end - end - - return signs + return signs end --- @param relpath string @@ -236,83 +211,86 @@ end --- @param invert boolean --- @return string[] function M.create_patch(relpath, hunks, mode_bits, invert) - invert = invert or false + invert = invert or false - local results = { - string.format('diff --git a/%s b/%s', relpath, relpath), - 'index 000000..000000 ' .. mode_bits, - '--- a/' .. relpath, - '+++ b/' .. relpath, - } + local results = { + string.format('diff --git a/%s b/%s', relpath, relpath), + 'index 000000..000000 ' .. mode_bits, + '--- a/' .. relpath, + '+++ b/' .. relpath, + } - local offset = 0 + local offset = 0 - for _, process_hunk in ipairs(hunks) do - local start, pre_count, now_count = + for _, process_hunk in ipairs(hunks) do + local start, pre_count, now_count = process_hunk.removed.start, process_hunk.removed.count, process_hunk.added.count - if process_hunk.type == 'add' then - start = start + 1 - end + if process_hunk.type == 'add' then + start = start + 1 + end - local pre_lines = process_hunk.removed.lines - local now_lines = process_hunk.added.lines + local pre_lines = process_hunk.removed.lines + local now_lines = process_hunk.added.lines - if invert then - pre_count, now_count = now_count, pre_count - pre_lines, now_lines = now_lines, pre_lines - end + if invert then + pre_count, now_count = now_count, pre_count + pre_lines, now_lines = now_lines, pre_lines + end - table.insert(results, string.format('@@ -%s,%s +%s,%s @@', start, pre_count, start + offset, now_count)) - for _, l in ipairs(pre_lines) do - results[#results + 1] = '-' .. l - end - for _, l in ipairs(now_lines) do - results[#results + 1] = '+' .. l - end + table.insert( + results, + string.format('@@ -%s,%s +%s,%s @@', start, pre_count, start + offset, now_count) + ) + for _, l in ipairs(pre_lines) do + results[#results + 1] = '-' .. l + end + for _, l in ipairs(now_lines) do + results[#results + 1] = '+' .. l + end - process_hunk.removed.start = start + offset - offset = offset + (now_count - pre_count) - end + process_hunk.removed.start = start + offset + offset = offset + (now_count - pre_count) + end - return results + return results end --- @param hunks Gitsigns.Hunk.Hunk[] --- @return Gitsigns.StatusObj function M.get_summary(hunks) - local status = { added = 0, changed = 0, removed = 0 } + local status = { added = 0, changed = 0, removed = 0 } - for _, hunk in ipairs(hunks or {}) do - if hunk.type == 'add' then - status.added = status.added + hunk.added.count - elseif hunk.type == 'delete' then - status.removed = status.removed + hunk.removed.count - elseif hunk.type == 'change' then - local add, remove = hunk.added.count, hunk.removed.count - local delta = min(add, remove) - status.changed = status.changed + delta - status.added = status.added + add - delta - status.removed = status.removed + remove - delta - end - end + for _, hunk in ipairs(hunks or {}) do + if hunk.type == 'add' then + status.added = status.added + hunk.added.count + elseif hunk.type == 'delete' then + status.removed = status.removed + hunk.removed.count + elseif hunk.type == 'change' then + local add, remove = hunk.added.count, hunk.removed.count + local delta = min(add, remove) + status.changed = status.changed + delta + status.added = status.added + add - delta + status.removed = status.removed + remove - delta + end + end - return status + return status end --- @param lnum integer --- @param hunks Gitsigns.Hunk.Hunk[] --- @return Gitsigns.Hunk.Hunk?, integer? function M.find_hunk(lnum, hunks) - for i, hunk in ipairs(hunks or {}) do - if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then - return hunk, i - end + for i, hunk in ipairs(hunks or {}) do + if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then + return hunk, i + end - if hunk.added.start <= lnum and hunk.vend >= lnum then - return hunk, i - end - end + if hunk.added.start <= lnum and hunk.vend >= lnum then + return hunk, i + end + end end --- @param lnum integer @@ -321,73 +299,73 @@ end --- @param wrap boolean --- @return Gitsigns.Hunk.Hunk, integer function M.find_nearest_hunk(lnum, hunks, forwards, wrap) - local ret --- @type Gitsigns.Hunk.Hunk - local index --- @type integer - local distance = math.huge - if forwards then - for i = 1, #hunks do - local hunk = hunks[i] - local dist = hunk.added.start - lnum - if dist > 0 and dist < distance then - distance = dist - ret = hunk - index = i - end + local ret --- @type Gitsigns.Hunk.Hunk + local index --- @type integer + local distance = math.huge + if forwards then + for i = 1, #hunks do + local hunk = hunks[i] + local dist = hunk.added.start - lnum + if dist > 0 and dist < distance then + distance = dist + ret = hunk + index = i end - else - for i = #hunks, 1, -1 do - local hunk = hunks[i] - local dist = lnum - hunk.vend - if dist > 0 and dist < distance then - distance = dist - ret = hunk - index = i - end + end + else + for i = #hunks, 1, -1 do + local hunk = hunks[i] + local dist = lnum - hunk.vend + if dist > 0 and dist < distance then + distance = dist + ret = hunk + index = i end - end - if not ret and wrap then - index = forwards and 1 or #hunks - ret = hunks[index] - end - return ret, index + end + end + if not ret and wrap then + index = forwards and 1 or #hunks + ret = hunks[index] + end + return ret, index end --- @param a Gitsigns.Hunk.Hunk[] --- @param b Gitsigns.Hunk.Hunk[] --- @return boolean function M.compare_heads(a, b) - if (a == nil) ~= (b == nil) then + if (a == nil) ~= (b == nil) then + return true + elseif a and #a ~= #b then + return true + end + for i, ah in ipairs(a or {}) do + if b[i].head ~= ah.head then return true - elseif a and #a ~= #b then - return true - end - for i, ah in ipairs(a or {}) do - if b[i].head ~= ah.head then - return true - end - end - return false + end + end + return false end --- @param a Gitsigns.Hunk.Hunk --- @param b Gitsigns.Hunk.Hunk --- @return boolean local function compare_new(a, b) - if a.added.start ~= b.added.start then + if a.added.start ~= b.added.start then + return false + end + + if a.added.count ~= b.added.count then + return false + end + + for i = 1, a.added.count do + if a.added.lines[i] ~= b.added.lines[i] then return false - end + end + end - if a.added.count ~= b.added.count then - return false - end - - for i = 1, a.added.count do - if a.added.lines[i] ~= b.added.lines[i] then - return false - end - end - - return true + return true end --- Return hunks in a using b's hunks as a filter. Only compare the 'new' section @@ -417,56 +395,56 @@ end --- @param b Gitsigns.Hunk.Hunk[] --- @return Gitsigns.Hunk.Hunk[]? function M.filter_common(a, b) - if not a and not b then - return - end + if not a and not b then + return + end - a, b = a or {}, b or {} - local max_iter = math.max(#a, #b) + a, b = a or {}, b or {} + local max_iter = math.max(#a, #b) - local a_i = 1 - local b_i = 1 + local a_i = 1 + local b_i = 1 - --- @type Gitsigns.Hunk.Hunk[] - local ret = {} + --- @type Gitsigns.Hunk.Hunk[] + local ret = {} - for _ = 1, max_iter do - local a_h, b_h = a[a_i], b[b_i] + for _ = 1, max_iter do + local a_h, b_h = a[a_i], b[b_i] - if not a_h then - -- Reached the end of a - break + if not a_h then + -- Reached the end of a + break + end + + if not b_h then + -- Reached the end of b, add remainder of a + for i = a_i, #a do + ret[#ret + 1] = a[i] end + break + end - if not b_h then - -- Reached the end of b, add remainder of a - for i = a_i, #a do - ret[#ret + 1] = a[i] - end - break + if a_h.added.start > b_h.added.start then + -- a pointer is ahead of b; increment b pointer + b_i = b_i + 1 + elseif a_h.added.start < b_h.added.start then + -- b pointer is ahead of a; add a_h to ret and increment a pointer + ret[#ret + 1] = a_h + a_i = a_i + 1 + else -- a_h.start == b_h.start + -- a_h and b_h start on the same line, if hunks have the same changes then + -- skip (filtered) otherwise add a_h to ret. Increment both hunk + -- pointers + -- TODO(lewis6991): Be smarter; if bh intercepts then break down ah. + if not compare_new(a_h, b_h) then + ret[#ret + 1] = a_h end + a_i = a_i + 1 + b_i = b_i + 1 + end + end - if a_h.added.start > b_h.added.start then - -- a pointer is ahead of b; increment b pointer - b_i = b_i + 1 - elseif a_h.added.start < b_h.added.start then - -- b pointer is ahead of a; add a_h to ret and increment a pointer - ret[#ret + 1] = a_h - a_i = a_i + 1 - else -- a_h.start == b_h.start - -- a_h and b_h start on the same line, if hunks have the same changes then - -- skip (filtered) otherwise add a_h to ret. Increment both hunk - -- pointers - -- TODO(lewis6991): Be smarter; if bh intercepts then break down ah. - if not compare_new(a_h, b_h) then - ret[#ret + 1] = a_h - end - a_i = a_i + 1 - b_i = b_i + 1 - end - end - - return ret + return ret end return M diff --git a/lua/gitsigns/manager.lua b/lua/gitsigns/manager.lua index 47fc953..05c38f0 100644 --- a/lua/gitsigns/manager.lua +++ b/lua/gitsigns/manager.lua @@ -7,7 +7,7 @@ local CacheEntry = gs_cache.CacheEntry local cache = gs_cache.cache local Signs = require('gitsigns.signs') -local Status = require("gitsigns.status") +local Status = require('gitsigns.status') local debounce_trailing = require('gitsigns.debounce').debounce_trailing local throttle_by_id = require('gitsigns.debounce').throttle_by_id @@ -22,7 +22,7 @@ local util = require('gitsigns.util') local run_diff = require('gitsigns.diff') local uv = require('gitsigns.uv') -local gs_hunks = require("gitsigns.hunks") +local gs_hunks = require('gitsigns.hunks') local Hunk = gs_hunks.Hunk local config = require('gitsigns.config').config @@ -34,155 +34,150 @@ local signs_staged local M = {} - - - - - - - - local scheduler_if_buf_valid = awrap(function(buf, cb) - vim.schedule(function() - if vim.api.nvim_buf_is_valid(buf) then - cb() - end - end) + vim.schedule(function() + if vim.api.nvim_buf_is_valid(buf) then + cb() + end + end) end, 2) local function apply_win_signs0(bufnr, signs, hunks, top, bot, clear, untracked) - if clear then - signs:remove(bufnr) -- Remove all signs - end + if clear then + signs:remove(bufnr) -- Remove all signs + end - for i, hunk in ipairs(hunks or {}) do - -- To stop the sign column width changing too much, if there are signs to be - -- added but none of them are visible in the window, then make sure to add at - -- least one sign. Only do this on the first call after an update when we all - -- the signs have been cleared. - if clear and i == 1 then - signs:add(bufnr, gs_hunks.calc_signs(hunk, hunk.added.start, hunk.added.start, untracked)) - end + for i, hunk in ipairs(hunks or {}) do + -- To stop the sign column width changing too much, if there are signs to be + -- added but none of them are visible in the window, then make sure to add at + -- least one sign. Only do this on the first call after an update when we all + -- the signs have been cleared. + if clear and i == 1 then + signs:add(bufnr, gs_hunks.calc_signs(hunk, hunk.added.start, hunk.added.start, untracked)) + end - if top <= hunk.vend and bot >= hunk.added.start then - signs:add(bufnr, gs_hunks.calc_signs(hunk, top, bot, untracked)) - end - if hunk.added.start > bot then - break - end - end + if top <= hunk.vend and bot >= hunk.added.start then + signs:add(bufnr, gs_hunks.calc_signs(hunk, top, bot, untracked)) + end + if hunk.added.start > bot then + break + end + end end local function apply_win_signs(bufnr, top, bot, clear, untracked) - local bcache = cache[bufnr] - if not bcache then - return - end - apply_win_signs0(bufnr, signs_normal, bcache.hunks, top, bot, clear, untracked) - if signs_staged then - apply_win_signs0(bufnr, signs_staged, bcache.hunks_staged, top, bot, clear, false) - end + local bcache = cache[bufnr] + if not bcache then + return + end + apply_win_signs0(bufnr, signs_normal, bcache.hunks, top, bot, clear, untracked) + if signs_staged then + apply_win_signs0(bufnr, signs_staged, bcache.hunks_staged, top, bot, clear, false) + end end M.on_lines = function(buf, first, last_orig, last_new) - local bcache = cache[buf] - if not bcache then - dprint('Cache for buffer was nil. Detaching') - return true - end + local bcache = cache[buf] + if not bcache then + dprint('Cache for buffer was nil. Detaching') + return true + end - signs_normal:on_lines(buf, first, last_orig, last_new) - if signs_staged then - signs_staged:on_lines(buf, first, last_orig, last_new) - end + signs_normal:on_lines(buf, first, last_orig, last_new) + if signs_staged then + signs_staged:on_lines(buf, first, last_orig, last_new) + end - -- Signs in changed regions get invalidated so we need to force a redraw if - -- any signs get removed. - if bcache.hunks and signs_normal:contains(buf, first, last_new) then + -- Signs in changed regions get invalidated so we need to force a redraw if + -- any signs get removed. + if bcache.hunks and signs_normal:contains(buf, first, last_new) then + -- Force a sign redraw on the next update (fixes #521) + bcache.force_next_update = true + end + + if signs_staged then + if bcache.hunks_staged and signs_staged:contains(buf, first, last_new) then -- Force a sign redraw on the next update (fixes #521) bcache.force_next_update = true - end + end + end - if signs_staged then - if bcache.hunks_staged and signs_staged:contains(buf, first, last_new) then - -- Force a sign redraw on the next update (fixes #521) - bcache.force_next_update = true - end - end - - M.update_debounced(buf, cache[buf]) + M.update_debounced(buf, cache[buf]) end local ns = api.nvim_create_namespace('gitsigns') local function apply_word_diff(bufnr, row) - -- Don't run on folded lines - if vim.fn.foldclosed(row + 1) ~= -1 then - return - end + -- Don't run on folded lines + if vim.fn.foldclosed(row + 1) ~= -1 then + return + end - local bcache = cache[bufnr] + local bcache = cache[bufnr] - if not bcache or not bcache.hunks then - return - end + if not bcache or not bcache.hunks then + return + end - local line = api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] - if not line then - -- Invalid line - return - end + local line = api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] + if not line then + -- Invalid line + return + end - local lnum = row + 1 + local lnum = row + 1 - local hunk = gs_hunks.find_hunk(lnum, bcache.hunks) - if not hunk then - -- No hunk at line - return - end + local hunk = gs_hunks.find_hunk(lnum, bcache.hunks) + if not hunk then + -- No hunk at line + return + end - if hunk.added.count ~= hunk.removed.count then - -- Only word diff if added count == removed - return - end + if hunk.added.count ~= hunk.removed.count then + -- Only word diff if added count == removed + return + end - local pos = lnum - hunk.added.start + 1 + local pos = lnum - hunk.added.start + 1 - local added_line = hunk.added.lines[pos] - local removed_line = hunk.removed.lines[pos] + local added_line = hunk.added.lines[pos] + local removed_line = hunk.removed.lines[pos] - local _, added_regions = require('gitsigns.diff_int').run_word_diff({ removed_line }, { added_line }) + local _, added_regions = require('gitsigns.diff_int').run_word_diff( + { removed_line }, + { added_line } + ) - local cols = #line + local cols = #line - for _, region in ipairs(added_regions) do - local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1 - if ecol == scol then - -- Make sure region is at least 1 column wide so deletes can be shown - ecol = scol + 1 - end + for _, region in ipairs(added_regions) do + local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1 + if ecol == scol then + -- Make sure region is at least 1 column wide so deletes can be shown + ecol = scol + 1 + end - local hl_group = rtype == 'add' and 'GitSignsAddLnInline' or - rtype == 'change' and 'GitSignsChangeLnInline' or - 'GitSignsDeleteLnInline' + local hl_group = rtype == 'add' and 'GitSignsAddLnInline' + or rtype == 'change' and 'GitSignsChangeLnInline' + or 'GitSignsDeleteLnInline' - local opts = { - ephemeral = true, - priority = 1000, - } + local opts = { + ephemeral = true, + priority = 1000, + } - if ecol > cols and ecol == scol + 1 then - -- delete on last column, use virtual text instead - opts.virt_text = { { ' ', hl_group } } - opts.virt_text_pos = 'overlay' - else - opts.end_col = ecol - opts.hl_group = hl_group - end + if ecol > cols and ecol == scol + 1 then + -- delete on last column, use virtual text instead + opts.virt_text = { { ' ', hl_group } } + opts.virt_text_pos = 'overlay' + else + opts.end_col = ecol + opts.hl_group = hl_group + end - api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts) - api.nvim__buf_redraw_range(bufnr, row, row + 1) - end + api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts) + api.nvim__buf_redraw_range(bufnr, row, row + 1) + end end local ns_rm = api.nvim_create_namespace('gitsigns_removed') @@ -190,182 +185,185 @@ local ns_rm = api.nvim_create_namespace('gitsigns_removed') local VIRT_LINE_LEN = 300 local function clear_deleted(bufnr) - local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {}) - for _, mark in ipairs(marks) do - api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1]) - end + local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {}) + for _, mark in ipairs(marks) do + api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1]) + end end function M.show_deleted(bufnr, nsd, hunk) - local virt_lines = {} + local virt_lines = {} - for i, line in ipairs(hunk.removed.lines) do - local vline = {} - local last_ecol = 1 + for i, line in ipairs(hunk.removed.lines) do + local vline = {} + local last_ecol = 1 - if config.word_diff then - local regions = require('gitsigns.diff_int').run_word_diff( - { hunk.removed.lines[i] }, { hunk.added.lines[i] }) + if config.word_diff then + local regions = require('gitsigns.diff_int').run_word_diff( + { hunk.removed.lines[i] }, + { hunk.added.lines[i] } + ) - for _, region in ipairs(regions) do - local rline, scol, ecol = region[1], region[3], region[4] - if rline > 1 then - break - end - vline[#vline + 1] = { line:sub(last_ecol, scol - 1), 'GitSignsDeleteVirtLn' } - vline[#vline + 1] = { line:sub(scol, ecol - 1), 'GitSignsDeleteVirtLnInline' } - last_ecol = ecol - end + for _, region in ipairs(regions) do + local rline, scol, ecol = region[1], region[3], region[4] + if rline > 1 then + break + end + vline[#vline + 1] = { line:sub(last_ecol, scol - 1), 'GitSignsDeleteVirtLn' } + vline[#vline + 1] = { line:sub(scol, ecol - 1), 'GitSignsDeleteVirtLnInline' } + last_ecol = ecol end + end - if #line > 0 then - vline[#vline + 1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn' } - end + if #line > 0 then + vline[#vline + 1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn' } + end - -- Add extra padding so the entire line is highlighted - local padding = string.rep(' ', VIRT_LINE_LEN - #line) - vline[#vline + 1] = { padding, 'GitSignsDeleteVirtLn' } + -- Add extra padding so the entire line is highlighted + local padding = string.rep(' ', VIRT_LINE_LEN - #line) + vline[#vline + 1] = { padding, 'GitSignsDeleteVirtLn' } - virt_lines[i] = vline - end + virt_lines[i] = vline + end - local topdelete = hunk.added.start == 0 and hunk.type == 'delete' + local topdelete = hunk.added.start == 0 and hunk.type == 'delete' - local row = topdelete and 0 or hunk.added.start - 1 - api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { - virt_lines = virt_lines, - -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 - virt_lines_above = hunk.type ~= 'delete' or topdelete, - }) + local row = topdelete and 0 or hunk.added.start - 1 + api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { + virt_lines = virt_lines, + -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 + virt_lines_above = hunk.type ~= 'delete' or topdelete, + }) end function M.show_deleted_in_float(bufnr, nsd, hunk) - local virt_lines = {} - for i = 1, hunk.removed.count do - virt_lines[i] = { { '', 'Normal' } } - end + local virt_lines = {} + for i = 1, hunk.removed.count do + virt_lines[i] = { { '', 'Normal' } } + end - local topdelete = hunk.added.start == 0 and hunk.type == 'delete' - local virt_lines_above = hunk.type ~= 'delete' or topdelete + local topdelete = hunk.added.start == 0 and hunk.type == 'delete' + local virt_lines_above = hunk.type ~= 'delete' or topdelete - local row = topdelete and 0 or hunk.added.start - 1 - api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { - virt_lines = virt_lines, - -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 - virt_lines_above = virt_lines_above, - }) + local row = topdelete and 0 or hunk.added.start - 1 + api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { + virt_lines = virt_lines, + -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 + virt_lines_above = virt_lines_above, + }) - local bcache = cache[bufnr] - local pbufnr = api.nvim_create_buf(false, true) - api.nvim_buf_set_lines(pbufnr, 0, -1, false, bcache.compare_text) + local bcache = cache[bufnr] + local pbufnr = api.nvim_create_buf(false, true) + api.nvim_buf_set_lines(pbufnr, 0, -1, false, bcache.compare_text) - local cwin = api.nvim_get_current_win() - local width = api.nvim_win_get_width(0) + local cwin = api.nvim_get_current_win() + local width = api.nvim_win_get_width(0) - local opts = { - relative = 'win', - win = cwin, - width = width, - height = hunk.removed.count, - anchor = 'SW', - bufpos = { hunk.added.start, 0 }, - } + local opts = { + relative = 'win', + win = cwin, + width = width, + height = hunk.removed.count, + anchor = 'SW', + bufpos = { hunk.added.start, 0 }, + } - local bufpos_offset = virt_lines_above and not topdelete and 1 or 0 - opts.bufpos[1] = opts.bufpos[1] - bufpos_offset + local bufpos_offset = virt_lines_above and not topdelete and 1 or 0 + opts.bufpos[1] = opts.bufpos[1] - bufpos_offset - local winid = api.nvim_open_win(pbufnr, false, opts) + local winid = api.nvim_open_win(pbufnr, false, opts) - -- Align buffer text by accounting for differences in the statuscolumn - local textoff = vim.fn.getwininfo(cwin)[1].textoff - local ptextoff = vim.fn.getwininfo(winid)[1].textoff - local col_offset = textoff - ptextoff + -- Align buffer text by accounting for differences in the statuscolumn + local textoff = vim.fn.getwininfo(cwin)[1].textoff + local ptextoff = vim.fn.getwininfo(winid)[1].textoff + local col_offset = textoff - ptextoff - if col_offset ~= 0 then - opts.width = opts.width - col_offset - opts.bufpos[2] = opts.bufpos[2] + col_offset - api.nvim_win_set_config(winid, opts) - end + if col_offset ~= 0 then + opts.width = opts.width - col_offset + opts.bufpos[2] = opts.bufpos[2] + col_offset + api.nvim_win_set_config(winid, opts) + end - vim.bo[pbufnr].filetype = vim.bo[bufnr].filetype - vim.bo[pbufnr].bufhidden = 'wipe' - vim.wo[winid].scrolloff = 0 - vim.wo[winid].relativenumber = false + vim.bo[pbufnr].filetype = vim.bo[bufnr].filetype + vim.bo[pbufnr].bufhidden = 'wipe' + vim.wo[winid].scrolloff = 0 + vim.wo[winid].relativenumber = false - api.nvim_win_call(winid, function() - -- Expand folds - vim.cmd('normal ' .. 'zR') + api.nvim_win_call(winid, function() + -- Expand folds + vim.cmd('normal ' .. 'zR') - -- Navigate to hunk - vim.cmd('normal ' .. tostring(hunk.removed.start) .. 'gg') - vim.cmd('normal ' .. vim.api.nvim_replace_termcodes('z', true, false, true)) - end) + -- Navigate to hunk + vim.cmd('normal ' .. tostring(hunk.removed.start) .. 'gg') + vim.cmd('normal ' .. vim.api.nvim_replace_termcodes('z', true, false, true)) + end) - -- Apply highlights + -- Apply highlights - for i = hunk.removed.start, hunk.removed.start + hunk.removed.count do - api.nvim_buf_set_extmark(pbufnr, nsd, i - 1, 0, { - hl_group = 'GitSignsDeleteVirtLn', - hl_eol = true, - end_row = i, - priority = 1000, - }) - end + for i = hunk.removed.start, hunk.removed.start + hunk.removed.count do + api.nvim_buf_set_extmark(pbufnr, nsd, i - 1, 0, { + hl_group = 'GitSignsDeleteVirtLn', + hl_eol = true, + end_row = i, + priority = 1000, + }) + end - local removed_regions = - require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) + local removed_regions = + require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) - for _, region in ipairs(removed_regions) do - local start_row = (hunk.removed.start - 1) + (region[1] - 1) - local start_col = region[3] - 1 - local end_col = region[4] - 1 - api.nvim_buf_set_extmark(pbufnr, nsd, start_row, start_col, { - hl_group = 'GitSignsDeleteVirtLnInline', - end_col = end_col, - end_row = start_row, - priority = 1001, - }) - end + for _, region in ipairs(removed_regions) do + local start_row = (hunk.removed.start - 1) + (region[1] - 1) + local start_col = region[3] - 1 + local end_col = region[4] - 1 + api.nvim_buf_set_extmark(pbufnr, nsd, start_row, start_col, { + hl_group = 'GitSignsDeleteVirtLnInline', + end_col = end_col, + end_row = start_row, + priority = 1001, + }) + end - return winid + return winid end function M.show_added(bufnr, nsw, hunk) - local start_row = hunk.added.start - 1 + local start_row = hunk.added.start - 1 - for offset = 0, hunk.added.count - 1 do - local row = start_row + offset - api.nvim_buf_set_extmark(bufnr, nsw, row, 0, { - end_row = row + 1, - hl_group = 'GitSignsAddPreview', - hl_eol = true, - priority = 1000, - }) - end + for offset = 0, hunk.added.count - 1 do + local row = start_row + offset + api.nvim_buf_set_extmark(bufnr, nsw, row, 0, { + end_row = row + 1, + hl_group = 'GitSignsAddPreview', + hl_eol = true, + priority = 1000, + }) + end - local _, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) + local _, added_regions = + require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) - for _, region in ipairs(added_regions) do - local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1 - api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, { - end_col = ecol, - hl_group = rtype == 'add' and 'GitSignsAddInline' or - rtype == 'change' and 'GitSignsChangeInline' or - 'GitSignsDeleteInline', - priority = 1001, - }) - end + for _, region in ipairs(added_regions) do + local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1 + api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, { + end_col = ecol, + hl_group = rtype == 'add' and 'GitSignsAddInline' + or rtype == 'change' and 'GitSignsChangeInline' + or 'GitSignsDeleteInline', + priority = 1001, + }) + end end local function update_show_deleted(bufnr) - local bcache = cache[bufnr] + local bcache = cache[bufnr] - clear_deleted(bufnr) - if config.show_deleted then - for _, hunk in ipairs(bcache.hunks or {}) do - M.show_deleted(bufnr, ns_rm, hunk) - end - end + clear_deleted(bufnr) + if config.show_deleted then + for _, hunk in ipairs(bcache.hunks or {}) do + M.show_deleted(bufnr, ns_rm, hunk) + end + end end local update_cnt = 0 @@ -375,137 +373,142 @@ local update_cnt = 0 -- whilst another one is in progress. If this happens then schedule another -- update after the current one has completed. M.update = throttle_by_id(function(bufnr, bcache) - local __FUNC__ = 'update' - bcache = bcache or cache[bufnr] - if not bcache then - eprint('Cache for buffer ' .. bufnr .. ' was nil') - return - end - local old_hunks, old_hunks_staged = bcache.hunks, bcache.hunks_staged - bcache.hunks, bcache.hunks_staged = nil, nil + local __FUNC__ = 'update' + bcache = bcache or cache[bufnr] + if not bcache then + eprint('Cache for buffer ' .. bufnr .. ' was nil') + return + end + local old_hunks, old_hunks_staged = bcache.hunks, bcache.hunks_staged + bcache.hunks, bcache.hunks_staged = nil, nil - scheduler_if_buf_valid(bufnr) - local buftext = util.buf_lines(bufnr) - local git_obj = bcache.git_obj + scheduler_if_buf_valid(bufnr) + local buftext = util.buf_lines(bufnr) + local git_obj = bcache.git_obj - if not bcache.compare_text or config._refresh_staged_on_update then - bcache.compare_text = git_obj:get_show_text(bcache:get_compare_rev()) - end + if not bcache.compare_text or config._refresh_staged_on_update then + bcache.compare_text = git_obj:get_show_text(bcache:get_compare_rev()) + end - bcache.hunks = run_diff(bcache.compare_text, buftext) + bcache.hunks = run_diff(bcache.compare_text, buftext) - if config._signs_staged_enable then - if not bcache.compare_text_head or config._refresh_staged_on_update then - bcache.compare_text_head = git_obj:get_show_text(bcache:get_staged_compare_rev()) - end - local hunks_head = run_diff(bcache.compare_text_head, buftext) - bcache.hunks_staged = gs_hunks.filter_common(hunks_head, bcache.hunks) - end + if config._signs_staged_enable then + if not bcache.compare_text_head or config._refresh_staged_on_update then + bcache.compare_text_head = git_obj:get_show_text(bcache:get_staged_compare_rev()) + end + local hunks_head = run_diff(bcache.compare_text_head, buftext) + bcache.hunks_staged = gs_hunks.filter_common(hunks_head, bcache.hunks) + end - scheduler_if_buf_valid(bufnr) + scheduler_if_buf_valid(bufnr) - -- Note the decoration provider may have invalidated bcache.hunks at this - -- point - if bcache.force_next_update or gs_hunks.compare_heads(bcache.hunks, old_hunks) or - gs_hunks.compare_heads(bcache.hunks_staged, old_hunks_staged) then - -- Apply signs to the window. Other signs will be added by the decoration - -- provider as they are drawn. - apply_win_signs(bufnr, vim.fn.line('w0'), vim.fn.line('w$'), true, git_obj.object_name == nil) + -- Note the decoration provider may have invalidated bcache.hunks at this + -- point + if + bcache.force_next_update + or gs_hunks.compare_heads(bcache.hunks, old_hunks) + or gs_hunks.compare_heads(bcache.hunks_staged, old_hunks_staged) + then + -- Apply signs to the window. Other signs will be added by the decoration + -- provider as they are drawn. + apply_win_signs(bufnr, vim.fn.line('w0'), vim.fn.line('w$'), true, git_obj.object_name == nil) - update_show_deleted(bufnr) - bcache.force_next_update = false + update_show_deleted(bufnr) + bcache.force_next_update = false - api.nvim_exec_autocmds('User', { - pattern = 'GitSignsUpdate', - modeline = false, - }) - end + api.nvim_exec_autocmds('User', { + pattern = 'GitSignsUpdate', + modeline = false, + }) + end - local summary = gs_hunks.get_summary(bcache.hunks) - summary.head = git_obj.repo.abbrev_head - Status:update(bufnr, summary) + local summary = gs_hunks.get_summary(bcache.hunks) + summary.head = git_obj.repo.abbrev_head + Status:update(bufnr, summary) - update_cnt = update_cnt + 1 + update_cnt = update_cnt + 1 - dprintf('updates: %s, jobs: %s', update_cnt, subprocess.job_cnt) + dprintf('updates: %s, jobs: %s', update_cnt, subprocess.job_cnt) end, true) M.detach = function(bufnr, keep_signs) - if not keep_signs then - -- Remove all signs - signs_normal:remove(bufnr) - if signs_staged then - signs_staged:remove(bufnr) - end - end + if not keep_signs then + -- Remove all signs + signs_normal:remove(bufnr) + if signs_staged then + signs_staged:remove(bufnr) + end + end end local function handle_moved(bufnr, bcache, old_relpath) - local git_obj = bcache.git_obj - local do_update = false + local git_obj = bcache.git_obj + local do_update = false - local new_name = git_obj:has_moved() - if new_name then - dprintf('File moved to %s', new_name) - git_obj.relpath = new_name - if not git_obj.orig_relpath then - git_obj.orig_relpath = old_relpath - end + local new_name = git_obj:has_moved() + if new_name then + dprintf('File moved to %s', new_name) + git_obj.relpath = new_name + if not git_obj.orig_relpath then + git_obj.orig_relpath = old_relpath + end + do_update = true + elseif git_obj.orig_relpath then + local orig_file = git_obj.repo.toplevel .. util.path_sep .. git_obj.orig_relpath + if git_obj:file_info(orig_file).relpath then + dprintf('Moved file reset') + git_obj.relpath = git_obj.orig_relpath + git_obj.orig_relpath = nil do_update = true - elseif git_obj.orig_relpath then - local orig_file = git_obj.repo.toplevel .. util.path_sep .. git_obj.orig_relpath - if git_obj:file_info(orig_file).relpath then - dprintf('Moved file reset') - git_obj.relpath = git_obj.orig_relpath - git_obj.orig_relpath = nil - do_update = true - end - else - -- File removed from index, do nothing - end + end + else + -- File removed from index, do nothing + end - if do_update then - git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath - bcache.file = git_obj.file - git_obj:update_file_info() - scheduler() + if do_update then + git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath + bcache.file = git_obj.file + git_obj:update_file_info() + scheduler() - local bufexists = vim.fn.bufexists(bcache.file) == 1 - local old_name = api.nvim_buf_get_name(bufnr) + local bufexists = vim.fn.bufexists(bcache.file) == 1 + local old_name = api.nvim_buf_get_name(bufnr) - if not bufexists then - util.buf_rename(bufnr, bcache.file) - end + if not bufexists then + util.buf_rename(bufnr, bcache.file) + end - local msg = bufexists and 'Cannot rename' or 'Renamed' - dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) - end + local msg = bufexists and 'Cannot rename' or 'Renamed' + dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) + end end - function M.watch_gitdir(bufnr, gitdir) - if not config.watch_gitdir.enable then - return - end + if not config.watch_gitdir.enable then + return + end - dprintf('Watching git dir') - local w = uv.new_fs_poll(true) - w:start(gitdir, config.watch_gitdir.interval, void(function(err) + dprintf('Watching git dir') + local w = uv.new_fs_poll(true) + w:start( + gitdir, + config.watch_gitdir.interval, + void(function(err) local __FUNC__ = 'watcher_cb' if err then - dprintf('Git dir update error: %s', err) - return + dprintf('Git dir update error: %s', err) + return end dprint('Git dir update') local bcache = cache[bufnr] if not bcache then - -- Very occasionally an external git operation may cause the buffer to - -- detach and update the git dir simultaneously. When this happens this - -- handler will trigger but there will be no cache. - dprint('Has detached, aborting') - return + -- Very occasionally an external git operation may cause the buffer to + -- detach and update the git dir simultaneously. When this happens this + -- handler will trigger but there will be no cache. + dprint('Has detached, aborting') + return end local git_obj = bcache.git_obj @@ -521,62 +524,63 @@ function M.watch_gitdir(bufnr, gitdir) git_obj:update_file_info() if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then - -- File was tracked but is no longer tracked. Must of been removed or - -- moved. Check if it was moved and switch to it. - handle_moved(bufnr, bcache, old_relpath) + -- File was tracked but is no longer tracked. Must of been removed or + -- moved. Check if it was moved and switch to it. + handle_moved(bufnr, bcache, old_relpath) end bcache:invalidate() M.update(bufnr, bcache) - end)) - return w + end) + ) + return w end function M.reset_signs() - -- Remove all signs - if signs_normal then - signs_normal:reset() - end - if signs_staged then - signs_staged:reset() - end + -- Remove all signs + if signs_normal then + signs_normal:reset() + end + if signs_staged then + signs_staged:reset() + end end local function on_win(_, _, bufnr, topline, botline_guess) - local bcache = cache[bufnr] - if not bcache or not bcache.hunks then - return false - end - local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr)) + local bcache = cache[bufnr] + if not bcache or not bcache.hunks then + return false + end + local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr)) - local untracked = bcache.git_obj.object_name == nil + local untracked = bcache.git_obj.object_name == nil - apply_win_signs(bufnr, topline + 1, botline + 1, false, untracked) + apply_win_signs(bufnr, topline + 1, botline + 1, false, untracked) - if not (config.word_diff and config.diff_opts.internal) then - return false - end + if not (config.word_diff and config.diff_opts.internal) then + return false + end end local function on_line(_, _, bufnr, row) - apply_word_diff(bufnr, row) + apply_word_diff(bufnr, row) end function M.setup() - -- Calling this before any await calls will stop nvim's intro messages being - -- displayed - api.nvim_set_decoration_provider(ns, { - on_win = on_win, - on_line = on_line, - }) + -- Calling this before any await calls will stop nvim's intro messages being + -- displayed + api.nvim_set_decoration_provider(ns, { + on_win = on_win, + on_line = on_line, + }) - signs_normal = Signs.new(config.signs) - if config._signs_staged_enable then - signs_staged = Signs.new(config._signs_staged, 'staged') - end + signs_normal = Signs.new(config.signs) + if config._signs_staged_enable then + signs_staged = Signs.new(config._signs_staged, 'staged') + end - M.update_debounced = debounce_trailing(config.update_debounce, void(M.update)) + M.update_debounced = debounce_trailing(config.update_debounce, void(M.update)) end return M diff --git a/lua/gitsigns/mappings.lua b/lua/gitsigns/mappings.lua index bd51b21..c67ef22 100644 --- a/lua/gitsigns/mappings.lua +++ b/lua/gitsigns/mappings.lua @@ -5,83 +5,91 @@ local validate = vim.validate local api = vim.api local valid_modes = { - n = 'n', v = 'v', x = 'x', i = 'i', o = 'o', t = 't', c = 'c', s = 's', - -- :map! and :map - ['!'] = '!', [' '] = '', + n = 'n', + v = 'v', + x = 'x', + i = 'i', + o = 'o', + t = 't', + c = 'c', + s = 's', + -- :map! and :map + ['!'] = '!', + [' '] = '', } local valid_options = { - buffer = 'boolean', - expr = 'boolean', - noremap = 'boolean', - nowait = 'boolean', - script = 'boolean', - silent = 'boolean', - unique = 'boolean', + buffer = 'boolean', + expr = 'boolean', + noremap = 'boolean', + nowait = 'boolean', + script = 'boolean', + silent = 'boolean', + unique = 'boolean', } local function validate_option_keywords(options) - for option_name, expected_type in pairs(valid_options) do - local value = options[option_name] - if value then - validate({ - [option_name] = { value, expected_type }, - }) - end - end + for option_name, expected_type in pairs(valid_options) do + local value = options[option_name] + if value then + validate({ + [option_name] = { value, expected_type }, + }) + end + end end local function apply_mappings(mappings, bufnr) - validate({ - mappings = { mappings, 'table' }, - }) + validate({ + mappings = { mappings, 'table' }, + }) - local default_options = {} - for key, val in pairs(mappings) do + local default_options = {} + for key, val in pairs(mappings) do + -- Skip any inline default keywords. + if valid_options[key] then + default_options[key] = val + end + end + + for key, opts in pairs(mappings) do + repeat -- Skip any inline default keywords. if valid_options[key] then - default_options[key] = val + break end - end - for key, opts in pairs(mappings) do - repeat - -- Skip any inline default keywords. - if valid_options[key] then - break - end + local rhs + local options + if type(opts) == 'string' then + rhs = opts + options = {} + elseif type(opts) == 'table' then + rhs = opts[1] + local boptions = {} + for k in pairs(valid_options) do + boptions[k] = opts[k] + end + options = boptions + else + error(('Invalid type for option rhs: %q = %s'):format(type(opts), vim.inspect(opts))) + end + options = vim.tbl_extend('keep', default_options, options) - local rhs - local options - if type(opts) == "string" then - rhs = opts - options = {} - elseif type(opts) == "table" then - rhs = opts[1] - local boptions = {} - for k in pairs(valid_options) do - boptions[k] = opts[k] - end - options = boptions - else - error(("Invalid type for option rhs: %q = %s"):format(type(opts), vim.inspect(opts))) - end - options = vim.tbl_extend('keep', default_options, options) + validate_option_keywords(options) - validate_option_keywords(options) + local mode, mapping = key:match('^(.)[ ]*(.+)$') - local mode, mapping = key:match("^(.)[ ]*(.+)$") + if not mode or not valid_modes[mode] then + error('Invalid mode specified for keymapping. mode=' .. mode) + end - if not mode or not valid_modes[mode] then - error("Invalid mode specified for keymapping. mode=" .. mode) - end + -- In case users haven't updated their config. + options.buffer = nil - -- In case users haven't updated their config. - options.buffer = nil - - api.nvim_buf_set_keymap(bufnr, mode, mapping, rhs, options) - until true - end + api.nvim_buf_set_keymap(bufnr, mode, mapping, rhs, options) + until true + end end return apply_mappings diff --git a/lua/gitsigns/message.lua b/lua/gitsigns/message.lua index 3104441..df14513 100644 --- a/lua/gitsigns/message.lua +++ b/lua/gitsigns/message.lua @@ -1,16 +1,11 @@ - - local M = {} - - - M.warn = vim.schedule_wrap(function(s, ...) - vim.notify(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' }) + vim.notify(s:format(...), vim.log.levels.WARN, { title = 'gitsigns' }) end) M.error = vim.schedule_wrap(function(s, ...) - vim.notify(s:format(...), vim.log.levels.ERROR, { title = 'gitsigns' }) + vim.notify(s:format(...), vim.log.levels.ERROR, { title = 'gitsigns' }) end) return M diff --git a/lua/gitsigns/popup.lua b/lua/gitsigns/popup.lua index fe6e2c8..ab1f797 100644 --- a/lua/gitsigns/popup.lua +++ b/lua/gitsigns/popup.lua @@ -1,245 +1,237 @@ -local popup = {HlMark = {}, } - - - - - - - - - +local popup = { HlMark = {} } local HlMark = popup.HlMark local api = vim.api local function bufnr_calc_width(bufnr, lines) - return api.nvim_buf_call(bufnr, function() - local width = 0 - for _, l in ipairs(lines) do - if vim.fn.type(l) == vim.v.t_string then - local len = vim.fn.strdisplaywidth(l) - if len > width then - width = len - end - end + return api.nvim_buf_call(bufnr, function() + local width = 0 + for _, l in ipairs(lines) do + if vim.fn.type(l) == vim.v.t_string then + local len = vim.fn.strdisplaywidth(l) + if len > width then + width = len + end end - return width + 1 -- Add 1 for some miinor padding - end) + end + return width + 1 -- Add 1 for some miinor padding + end) end -- Expand height until all lines are visible to account for wrapped lines. local function expand_height(winid, nlines) - local newheight = 0 - for _ = 0, 50 do - local winheight = api.nvim_win_get_height(winid) - if newheight > winheight then - -- Window must be max height - break - end - local wd = api.nvim_win_call(winid, function() - return vim.fn.line('w$') - end) - if wd >= nlines then - break - end - newheight = winheight + nlines - wd - api.nvim_win_set_height(winid, newheight) - end + local newheight = 0 + for _ = 0, 50 do + local winheight = api.nvim_win_get_height(winid) + if newheight > winheight then + -- Window must be max height + break + end + local wd = api.nvim_win_call(winid, function() + return vim.fn.line('w$') + end) + if wd >= nlines then + break + end + newheight = winheight + nlines - wd + api.nvim_win_set_height(winid, newheight) + end end local function offset_hlmarks(hlmarks, row_offset) - for _, h in ipairs(hlmarks) do - if h.start_row then - h.start_row = h.start_row + row_offset - end - if h.end_row then - h.end_row = h.end_row + row_offset - end - end + for _, h in ipairs(hlmarks) do + if h.start_row then + h.start_row = h.start_row + row_offset + end + if h.end_row then + h.end_row = h.end_row + row_offset + end + end end local function process_linesspec(fmt) - local lines = {} - local hls = {} + local lines = {} + local hls = {} - local row = 0 - for _, section in ipairs(fmt) do - local sec = {} - local pos = 0 - for _, part in ipairs(section) do - local text = part[1] - local hl = part[2] + local row = 0 + for _, section in ipairs(fmt) do + local sec = {} + local pos = 0 + for _, part in ipairs(section) do + local text = part[1] + local hl = part[2] - sec[#sec + 1] = text + sec[#sec + 1] = text - local srow = row - local scol = pos + local srow = row + local scol = pos - local ts = vim.split(text, '\n') + local ts = vim.split(text, '\n') - if #ts > 1 then - pos = 0 - row = row + #ts - 1 - else - pos = pos + #text - end - - if type(hl) == "string" then - hls[#hls + 1] = { - hl_group = hl, - start_row = srow, - end_row = row, - start_col = scol, - end_col = pos, - } - else -- hl is {HlMark} - offset_hlmarks(hl, srow) - vim.list_extend(hls, hl) - end + if #ts > 1 then + pos = 0 + row = row + #ts - 1 + else + pos = pos + #text end - for _, l in ipairs(vim.split(table.concat(sec, ''), '\n')) do - lines[#lines + 1] = l - end - row = row + 1 - end - return lines, hls + if type(hl) == 'string' then + hls[#hls + 1] = { + hl_group = hl, + start_row = srow, + end_row = row, + start_col = scol, + end_col = pos, + } + else -- hl is {HlMark} + offset_hlmarks(hl, srow) + vim.list_extend(hls, hl) + end + end + for _, l in ipairs(vim.split(table.concat(sec, ''), '\n')) do + lines[#lines + 1] = l + end + row = row + 1 + end + + return lines, hls end local function close_all_but(id) - for _, winid in ipairs(api.nvim_list_wins()) do - if vim.w[winid].gitsigns_preview ~= nil and - vim.w[winid].gitsigns_preview ~= id then - pcall(api.nvim_win_close, winid, true) - end - end + for _, winid in ipairs(api.nvim_list_wins()) do + if vim.w[winid].gitsigns_preview ~= nil and vim.w[winid].gitsigns_preview ~= id then + pcall(api.nvim_win_close, winid, true) + end + end end function popup.close(id) - for _, winid in ipairs(api.nvim_list_wins()) do - if vim.w[winid].gitsigns_preview == id then - pcall(api.nvim_win_close, winid, true) - end - end + for _, winid in ipairs(api.nvim_list_wins()) do + if vim.w[winid].gitsigns_preview == id then + pcall(api.nvim_win_close, winid, true) + end + end end function popup.create0(lines, opts, id) - -- Close any popups not matching id - close_all_but(id) + -- Close any popups not matching id + close_all_but(id) - local ts = vim.bo.tabstop - local bufnr = api.nvim_create_buf(false, true) - assert(bufnr, "Failed to create buffer") + local ts = vim.bo.tabstop + local bufnr = api.nvim_create_buf(false, true) + assert(bufnr, 'Failed to create buffer') - -- In case nvim was opened with '-M' - vim.bo[bufnr].modifiable = true - api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) - vim.bo[bufnr].modifiable = false + -- In case nvim was opened with '-M' + vim.bo[bufnr].modifiable = true + api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) + vim.bo[bufnr].modifiable = false - -- Set tabstop before calculating the buffer width so that the correct width - -- is calculated - vim.bo[bufnr].tabstop = ts + -- Set tabstop before calculating the buffer width so that the correct width + -- is calculated + vim.bo[bufnr].tabstop = ts - local opts1 = vim.deepcopy(opts or {}) - opts1.height = opts1.height or #lines -- Guess, adjust later - opts1.width = opts1.width or bufnr_calc_width(bufnr, lines) + local opts1 = vim.deepcopy(opts or {}) + opts1.height = opts1.height or #lines -- Guess, adjust later + opts1.width = opts1.width or bufnr_calc_width(bufnr, lines) - local winid = api.nvim_open_win(bufnr, false, opts1) + local winid = api.nvim_open_win(bufnr, false, opts1) - vim.w[winid].gitsigns_preview = id or true + vim.w[winid].gitsigns_preview = id or true - if not opts.height then - expand_height(winid, #lines) - end + if not opts.height then + expand_height(winid, #lines) + end - if opts1.style == 'minimal' then - -- If 'signcolumn' = auto:1-2, then a empty signcolumn will appear and cause - -- line wrapping. - vim.wo[winid].signcolumn = 'no' - end + if opts1.style == 'minimal' then + -- If 'signcolumn' = auto:1-2, then a empty signcolumn will appear and cause + -- line wrapping. + vim.wo[winid].signcolumn = 'no' + end - -- Close the popup when navigating to any window which is not the preview - -- itself. - local group = 'gitsigns_popup' - local group_id = api.nvim_create_augroup(group, {}) - local old_cursor = api.nvim_win_get_cursor(0) + -- Close the popup when navigating to any window which is not the preview + -- itself. + local group = 'gitsigns_popup' + local group_id = api.nvim_create_augroup(group, {}) + local old_cursor = api.nvim_win_get_cursor(0) - api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { - group = group_id, - callback = function() - local cursor = api.nvim_win_get_cursor(0) - -- Did the cursor REALLY change (neovim/neovim#12923) - if (old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2]) and - api.nvim_get_current_win() ~= winid then - -- Clear the augroup - api.nvim_create_augroup(group, {}) - pcall(api.nvim_win_close, winid, true) - return - end - old_cursor = cursor - end, - }) + api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { + group = group_id, + callback = function() + local cursor = api.nvim_win_get_cursor(0) + -- Did the cursor REALLY change (neovim/neovim#12923) + if + (old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2]) + and api.nvim_get_current_win() ~= winid + then + -- Clear the augroup + api.nvim_create_augroup(group, {}) + pcall(api.nvim_win_close, winid, true) + return + end + old_cursor = cursor + end, + }) - api.nvim_create_autocmd('WinClosed', { - pattern = tostring(winid), - group = group_id, - callback = function() - -- Clear the augroup - api.nvim_create_augroup(group, {}) - end, - }) + api.nvim_create_autocmd('WinClosed', { + pattern = tostring(winid), + group = group_id, + callback = function() + -- Clear the augroup + api.nvim_create_augroup(group, {}) + end, + }) - -- update window position to follow the cursor when scrolling - api.nvim_create_autocmd('WinScrolled', { - buffer = api.nvim_get_current_buf(), - group = group_id, - callback = function() - if api.nvim_win_is_valid(winid) then - api.nvim_win_set_config(winid, opts1) - end - end, - }) + -- update window position to follow the cursor when scrolling + api.nvim_create_autocmd('WinScrolled', { + buffer = api.nvim_get_current_buf(), + group = group_id, + callback = function() + if api.nvim_win_is_valid(winid) then + api.nvim_win_set_config(winid, opts1) + end + end, + }) - return winid, bufnr + return winid, bufnr end local ns = api.nvim_create_namespace('gitsigns_popup') function popup.create(lines_spec, opts, id) - local lines, highlights = process_linesspec(lines_spec) - local winid, bufnr = popup.create0(lines, opts, id) + local lines, highlights = process_linesspec(lines_spec) + local winid, bufnr = popup.create0(lines, opts, id) - for _, hl in ipairs(highlights) do - local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, { - hl_group = hl.hl_group, - end_row = hl.end_row, - end_col = hl.end_col, - hl_eol = true, - }) - if not ok then - error(vim.inspect(hl) .. '\n' .. err) - end - end + for _, hl in ipairs(highlights) do + local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, { + hl_group = hl.hl_group, + end_row = hl.end_row, + end_col = hl.end_col, + hl_eol = true, + }) + if not ok then + error(vim.inspect(hl) .. '\n' .. err) + end + end - return winid, bufnr + return winid, bufnr end function popup.is_open(id) - for _, winid in ipairs(api.nvim_list_wins()) do - if vim.w[winid].gitsigns_preview == id then - return winid - end - end - return nil + for _, winid in ipairs(api.nvim_list_wins()) do + if vim.w[winid].gitsigns_preview == id then + return winid + end + end + return nil end function popup.focus_open(id) - local winid = popup.is_open(id) - if winid then - api.nvim_set_current_win(winid) - end - return winid + local winid = popup.is_open(id) + if winid then + api.nvim_set_current_win(winid) + end + return winid end return popup diff --git a/lua/gitsigns/repeat.lua b/lua/gitsigns/repeat.lua index 66b5fcb..8026357 100644 --- a/lua/gitsigns/repeat.lua +++ b/lua/gitsigns/repeat.lua @@ -2,27 +2,27 @@ local api = vim.api local M = {} - - - function M.mk_repeatable(fn) - return function(...) - local args = { ... } - local nargs = select('#', ...) - vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action" + return function(...) + local args = { ... } + local nargs = select('#', ...) + vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action" - M.repeat_action = function() - fn(unpack(args, 1, nargs)) - if vim.fn.exists('*repeat#set') == 1 then - local action = api.nvim_replace_termcodes( - string.format('call %s()', vim.go.operatorfunc), - true, true, true) - vim.fn['repeat#set'](action, -1) - end + M.repeat_action = function() + fn(unpack(args, 1, nargs)) + if vim.fn.exists('*repeat#set') == 1 then + local action = api.nvim_replace_termcodes( + string.format('call %s()', vim.go.operatorfunc), + true, + true, + true + ) + vim.fn['repeat#set'](action, -1) end + end - vim.cmd('normal! g@l') - end + vim.cmd('normal! g@l') + end end return M diff --git a/lua/gitsigns/signs.lua b/lua/gitsigns/signs.lua index 92ae0df..0cd6870 100644 --- a/lua/gitsigns/signs.lua +++ b/lua/gitsigns/signs.lua @@ -1,5 +1,4 @@ local config = require('gitsigns.config').config -local SignsConfig = require('gitsigns.config').Config.SignsConfig local dprint = require('gitsigns.debug.log').dprint @@ -10,34 +9,34 @@ local B = require('gitsigns.signs.base') -- end function B.new(cfg, name) - local __FUNC__ = 'signs.init' - local C - if config._extmark_signs then - dprint('Using extmark signs') - C = require('gitsigns.signs.extmarks') - else - dprint('Using vimfn signs') - C = require('gitsigns.signs.vimfn') - end + local __FUNC__ = 'signs.init' + local C + if config._extmark_signs then + dprint('Using extmark signs') + C = require('gitsigns.signs.extmarks') + else + dprint('Using vimfn signs') + C = require('gitsigns.signs.vimfn') + end - local hls = (name == 'staged' and config._signs_staged or config.signs) - -- Add when config.signs.*.[hl,numhl,linehl] are removed - -- for _, t in ipairs { - -- 'add', - -- 'change', - -- 'delete', - -- 'topdelete', - -- 'changedelete', - -- 'untracked', - -- } do - -- local hl = string.format('GitSigns%s%s', name, capitalise_word(t)) - -- obj.hls[t] = { - -- hl = hl, - -- numhl = hl..'Nr', - -- linehl = hl..'Ln', - -- } - -- end - return C._new(cfg, hls, name) + local hls = (name == 'staged' and config._signs_staged or config.signs) + -- Add when config.signs.*.[hl,numhl,linehl] are removed + -- for _, t in ipairs { + -- 'add', + -- 'change', + -- 'delete', + -- 'topdelete', + -- 'changedelete', + -- 'untracked', + -- } do + -- local hl = string.format('GitSigns%s%s', name, capitalise_word(t)) + -- obj.hls[t] = { + -- hl = hl, + -- numhl = hl..'Nr', + -- linehl = hl..'Ln', + -- } + -- end + return C._new(cfg, hls, name) end return B diff --git a/lua/gitsigns/signs/base.lua b/lua/gitsigns/signs/base.lua index 3ed5fa6..0fbce95 100644 --- a/lua/gitsigns/signs/base.lua +++ b/lua/gitsigns/signs/base.lua @@ -1,46 +1,9 @@ local SignsConfig = require('gitsigns.config').Config.SignsConfig -local M = {Sign = {}, HlDef = {}, } - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- Used by signs/extmarks.tl - - - -- Used by signs/vimfn.tl - - - - - - - - +local M = { Sign = {}, HlDef = {} } +-- Used by signs/extmarks.tl +-- Used by signs/vimfn.tl return M diff --git a/lua/gitsigns/signs/extmarks.lua b/lua/gitsigns/signs/extmarks.lua index eb118ee..9b8f3df 100644 --- a/lua/gitsigns/signs/extmarks.lua +++ b/lua/gitsigns/signs/extmarks.lua @@ -10,80 +10,85 @@ local M = {} local group_base = 'gitsigns_extmark_signs_' function M._new(cfg, hls, name) - local self = setmetatable({}, { __index = M }) - self.config = cfg - self.hls = hls - self.group = group_base .. (name or '') - self.ns = api.nvim_create_namespace(self.group) - return self + local self = setmetatable({}, { __index = M }) + self.config = cfg + self.hls = hls + self.group = group_base .. (name or '') + self.ns = api.nvim_create_namespace(self.group) + return self end function M:on_lines(buf, _, last_orig, last_new) - -- Remove extmarks on line deletions to mimic - -- the behaviour of vim signs. - if last_orig > last_new then - self:remove(buf, last_new + 1, last_orig) - end + -- Remove extmarks on line deletions to mimic + -- the behaviour of vim signs. + if last_orig > last_new then + self:remove(buf, last_new + 1, last_orig) + end end function M:remove(bufnr, start_lnum, end_lnum) - if start_lnum then - api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum - 1, end_lnum or start_lnum) - else - api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1) - end + if start_lnum then + api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum - 1, end_lnum or start_lnum) + else + api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1) + end end function M:add(bufnr, signs) - if not config.signcolumn and not config.numhl and not config.linehl then - -- Don't place signs if it won't show anything - return - end + if not config.signcolumn and not config.numhl and not config.linehl then + -- Don't place signs if it won't show anything + return + end - for _, s in ipairs(signs) do - if not self:contains(bufnr, s.lnum) then - local cs = self.config[s.type] - local text = cs.text - if config.signcolumn and cs.show_count and s.count then - local count = s.count - local cc = config.count_chars - local count_char = cc[count] or cc['+'] or '' - text = cs.text .. count_char - end - - local hls = self.hls[s.type] - - local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum - 1, -1, { - id = s.lnum, - sign_text = config.signcolumn and text or '', - priority = config.sign_priority, - sign_hl_group = hls.hl, - number_hl_group = config.numhl and hls.numhl or nil, - line_hl_group = config.linehl and hls.linehl or nil, - }) - - if not ok and config.debug_mode then - vim.schedule(function() - error(table.concat({ - string.format('Error placing extmark on line %d', s.lnum), - err, - }, '\n')) - end) - end + for _, s in ipairs(signs) do + if not self:contains(bufnr, s.lnum) then + local cs = self.config[s.type] + local text = cs.text + if config.signcolumn and cs.show_count and s.count then + local count = s.count + local cc = config.count_chars + local count_char = cc[count] or cc['+'] or '' + text = cs.text .. count_char end - end + + local hls = self.hls[s.type] + + local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum - 1, -1, { + id = s.lnum, + sign_text = config.signcolumn and text or '', + priority = config.sign_priority, + sign_hl_group = hls.hl, + number_hl_group = config.numhl and hls.numhl or nil, + line_hl_group = config.linehl and hls.linehl or nil, + }) + + if not ok and config.debug_mode then + vim.schedule(function() + error(table.concat({ + string.format('Error placing extmark on line %d', s.lnum), + err, + }, '\n')) + end) + end + end + end end function M:contains(bufnr, start, last) - local marks = api.nvim_buf_get_extmarks( - bufnr, self.ns, { start - 1, 0 }, { last or start, 0 }, { limit = 1 }) - return #marks > 0 + local marks = api.nvim_buf_get_extmarks( + bufnr, + self.ns, + { start - 1, 0 }, + { last or start, 0 }, + { limit = 1 } + ) + return #marks > 0 end function M:reset() - for _, buf in ipairs(api.nvim_list_bufs()) do - self:remove(buf) - end + for _, buf in ipairs(api.nvim_list_bufs()) do + self:remove(buf) + end end return M diff --git a/lua/gitsigns/signs/vimfn.lua b/lua/gitsigns/signs/vimfn.lua index 77b7784..9b2c1c6 100644 --- a/lua/gitsigns/signs/vimfn.lua +++ b/lua/gitsigns/signs/vimfn.lua @@ -18,146 +18,145 @@ local M = {} -- - skip adding a sign if it has already been placed. local function capitalise_word(x) - return x:sub(1, 1):upper() .. x:sub(2) + return x:sub(1, 1):upper() .. x:sub(2) end local sign_define_cache = {} local sign_name_cache = {} local function get_sign_name(name, stype) - local key = name .. stype - if not sign_name_cache[key] then - sign_name_cache[key] = string.format( - '%s%s%s', 'GitSigns', capitalise_word(key), capitalise_word(stype)) - end + local key = name .. stype + if not sign_name_cache[key] then + sign_name_cache[key] = + string.format('%s%s%s', 'GitSigns', capitalise_word(key), capitalise_word(stype)) + end - return sign_name_cache[key] + return sign_name_cache[key] end local function sign_get(name) - if not sign_define_cache[name] then - local s = fn.sign_getdefined(name) - if not vim.tbl_isempty(s) then - sign_define_cache[name] = s - end - end - return sign_define_cache[name] + if not sign_define_cache[name] then + local s = fn.sign_getdefined(name) + if not vim.tbl_isempty(s) then + sign_define_cache[name] = s + end + end + return sign_define_cache[name] end local function define_sign(name, opts, redefine) - if redefine then - sign_define_cache[name] = nil - fn.sign_undefine(name) - fn.sign_define(name, opts) - elseif not sign_get(name) then - fn.sign_define(name, opts) - end + if redefine then + sign_define_cache[name] = nil + fn.sign_undefine(name) + fn.sign_define(name, opts) + elseif not sign_get(name) then + fn.sign_define(name, opts) + end end local function define_signs(obj, redefine) - -- Define signs - for stype, cs in pairs(obj.config) do - local hls = obj.hls[stype] - define_sign(get_sign_name(obj.name, stype), { - texthl = hls.hl, - text = config.signcolumn and cs.text or nil, - numhl = config.numhl and hls.numhl or nil, - linehl = config.linehl and hls.linehl or nil, - }, redefine) - end + -- Define signs + for stype, cs in pairs(obj.config) do + local hls = obj.hls[stype] + define_sign(get_sign_name(obj.name, stype), { + texthl = hls.hl, + text = config.signcolumn and cs.text or nil, + numhl = config.numhl and hls.numhl or nil, + linehl = config.linehl and hls.linehl or nil, + }, redefine) + end end local group_base = 'gitsigns_vimfn_signs_' function M._new(cfg, hls, name) - local self = setmetatable({}, { __index = M }) - self.name = name or '' - self.group = group_base .. (name or '') - self.config = cfg - self.hls = hls - self.placed = emptytable() + local self = setmetatable({}, { __index = M }) + self.name = name or '' + self.group = group_base .. (name or '') + self.config = cfg + self.hls = hls + self.placed = emptytable() - define_signs(self, false) + define_signs(self, false) - return self + return self end -function M:on_lines(_, _, _, _) -end +function M:on_lines(_, _, _, _) end function M:remove(bufnr, start_lnum, end_lnum) - end_lnum = end_lnum or start_lnum + end_lnum = end_lnum or start_lnum - if start_lnum then - for lnum = start_lnum, end_lnum do - self.placed[bufnr][lnum] = nil - fn.sign_unplace(self.group, { buffer = bufnr, id = lnum }) - end - else - self.placed[bufnr] = nil - fn.sign_unplace(self.group, { buffer = bufnr }) - end + if start_lnum then + for lnum = start_lnum, end_lnum do + self.placed[bufnr][lnum] = nil + fn.sign_unplace(self.group, { buffer = bufnr, id = lnum }) + end + else + self.placed[bufnr] = nil + fn.sign_unplace(self.group, { buffer = bufnr }) + end end function M:add(bufnr, signs) - if not config.signcolumn and not config.numhl and not config.linehl then - -- Don't place signs if it won't show anything - return - end + if not config.signcolumn and not config.numhl and not config.linehl then + -- Don't place signs if it won't show anything + return + end - local to_place = {} + local to_place = {} - for _, s in ipairs(signs) do - local sign_name = get_sign_name(self.name, s.type) + for _, s in ipairs(signs) do + local sign_name = get_sign_name(self.name, s.type) - local cs = self.config[s.type] - if config.signcolumn and cs.show_count and s.count then - local count = s.count - local cc = config.count_chars - local count_suffix = cc[count] and tostring(count) or (cc['+'] and 'Plus') or '' - local count_char = cc[count] or cc['+'] or '' - local hls = self.hls[s.type] - sign_name = sign_name .. count_suffix - define_sign(sign_name, { - texthl = hls.hl, - text = config.signcolumn and cs.text .. count_char or '', - numhl = config.numhl and hls.numhl or nil, - linehl = config.linehl and hls.linehl or nil, - }) - end + local cs = self.config[s.type] + if config.signcolumn and cs.show_count and s.count then + local count = s.count + local cc = config.count_chars + local count_suffix = cc[count] and tostring(count) or (cc['+'] and 'Plus') or '' + local count_char = cc[count] or cc['+'] or '' + local hls = self.hls[s.type] + sign_name = sign_name .. count_suffix + define_sign(sign_name, { + texthl = hls.hl, + text = config.signcolumn and cs.text .. count_char or '', + numhl = config.numhl and hls.numhl or nil, + linehl = config.linehl and hls.linehl or nil, + }) + end - if not self.placed[bufnr][s.lnum] then - local sign = { - id = s.lnum, - group = self.group, - name = sign_name, - buffer = bufnr, - lnum = s.lnum, - priority = config.sign_priority, - } - self.placed[bufnr][s.lnum] = s - to_place[#to_place + 1] = sign - end - end + if not self.placed[bufnr][s.lnum] then + local sign = { + id = s.lnum, + group = self.group, + name = sign_name, + buffer = bufnr, + lnum = s.lnum, + priority = config.sign_priority, + } + self.placed[bufnr][s.lnum] = s + to_place[#to_place + 1] = sign + end + end - if #to_place > 0 then - fn.sign_placelist(to_place) - end + if #to_place > 0 then + fn.sign_placelist(to_place) + end end function M:contains(bufnr, start, last) - for i = start + 1, last + 1 do - if self.placed[bufnr][i] then - return true - end - end - return false + for i = start + 1, last + 1 do + if self.placed[bufnr][i] then + return true + end + end + return false end function M:reset() - self.placed = emptytable() - fn.sign_unplace(self.group) - define_signs(self, true) + self.placed = emptytable() + fn.sign_unplace(self.group) + define_signs(self, true) end return M diff --git a/lua/gitsigns/status.lua b/lua/gitsigns/status.lua index d4e92fb..0a41075 100644 --- a/lua/gitsigns/status.lua +++ b/lua/gitsigns/status.lua @@ -1,46 +1,38 @@ local api = vim.api - local StatusObj = {} - - - - - - - local Status = { - StatusObj = StatusObj, + StatusObj = StatusObj, } function Status:update(bufnr, status) - if not api.nvim_buf_is_loaded(bufnr) then - return - end - local bstatus = vim.b[bufnr].gitsigns_status_dict - if bstatus then - status = vim.tbl_extend('force', bstatus, status) - end - vim.b[bufnr].gitsigns_head = status.head or '' - vim.b[bufnr].gitsigns_status_dict = status + if not api.nvim_buf_is_loaded(bufnr) then + return + end + local bstatus = vim.b[bufnr].gitsigns_status_dict + if bstatus then + status = vim.tbl_extend('force', bstatus, status) + end + vim.b[bufnr].gitsigns_head = status.head or '' + vim.b[bufnr].gitsigns_status_dict = status - local config = require('gitsigns.config').config + local config = require('gitsigns.config').config - vim.b[bufnr].gitsigns_status = config.status_formatter(status) + vim.b[bufnr].gitsigns_status = config.status_formatter(status) end function Status:clear(bufnr) - if not api.nvim_buf_is_loaded(bufnr) then - return - end - vim.b[bufnr].gitsigns_head = nil - vim.b[bufnr].gitsigns_status_dict = nil - vim.b[bufnr].gitsigns_status = nil + if not api.nvim_buf_is_loaded(bufnr) then + return + end + vim.b[bufnr].gitsigns_head = nil + vim.b[bufnr].gitsigns_status_dict = nil + vim.b[bufnr].gitsigns_status = nil end function Status:clear_diff(bufnr) - self:update(bufnr, { added = 0, removed = 0, changed = 0 }) + self:update(bufnr, { added = 0, removed = 0, changed = 0 }) end return Status diff --git a/lua/gitsigns/subprocess.lua b/lua/gitsigns/subprocess.lua index a42ad3e..1fd76d7 100644 --- a/lua/gitsigns/subprocess.lua +++ b/lua/gitsigns/subprocess.lua @@ -1,119 +1,108 @@ -local log = require("gitsigns.debug.log") -local guv = require("gitsigns.uv") +local log = require('gitsigns.debug.log') +local guv = require('gitsigns.uv') local uv = vim.loop -local M = {JobSpec = {}, } - - - - - - - - - +local M = { JobSpec = {} } M.job_cnt = 0 --- @param ... uv_pipe_t local function try_close(...) - for i = 1, select('#', ...) do - local pipe = select(i, ...) - if pipe and not pipe:is_closing() then - pipe:close() - end - end + for i = 1, select('#', ...) do + local pipe = select(i, ...) + if pipe and not pipe:is_closing() then + pipe:close() + end + end end --- @param pipe uv_pipe_t --- @param x string[]|string local function handle_writer(pipe, x) - if type(x) == "table" then - for i, v in ipairs(x) do - pipe:write(v) - if i ~= #(x) then - pipe:write("\n") - else - pipe:write("\n", function() - try_close(pipe) - end) - end + if type(x) == 'table' then + for i, v in ipairs(x) do + pipe:write(v) + if i ~= #x then + pipe:write('\n') + else + pipe:write('\n', function() + try_close(pipe) + end) end - elseif x then - -- write is string - pipe:write(x, function() - try_close(pipe) - end) - end + end + elseif x then + -- write is string + pipe:write(x, function() + try_close(pipe) + end) + end end --- @param pipe uv_pipe_t --- @param output string[] local function handle_reader(pipe, output) - pipe:read_start(function(err, data) - if err then - log.eprint(err) - end - if data then - output[#output + 1] = data - else - try_close(pipe) - end - end) + pipe:read_start(function(err, data) + if err then + log.eprint(err) + end + if data then + output[#output + 1] = data + else + try_close(pipe) + end + end) end --- @param obj table --- @param callback fun(_: integer, _: integer, _: string?, _: string?) function M.run_job(obj, callback) - local __FUNC__ = 'run_job' - if log.debug_mode then - local cmd = obj.command .. ' ' .. table.concat(obj.args, ' ') - log.dprint(cmd) - end + local __FUNC__ = 'run_job' + if log.debug_mode then + local cmd = obj.command .. ' ' .. table.concat(obj.args, ' ') + log.dprint(cmd) + end - local stdout_data = {} - local stderr_data = {} + local stdout_data = {} + local stderr_data = {} - local stdout = guv.new_pipe(false) - local stderr = guv.new_pipe(false) - local stdin - if obj.writer then - stdin = guv.new_pipe(false) - end + local stdout = guv.new_pipe(false) + local stderr = guv.new_pipe(false) + local stdin + if obj.writer then + stdin = guv.new_pipe(false) + end - --- @type uv_process_t?, integer|string - local handle, _pid - handle, _pid = vim.loop.spawn(obj.command, { - args = obj.args, - stdio = { stdin, stdout, stderr }, - cwd = obj.cwd, - }, - function(code, signal) - if handle then - handle:close() - end - stdout:read_stop() - stderr:read_stop() + --- @type uv_process_t?, integer|string + local handle, _pid + handle, _pid = vim.loop.spawn(obj.command, { + args = obj.args, + stdio = { stdin, stdout, stderr }, + cwd = obj.cwd, + }, function(code, signal) + if handle then + handle:close() + end + stdout:read_stop() + stderr:read_stop() - try_close(stdin, stdout, stderr) + try_close(stdin, stdout, stderr) - local stdout_result = #stdout_data > 0 and table.concat(stdout_data) or nil - local stderr_result = #stderr_data > 0 and table.concat(stderr_data) or nil + local stdout_result = #stdout_data > 0 and table.concat(stdout_data) or nil + local stderr_result = #stderr_data > 0 and table.concat(stderr_data) or nil - callback(code, signal, stdout_result, stderr_result) - end) + callback(code, signal, stdout_result, stderr_result) + end) + if not handle then + try_close(stdin, stdout, stderr) + error(debug.traceback('Failed to spawn process: ' .. vim.inspect(obj))) + end - if not handle then - try_close(stdin, stdout, stderr) - error(debug.traceback("Failed to spawn process: " .. vim.inspect(obj))) - end + handle_reader(stdout, stdout_data) + handle_reader(stderr, stderr_data) + handle_writer(stdin, obj.writer) - handle_reader(stdout, stdout_data) - handle_reader(stderr, stderr_data) - handle_writer(stdin, obj.writer) - - M.job_cnt = M.job_cnt + 1 + M.job_cnt = M.job_cnt + 1 end return M diff --git a/lua/gitsigns/test.lua b/lua/gitsigns/test.lua index 9c3958c..be60d9b 100644 --- a/lua/gitsigns/test.lua +++ b/lua/gitsigns/test.lua @@ -1,35 +1,34 @@ - local M = {} - - local function eq(act, exp) - assert(act == exp, string.format('%s != %s', act, exp)) + assert(act == exp, string.format('%s != %s', act, exp)) end M._tests = {} M._tests.expand_format = function() - local util = require('gitsigns.util') - assert('hello % world % 2021' == util.expand_format(' % % ', { - var1 = 'hello', var2 = 'world', var_time = 1616838297, })) + local util = require('gitsigns.util') + assert('hello % world % 2021' == util.expand_format(' % % ', { + var1 = 'hello', + var2 = 'world', + var_time = 1616838297, + })) end - M._tests.test_args = function() - local parse_args = require('gitsigns.cli.argparse').parse_args + local parse_args = require('gitsigns.cli.argparse').parse_args - local pos_args, named_args = parse_args('hello there key=value, key1="a b c"') + local pos_args, named_args = parse_args('hello there key=value, key1="a b c"') - eq(pos_args[1], 'hello') - eq(pos_args[2], 'there') - eq(named_args.key, 'value,') - eq(named_args.key1, 'a b c') + eq(pos_args[1], 'hello') + eq(pos_args[2], 'there') + eq(named_args.key, 'value,') + eq(named_args.key1, 'a b c') - pos_args, named_args = parse_args('base=HEAD~1 posarg') + pos_args, named_args = parse_args('base=HEAD~1 posarg') - eq(named_args.base, 'HEAD~1') - eq(pos_args[1], 'posarg') + eq(named_args.base, 'HEAD~1') + eq(pos_args[1], 'posarg') end return M diff --git a/lua/gitsigns/util.lua b/lua/gitsigns/util.lua index e7c9d62..609defd 100644 --- a/lua/gitsigns/util.lua +++ b/lua/gitsigns/util.lua @@ -1,41 +1,37 @@ local M = {} - - - - function M.path_exists(path) - return vim.loop.fs_stat(path) and true or false + return vim.loop.fs_stat(path) and true or false end local jit_os --- @type string if jit then - jit_os = jit.os:lower() + jit_os = jit.os:lower() end local is_unix = false if jit_os then - is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd' + is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd' else - local binfmt = package.cpath:match("%p[\\|/]?%p(%a+)") - is_unix = binfmt ~= "dll" + local binfmt = package.cpath:match('%p[\\|/]?%p(%a+)') + is_unix = binfmt ~= 'dll' end --- @param file string --- @return string function M.dirname(file) - return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep)) + return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep)) end --- @param file string --- @return string[] function M.file_lines(file) - local text = {} --- @type string[] - for line in io.lines(file) do - text[#text + 1] = line - end - return text + local text = {} --- @type string[] + for line in io.lines(file) do + text[#text + 1] = line + end + return text end M.path_sep = package.config:sub(1, 1) @@ -43,31 +39,31 @@ M.path_sep = package.config:sub(1, 1) --- @param bufnr integer --- @return string[] function M.buf_lines(bufnr) - -- nvim_buf_get_lines strips carriage returns if fileformat==dos - local buftext = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if vim.bo[bufnr].fileformat == 'dos' then - for i = 1, #buftext do - buftext[i] = buftext[i] .. '\r' - end - end - return buftext + -- nvim_buf_get_lines strips carriage returns if fileformat==dos + local buftext = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + if vim.bo[bufnr].fileformat == 'dos' then + for i = 1, #buftext do + buftext[i] = buftext[i] .. '\r' + end + end + return buftext end --- @param buf integer local function delete_alt(buf) - local alt = vim.api.nvim_buf_call(buf, function() - return vim.fn.bufnr('#') - end) - if alt ~= buf and alt ~= -1 then - pcall(vim.api.nvim_buf_delete, alt, { force = true }) - end + local alt = vim.api.nvim_buf_call(buf, function() + return vim.fn.bufnr('#') + end) + if alt ~= buf and alt ~= -1 then + pcall(vim.api.nvim_buf_delete, alt, { force = true }) + end end --- @param bufnr integer --- @param name string function M.buf_rename(bufnr, name) - vim.api.nvim_buf_set_name(bufnr, name) - delete_alt(bufnr) + vim.api.nvim_buf_set_name(bufnr, name) + delete_alt(bufnr) end --- @param bufnr integer @@ -75,112 +71,112 @@ end --- @param end_row integer --- @param lines string[] function M.set_lines(bufnr, start_row, end_row, lines) - if vim.bo[bufnr].fileformat == 'dos' then - for i = 1, #lines do - lines[i] = lines[i]:gsub('\r$', '') - end - end - vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines) + if vim.bo[bufnr].fileformat == 'dos' then + for i = 1, #lines do + lines[i] = lines[i]:gsub('\r$', '') + end + end + vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines) end --- @return string function M.tmpname() - if is_unix then - return os.tmpname() - end - return vim.fn.tempname() + if is_unix then + return os.tmpname() + end + return vim.fn.tempname() end --- @param timestamp number --- @return string function M.get_relative_time(timestamp) - local current_timestamp = os.time() - local elapsed = current_timestamp - timestamp + local current_timestamp = os.time() + local elapsed = current_timestamp - timestamp - if elapsed == 0 then - return 'a while ago' - end + if elapsed == 0 then + return 'a while ago' + end - local minute_seconds = 60 - local hour_seconds = minute_seconds * 60 - local day_seconds = hour_seconds * 24 - local month_seconds = day_seconds * 30 - local year_seconds = month_seconds * 12 + local minute_seconds = 60 + local hour_seconds = minute_seconds * 60 + local day_seconds = hour_seconds * 24 + local month_seconds = day_seconds * 30 + local year_seconds = month_seconds * 12 - local to_relative_string = function(time, divisor, time_word) - local num = math.floor(time / divisor) - if num > 1 then - time_word = time_word .. 's' - end + local to_relative_string = function(time, divisor, time_word) + local num = math.floor(time / divisor) + if num > 1 then + time_word = time_word .. 's' + end - return num .. ' ' .. time_word .. ' ago' - end + return num .. ' ' .. time_word .. ' ago' + end - if elapsed < minute_seconds then - return to_relative_string(elapsed, 1, 'second') - elseif elapsed < hour_seconds then - return to_relative_string(elapsed, minute_seconds, 'minute') - elseif elapsed < day_seconds then - return to_relative_string(elapsed, hour_seconds, 'hour') - elseif elapsed < month_seconds then - return to_relative_string(elapsed, day_seconds, 'day') - elseif elapsed < year_seconds then - return to_relative_string(elapsed, month_seconds, 'month') - else - return to_relative_string(elapsed, year_seconds, 'year') - end + if elapsed < minute_seconds then + return to_relative_string(elapsed, 1, 'second') + elseif elapsed < hour_seconds then + return to_relative_string(elapsed, minute_seconds, 'minute') + elseif elapsed < day_seconds then + return to_relative_string(elapsed, hour_seconds, 'hour') + elseif elapsed < month_seconds then + return to_relative_string(elapsed, day_seconds, 'day') + elseif elapsed < year_seconds then + return to_relative_string(elapsed, month_seconds, 'month') + else + return to_relative_string(elapsed, year_seconds, 'year') + end end --- @generic T --- @param x T[] --- @return T[] function M.copy_array(x) - local r = {} - for i, e in ipairs(x) do - r[i] = e - end - return r + local r = {} + for i, e in ipairs(x) do + r[i] = e + end + return r end --- Strip '\r' from the EOL of each line only if all lines end with '\r' --- @param xs0 string[] --- @return string[] function M.strip_cr(xs0) - for i = 1, #xs0 do - if xs0[i]:sub(-1) ~= '\r' then - -- don't strip, return early - return xs0 - end - end - -- all lines end with '\r', need to strip - local xs = vim.deepcopy(xs0) - for i = 1, #xs do - xs[i] = xs[i]:sub(1, -2) - end - return xs + for i = 1, #xs0 do + if xs0[i]:sub(-1) ~= '\r' then + -- don't strip, return early + return xs0 + end + end + -- all lines end with '\r', need to strip + local xs = vim.deepcopy(xs0) + for i = 1, #xs do + xs[i] = xs[i]:sub(1, -2) + end + return xs end function M.calc_base(base) - if base and base:sub(1, 1):match('[~\\^]') then - base = 'HEAD' .. base - end - return base + if base and base:sub(1, 1):match('[~\\^]') then + base = 'HEAD' .. base + end + return base end function M.emptytable() - return setmetatable({}, { - __index = function(t, k) - t[k] = {} - return t[k] - end, - }) + return setmetatable({}, { + __index = function(t, k) + t[k] = {} + return t[k] + end, + }) end local function expand_date(fmt, time) - if fmt == '%R' then - return M.get_relative_time(time) - end - return os.date(fmt, time) + if fmt == '%R' then + return M.get_relative_time(time) + end + return os.date(fmt, time) end ---@param fmt string @@ -188,36 +184,36 @@ end ---@param reltime boolean Use relative time as the default date format ---@return string function M.expand_format(fmt, info, reltime) - local ret = {} --- @type string[] + local ret = {} --- @type string[] - for _ = 1, 20 do -- loop protection - -- Capture or - local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)') - if not match then - break + for _ = 1, 20 do -- loop protection + -- Capture or + local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)') + if not match then + break + end + + ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1) + + local v = info[key] + + if v then + if type(v) == 'table' then + v = table.concat(v, '\n') end - - ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1) - - local v = info[key] - - if v then - if type(v) == "table" then - v = table.concat(v, '\n') - end - if vim.endswith(key, '_time') then - if time_fmt == '' then - time_fmt = reltime and '%R' or '%Y-%m-%d' - end - v = expand_date(time_fmt, v) - end - match = tostring(v) + if vim.endswith(key, '_time') then + if time_fmt == '' then + time_fmt = reltime and '%R' or '%Y-%m-%d' + end + v = expand_date(time_fmt, v) end - ret[#ret + 1] = match - end + match = tostring(v) + end + ret[#ret + 1] = match + end - ret[#ret + 1] = fmt - return table.concat(ret, '') + ret[#ret + 1] = fmt + return table.concat(ret, '') end return M diff --git a/lua/gitsigns/uv.lua b/lua/gitsigns/uv.lua index d813639..363b5e9 100644 --- a/lua/gitsigns/uv.lua +++ b/lua/gitsigns/uv.lua @@ -2,67 +2,65 @@ local uv = vim.loop local M = {} - - --- @type table local handles = {} M.handles = handles function M.print_handles() - local none = true - for _, e in pairs(handles) do - local handle, longlived, tr = e[1], e[2], e[3] - if handle and not longlived and not handle:is_closing() then - print('') - print(tr) - none = false - end - end - if none then - print('No active handles') - end + local none = true + for _, e in pairs(handles) do + local handle, longlived, tr = e[1], e[2], e[3] + if handle and not longlived and not handle:is_closing() then + print('') + print(tr) + none = false + end + end + if none then + print('No active handles') + end end vim.api.nvim_create_autocmd('VimLeavePre', { - callback = function() - for _, e in pairs(handles) do - local handle = e[1] - if handle and not handle:is_closing() then - handle:close() - end + callback = function() + for _, e in pairs(handles) do + local handle = e[1] + if handle and not handle:is_closing() then + handle:close() end - end, + end + end, }) --- @param longlived boolean --- @return uv_timer_t? function M.new_timer(longlived) - local r = uv.new_timer() - if r then - table.insert(handles, { r, longlived, debug.traceback() }) - end - return r + local r = uv.new_timer() + if r then + table.insert(handles, { r, longlived, debug.traceback() }) + end + return r end --- @param longlived boolean --- @return uv_fs_poll_t? function M.new_fs_poll(longlived) - local r = uv.new_fs_poll() - if r then - table.insert(handles, { r, longlived, debug.traceback() }) - end - return r + local r = uv.new_fs_poll() + if r then + table.insert(handles, { r, longlived, debug.traceback() }) + end + return r end --- @param ipc boolean --- @return uv_pipe_t? function M.new_pipe(ipc) - local r = uv.new_pipe(ipc) - if r then - table.insert(handles, { r, false, debug.traceback() }) - end - return r + local r = uv.new_pipe(ipc) + if r then + table.insert(handles, { r, false, debug.traceback() }) + end + return r end --- @param cmd string @@ -70,11 +68,11 @@ end --- @param on_exit fun(_: integer, _, integer): uv_process_t, integer --- @return uv_process_t?, string|integer function M.spawn(cmd, opts, on_exit) - local handle, pid = uv.spawn(cmd, opts, on_exit) - if handle then - table.insert(handles, { handle, false, cmd .. ' ' .. vim.inspect(opts) }) - end - return handle, pid + local handle, pid = uv.spawn(cmd, opts, on_exit) + if handle then + table.insert(handles, { handle, false, cmd .. ' ' .. vim.inspect(opts) }) + end + return handle, pid end return M diff --git a/teal/gitsigns.tl b/teal/gitsigns.tl deleted file mode 100644 index 29fabeb..0000000 --- a/teal/gitsigns.tl +++ /dev/null @@ -1,206 +0,0 @@ -local void = require('gitsigns.async').void -local scheduler = require('gitsigns.async').scheduler - -local gs_config = require('gitsigns.config') -local Config = gs_config.Config -local config = gs_config.config - -local log = require('gitsigns.debug.log') -local dprintf = log.dprintf -local dprint = log.dprint - -local api = vim.api -local uv = require('gitsigns.uv') - -local record M - setup: function(cfg: Config) - - -- from attach.tl - attach: function(cbuf: integer, ctx: table, trigger: string) - - _setup_done: boolean -end - -local cwd_watcher: vim.loop.FSPollObj - -local update_cwd_head = void(function() - local paths = vim.fs.find('.git', { - limit = 1, - upward = true, - type = 'directory' - }) - - if #paths == 0 then - return - end - - 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 - - local gs_cache = require('gitsigns.cache') - - -- 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 - end - - local git = require('gitsigns.git') - - 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) - -local function setup_cli() - api.nvim_create_user_command('Gitsigns', function(params: api.UserCmdParams) - require'gitsigns.cli'.run(params) - end, { - force = true, - nargs = '*', - range = true, - complete = function(arglead: string, line: string): {string} - return require'gitsigns.cli'.complete(arglead, line) - end}) -end - -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 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 - - 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 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. ---- ---- Attributes: ~ ---- {async} ---- ---- Parameters: ~ ---- {cfg} Table object containing configuration for ---- Gitsigns. See |gitsigns-usage| for more details. -M.setup = void(function(cfg: Config) - gs_config.build(cfg) - - if vim.fn.executable('git') == 0 then - 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 - - setup_debug() - setup_cli() - - api.nvim_create_augroup('gitsigns', {}) - - if config._test_mode then - require'gitsigns.attach'._setup() - require'gitsigns.git'._set_version(config._git_version) - end - - setup_attach() - setup_cwd_head() - - M._setup_done = true -end) - -return setmetatable(M, { - __index = function(_, f: string): any - 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/actions.tl b/teal/gitsigns/actions.tl deleted file mode 100644 index 7cb1f33..0000000 --- a/teal/gitsigns/actions.tl +++ /dev/null @@ -1,1316 +0,0 @@ -local void = require('gitsigns.async').void -local scheduler = require('gitsigns.async').scheduler - -local config = require('gitsigns.config').config -local mk_repeatable = require('gitsigns.repeat').mk_repeatable -local popup = require('gitsigns.popup') -local util = require('gitsigns.util') -local manager = require('gitsigns.manager') -local git = require('gitsigns.git') -local run_diff = require('gitsigns.diff') - -local gs_cache = require('gitsigns.cache') -local cache = gs_cache.cache -local CacheEntry = gs_cache.CacheEntry - -local gs_hunks = require('gitsigns.hunks') -local Hunk = gs_hunks.Hunk -local Hunk_Public = gs_hunks.Hunk_Public - -local api = vim.api -local current_buf = api.nvim_get_current_buf - -local record DiffthisOpts - vertical: boolean - split: string -end - -local record NavHunkOpts - forwards: boolean - wrap: boolean - navigation_message: boolean - foldopen: boolean - preview: boolean - greedy: boolean -end - -local record BlameOpts - full: boolean - ignore_whitespace: boolean -end - -local record StageHunkOpts - greedy: boolean -end - -local record ResetHunkOpts - greedy: boolean -end - -local record M - stage_hunk : function({integer, integer}, StageHunkOpts) - undo_stage_hunk : function() - reset_hunk : function({integer, integer}, ResetHunkOpts) - - stage_buffer : function() - reset_buffer : function() - reset_buffer_index : function() - - next_hunk : function(NavHunkOpts) - prev_hunk : function(NavHunkOpts) - preview_hunk : function() - preview_hunk_inline: function() - select_hunk : function() - get_hunks : function(bufnr: integer): {Hunk_Public} - - blame_line : function(BlameOpts) - change_base : function(base: string, global: boolean) - reset_base : function(global: boolean) - diffthis : function(base: string, opts: DiffthisOpts) - show : function(base: string) - - test: function(base: string, opts: {string:any}) - - record QFListOpts - use_location_list: boolean - nr: integer - open: boolean - end - - setqflist : function(target:integer|string, opts: QFListOpts, callback: function) - setloclist : function(nr: integer, target:integer|string) - - get_actions : function(bufnr: integer, lnum: integer) - - refresh : function() - toggle_signs : function(boolean): boolean - toggle_numhl : function(boolean): boolean - toggle_linehl : function(boolean): boolean - toggle_word_diff : function(boolean): boolean - toggle_current_line_blame : function(boolean): boolean - toggle_deleted : function(boolean): boolean - - arg_spec: {string:{integer,boolean,boolean}} -end - -local type CmdFunc = function(args: table, params: api.UserCmdParams) - --- Variations of functions from M which are used for the Gitsigns command -local C: {string:CmdFunc} = {} - -local type CmpFunc = function(arglead: string): {string} - -local CP: {string:CmpFunc} = {} - -local ns_inline = api.nvim_create_namespace('gitsigns_preview_inline') - -local function complete_heads(arglead: string): {string} - local all = vim.fn.systemlist{'git', 'rev-parse', '--symbolic', '--branches', '--tags', '--remotes'} - return vim.tbl_filter(function(x: string): boolean - return vim.startswith(x, arglead) - end, all) -end - ---- Toggle |gitsigns-config-signbooleancolumn| ---- ---- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. ---- ---- Returns:~ ---- Current value of |gitsigns-config-signcolumn| -M.toggle_signs = function(value: boolean): boolean - if value ~= nil then - config.signcolumn = value - else - config.signcolumn = not config.signcolumn - end - M.refresh() - return config.signcolumn -end - ---- Toggle |gitsigns-config-numhl| ---- ---- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. ---- ---- Returns:~ ---- Current value of |gitsigns-config-numhl| -M.toggle_numhl = function(value: boolean): boolean - if value ~= nil then - config.numhl = value - else - config.numhl = not config.numhl - end - M.refresh() - return config.numhl -end - ---- Toggle |gitsigns-config-linehl| ---- ---- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. ---- ---- Returns:~ ---- Current value of |gitsigns-config-linehl| -M.toggle_linehl = function(value: boolean): boolean - if value ~= nil then - config.linehl = value - else - config.linehl = not config.linehl - end - M.refresh() - return config.linehl -end - ---- Toggle |gitsigns-config-word_diff| ---- ---- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. ---- ---- Returns:~ ---- Current value of |gitsigns-config-word_diff| -M.toggle_word_diff = function(value: boolean): boolean - if value ~= nil then - config.word_diff = value - else - config.word_diff = not config.word_diff - end - -- Don't use refresh() to avoid flicker - api.nvim__buf_redraw_range(0, vim.fn.line('w0') - 1, vim.fn.line('w$')) - return config.word_diff -end - ---- Toggle |gitsigns-config-current_line_blame| ---- ---- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. ---- ---- Returns:~ ---- Current value of |gitsigns-config-current_line_blame| -M.toggle_current_line_blame = function(value: boolean): boolean - if value ~= nil then - config.current_line_blame = value - else - config.current_line_blame = not config.current_line_blame - end - M.refresh() - return config.current_line_blame -end - ---- Toggle |gitsigns-config-show_deleted| ---- ---- Parameters:~ ---- {value} boolean|nil Value to set toggle. If `nil` ---- the toggle value is inverted. ---- ---- Returns:~ ---- Current value of |gitsigns-config-show_deleted| -M.toggle_deleted = function(value: boolean): boolean - if value ~= nil then - config.show_deleted = value - else - config.show_deleted = not config.show_deleted - end - M.refresh() - return config.show_deleted -end - -local function get_cursor_hunk(bufnr: integer, hunks: {Hunk}): Hunk, integer - bufnr = bufnr or current_buf() - - if not hunks then - hunks = {} - vim.list_extend(hunks, cache[bufnr].hunks or {}) - vim.list_extend(hunks, cache[bufnr].hunks_staged or {}) - end - - local lnum = api.nvim_win_get_cursor(0)[1] - return gs_hunks.find_hunk(lnum, hunks) -end - -local function update(bufnr: integer) - manager.update(bufnr) - scheduler() - if vim.wo.diff then - require('gitsigns.diffthis').update(bufnr) - end -end - -local function get_range(params: api.UserCmdParams): {integer, integer} - local range: {integer, integer} - if params.range > 0 then - range = {params.line1, params.line2} - end - return range -end - -local function get_hunks(bufnr: integer, bcache: CacheEntry, greedy: boolean, staged: boolean): {Hunk} - local hunks: {Hunk} - - if greedy then - -- Re-run the diff without linematch - local buftext = util.buf_lines(bufnr) - local text: {string} - if staged then - text = bcache.compare_text_head - else - text = bcache.compare_text - end - if text then - hunks = run_diff(text, buftext, false) - end - scheduler() - else - if staged then - hunks = bcache.hunks_staged - else - hunks = bcache.hunks - end - end - - return hunks -end - -local function get_hunk(bufnr: integer, range: {integer, integer}, greedy: boolean, staged: boolean): Hunk - local bcache = cache[bufnr] - local hunks = get_hunks(bufnr, bcache, greedy, staged) - local hunk: Hunk - if range then - table.sort(range) - local top, bot = range[1], range[2] - hunk = gs_hunks.create_partial_hunk(hunks, top, bot) - hunk.added.lines = api.nvim_buf_get_lines(bufnr, top-1, bot, false) - hunk.removed.lines = vim.list_slice( - bcache.compare_text, - hunk.removed.start, - hunk.removed.start + hunk.removed.count - 1 - ) - else - hunk = get_cursor_hunk(bufnr, hunks) - end - return hunk -end - ---- Stage the hunk at the cursor position, or all lines in the ---- given range. If {range} is provided, all lines in the given ---- range are staged. This supports partial-hunks, meaning if a ---- range only includes a portion of a particular hunk, only the ---- lines within the range will be staged. ---- ---- Attributes: ~ ---- {async} ---- ---- Parameters:~ ---- {range} table|nil List-like table of two integers making ---- up the line range from which you want to stage the hunks. ---- If running via command line, then this is taken from the ---- command modifiers. ---- {opts} table|nil Additional options: ---- • {greedy}: (boolean) ---- Stage all contiguous hunks. Only useful if 'diff_opts' ---- contains `linematch`. Defaults to `true`. ---- -M.stage_hunk = mk_repeatable(void(function(range: {integer, integer}, opts: StageHunkOpts) - opts = opts or {} - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - if not util.path_exists(bcache.file) then - print("Error: Cannot stage lines. Please add the file to the working tree.") - return - end - - local hunk = get_hunk(bufnr, range, opts.greedy ~= false, false) - - local invert = false - if not hunk then - invert = true - hunk = get_hunk(bufnr, range, opts.greedy ~= false, true) - end - - if not hunk then - return - end - - bcache.git_obj:stage_hunks({hunk}, invert) - - table.insert(bcache.staged_diffs, hunk) - - bcache:invalidate() - update(bufnr) -end)) - -C.stage_hunk = function(_: table, params: api.UserCmdParams) - M.stage_hunk(get_range(params)) -end - ---- Reset the lines of the hunk at the cursor position, or all ---- lines in the given range. If {range} is provided, all lines in ---- the given range are reset. This supports partial-hunks, ---- meaning if a range only includes a portion of a particular ---- hunk, only the lines within the range will be reset. ---- ---- Parameters:~ ---- {range} table|nil List-like table of two integers making ---- up the line range from which you want to reset the hunks. ---- If running via command line, then this is taken from the ---- command modifiers. ---- {opts} table|nil Additional options: ---- • {greedy}: (boolean) ---- Stage all contiguous hunks. Only useful if 'diff_opts' ---- contains `linematch`. Defaults to `true`. ---- -M.reset_hunk = mk_repeatable(void(function(range: {integer, integer}, opts: ResetHunkOpts) - opts = opts or {} - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - local hunk = get_hunk(bufnr, range, opts.greedy ~= false, false) - - if not hunk then - return - end - - local lstart, lend: integer, integer - if hunk.type == 'delete' then - lstart = hunk.added.start - lend = hunk.added.start - else - lstart = hunk.added.start - 1 - lend = hunk.added.start - 1 + hunk.added.count - end - util.set_lines(bufnr, lstart, lend, hunk.removed.lines) -end)) - -C.reset_hunk = function(_: table, params: api.UserCmdParams) - M.reset_hunk(get_range(params)) -end - ---- Reset the lines of all hunks in the buffer. -M.reset_buffer = function() - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - util.set_lines(bufnr, 0, -1, bcache.compare_text) -end - ---- Undo the last call of stage_hunk(). ---- ---- Note: only the calls to stage_hunk() performed in the current ---- session can be undone. ---- ---- Attributes: ~ ---- {async} -M.undo_stage_hunk = void(function() - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - local hunk = table.remove(bcache.staged_diffs) - if not hunk then - print("No hunks to undo") - return - end - - bcache.git_obj:stage_hunks({hunk}, true) - bcache:invalidate() - update(bufnr) -end) - ---- Stage all hunks in current buffer. ---- ---- Attributes: ~ ---- {async} -M.stage_buffer = void(function() - local bufnr = current_buf() - - local bcache = cache[bufnr] - if not bcache then - return - end - - -- Only process files with existing hunks - local hunks = bcache.hunks - if #hunks == 0 then - print("No unstaged changes in file to stage") - return - end - - if not util.path_exists(bcache.git_obj.file) then - print("Error: Cannot stage file. Please add it to the working tree.") - return - end - - bcache.git_obj:stage_hunks(hunks) - - for _, hunk in ipairs(hunks) do - table.insert(bcache.staged_diffs, hunk) - end - - bcache:invalidate() - update(bufnr) -end) - ---- Unstage all hunks for current buffer in the index. Note: ---- Unlike |gitsigns.undo_stage_hunk()| this doesn't simply undo ---- stages, this runs an `git reset` on current buffers file. ---- ---- Attributes: ~ ---- {async} -M.reset_buffer_index = void(function() - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - -- `bcache.staged_diffs` won't contain staged changes outside of current - -- neovim session so signs added from this unstage won't be complete They will - -- however be fixed by gitdir watcher and properly updated We should implement - -- some sort of initial population from git diff, after that this function can - -- be improved to check if any staged hunks exists and it can undo changes - -- using git apply line by line instead of resetting whole file - bcache.staged_diffs = {} - - bcache.git_obj:unstage_file() - - bcache:invalidate() - update(bufnr) -end) - -local function process_nav_opts(opts: NavHunkOpts) - -- show navigation message - if opts.navigation_message == nil then - opts.navigation_message = not vim.opt.shortmess:get().S - end - - -- wrap around - if opts.wrap == nil then - opts.wrap = vim.opt.wrapscan:get() - end - - if opts.foldopen == nil then - opts.foldopen = vim.tbl_contains(vim.opt.foldopen:get(), 'search') - end - - if opts.greedy == nil then - opts.greedy = true - end -end - --- Defer function to the next main event -local function defer(fn: function) - if vim.in_fast_event() then - vim.schedule(fn) - else - vim.defer_fn(fn, 1) - end -end - -local function has_preview_inline(bufnr: integer): boolean - return #api.nvim_buf_get_extmarks(bufnr, ns_inline, 0, -1, {limit=1}) > 0 -end - -local nav_hunk = void(function(opts: NavHunkOpts) - process_nav_opts(opts) - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - local hunks: {Hunk} = {} - vim.list_extend(hunks, get_hunks(bufnr, bcache, opts.greedy, false) or {}) - local hunks_head = get_hunks(bufnr, bcache, opts.greedy, true) or {} - vim.list_extend(hunks, gs_hunks.filter_common(hunks_head, bcache.hunks)) - - if not hunks or vim.tbl_isempty(hunks) then - if opts.navigation_message then - api.nvim_echo({{'No hunks', 'WarningMsg'}}, false, {}) - end - return - end - local line = api.nvim_win_get_cursor(0)[1] - - local hunk, index = gs_hunks.find_nearest_hunk(line, hunks, opts.forwards, opts.wrap) - - if hunk == nil then - if opts.navigation_message then - api.nvim_echo({{'No more hunks', 'WarningMsg'}}, false, {}) - end - return - end - - local row = opts.forwards and hunk.added.start or hunk.vend - if row then - -- Handle topdelete - if row == 0 then - row = 1 - end - vim.cmd [[ normal! m' ]] -- add current cursor position to the jump list - api.nvim_win_set_cursor(0, {row, 0}) - if opts.foldopen then - vim.cmd('silent! foldopen!') - end - if opts.preview or popup.is_open('hunk') ~= nil then - -- Use defer so the cursor change can settle, otherwise the popup might - -- appear in the old position - defer(function() - -- Close the popup in case one is open which will cause it to focus the - -- popup - popup.close('hunk') - M.preview_hunk() - end) - elseif has_preview_inline(bufnr) then - defer(M.preview_hunk_inline) - end - - if index ~= nil and opts.navigation_message then - api.nvim_echo({{string.format('Hunk %d of %d', index, #hunks), 'None'}}, false, {}) - end - - end -end) - ---- Jump to the next hunk in the current buffer. If a hunk preview ---- (popup or inline) was previously opened, it will be re-opened ---- at the next hunk. ---- ---- Parameters: ~ ---- {opts} table|nil Configuration table. Keys: ---- • {wrap}: (boolean) ---- Whether to loop around file or not. Defaults ---- to the value 'wrapscan' ---- • {navigation_message}: (boolean) ---- Whether to show navigation messages or not. ---- Looks at 'shortmess' for default behaviour. ---- • {foldopen}: (boolean) ---- Expand folds when navigating to a hunk which is ---- inside a fold. Defaults to `true` if 'foldopen' ---- contains `search`. ---- • {preview}: (boolean) ---- Automatically open preview_hunk() upon navigating ---- to a hunk. ---- • {greedy}: (boolean) ---- Only navigate between non-contiguous hunks. Only useful if ---- 'diff_opts' contains `linematch`. Defaults to `true`. -M.next_hunk = function(opts: NavHunkOpts) - opts = opts or {} - opts.forwards = true - nav_hunk(opts) -end - ---- Jump to the previous hunk in the current buffer. If a hunk preview ---- (popup or inline) was previously opened, it will be re-opened ---- at the previous hunk. ---- ---- Parameters: ~ ---- See |gitsigns.next_hunk()|. -M.prev_hunk = function(opts: NavHunkOpts) - opts = opts or {} - opts.forwards = false - nav_hunk(opts) -end - -local HlMark = popup.HlMark - -local function lines_format(fmt: {{{string,string|{HlMark}}}}, - info: util.FmtInfo): {{{string,string|{HlMark}}}} - - local ret = vim.deepcopy(fmt) - - for _, line in ipairs(ret) do - for _, s in ipairs(line) do - s[1] = util.expand_format(s[1], info) - end - end - - return ret -end - -local function hlmarks_for_hunk(hunk: Hunk, hl: string): {HlMark} - local hls: {HlMark} = {} - - local removed, added = hunk.removed, hunk.added - - if hl then - hls[#hls+1] = { - hl_group = hl, - start_row = 0, - end_row = removed.count + added.count, - } - end - - hls[#hls+1] = { - hl_group = 'GitSignsDeletePreview', - start_row = 0, - end_row = removed.count, - } - - hls[#hls+1] = { - hl_group = 'GitSignsAddPreview', - start_row = removed.count, - end_row = removed.count + added.count, - } - - if config.diff_opts.internal then - local removed_regions, added_regions = - require('gitsigns.diff_int').run_word_diff(removed.lines, added.lines) - for _, region in ipairs(removed_regions) do - hls[#hls+1] = { - hl_group = 'GitSignsDeleteInline', - start_row = region[1] - 1, - start_col = region[3], - end_col = region[4], - } - end - for _, region in ipairs(added_regions) do - hls[#hls+1] = { - hl_group = 'GitSignsAddInline', - start_row = region[1] + removed.count - 1, - start_col = region[3], - end_col = region[4], - } - end - end - - return hls -end - -local function insert_hunk_hlmarks(fmt: popup.LinesSpec, hunk: Hunk) - for _, line in ipairs(fmt) do - for _, s in ipairs(line) do - local hl = s[2] - if s[1] == '' and hl is string then - s[2] = hlmarks_for_hunk(hunk, hl) - end - end - end -end - -local function noautocmd(f: function()): function() - return function() - local ei = vim.o.eventignore - vim.o.eventignore = 'all' - f() - vim.o.eventignore = ei - end -end - ---- Preview the hunk at the cursor position in a floating ---- window. If the preview is already open, calling this ---- will cause the window to get focus. -M.preview_hunk = noautocmd(function() - -- Wrap in noautocmd so vim-repeat continues to work - - if popup.focus_open('hunk') then - return - end - - local bufnr = current_buf() - - local hunk, index = get_cursor_hunk(bufnr) - - if not hunk then - return - end - - local lines_fmt = { - {{'Hunk of ', 'Title'}}, - {{'', 'NormalFloat' }} - } - - insert_hunk_hlmarks(lines_fmt, hunk) - - local lines_spec = lines_format(lines_fmt, { - hunk_no = index, - num_hunks = #cache[bufnr].hunks, - hunk = gs_hunks.patch_lines(hunk, vim.bo[bufnr].fileformat), - }) - - popup.create(lines_spec, config.preview_config, 'hunk') -end) - -local function clear_preview_inline(bufnr: integer) - api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) -end - ---- Preview the hunk at the cursor position inline in the buffer. -M.preview_hunk_inline = function() - local bufnr = current_buf() - - local hunk = get_cursor_hunk(bufnr) - - if not hunk then - return - end - - clear_preview_inline(bufnr) - - local winid: integer - manager.show_added(bufnr, ns_inline, hunk) - if config._inline2 then - if hunk.removed.count > 0 then - winid = manager.show_deleted_in_float(bufnr, ns_inline, hunk) - end - else - manager.show_deleted(bufnr, ns_inline, hunk) - end - - api.nvim_create_autocmd({ 'CursorMoved', 'InsertEnter' }, { - buffer = bufnr, - desc = 'Clear gitsigns inline preview', - callback = function() - if winid then - pcall(api.nvim_win_close, winid, true) - end - clear_preview_inline(bufnr) - end, - once = true - }) - - -- Virtual lines will be hidden if cursor is on the top row, so automatically - -- scroll the viewport. - if api.nvim_win_get_cursor(0)[1] == 1 then - local keys = hunk.removed.count..'' - local cy = api.nvim_replace_termcodes(keys, true, false, true) - api.nvim_feedkeys(cy, 'n', false) - end - -end - ---- Select the hunk under the cursor. -M.select_hunk = function() - local hunk = get_cursor_hunk() - if not hunk then return end - - vim.cmd('normal! '..hunk.added.start..'GV'..hunk.vend..'G') -end - ---- Get hunk array for specified buffer. ---- ---- Parameters: ~ ---- {bufnr} integer: Buffer number, if not provided (or 0) ---- will use current buffer. ---- ---- Return: ~ ---- Array of hunk objects. Each hunk object has keys: ---- • `"type"`: String with possible values: "add", "change", ---- "delete" ---- • `"head"`: Header that appears in the unified diff ---- output. ---- • `"lines"`: Line contents of the hunks prefixed with ---- either `"-"` or `"+"`. ---- • `"removed"`: Sub-table with fields: ---- • `"start"`: Line number (1-based) ---- • `"count"`: Line count ---- • `"added"`: Sub-table with fields: ---- • `"start"`: Line number (1-based) ---- • `"count"`: Line count -M.get_hunks = function(bufnr: integer): {Hunk_Public} - bufnr = bufnr or current_buf() - if not cache[bufnr] then return end - local ret = {} - -- TODO(lewis6991): allow this to accept a greedy option - for _, h in ipairs(cache[bufnr].hunks or {}) do - ret[#ret+1] = { - head = h.head, - lines = gs_hunks.patch_lines(h, vim.bo[bufnr].fileformat), - type = h.type, - added = h.added, - removed = h.removed - } - end - return ret -end - -local function get_blame_hunk(repo: git.Repo, info: git.BlameInfo): Hunk, integer, integer - local a = {} - -- If no previous so sha of blame added the file - if info.previous then - a = repo:get_show_text(info.previous_sha..':'..info.previous_filename) - end - local b = repo:get_show_text(info.sha..':'..info.filename) - local hunks = run_diff(a, b, false) - local hunk, i = gs_hunks.find_hunk(info.orig_lnum, hunks) - return hunk, i, #hunks -end - -local function create_blame_fmt(is_committed: boolean, full: boolean): popup.LinesSpec - if not is_committed then - return { - {{'', 'Label'}}, - } - end - - local header = { - {' ', 'Directory'}, - {' ', 'MoreMsg'}, - {'()', 'Label'}, - {':', 'NormalFloat'} - } - - if full then - return { - header, - {{'', 'NormalFloat'}}, - {{'Hunk of ', 'Title'}, {' ', 'LineNr'}}, - {{'', 'NormalFloat'}} - } - end - - return { - header, - {{'', 'NormalFloat'}} - } -end - ---- Run git blame on the current line and show the results in a ---- floating window. If already open, calling this will cause the ---- window to get focus. ---- ---- Parameters: ~ ---- {opts} (table|nil): ---- Additional options: ---- • {full}: (boolean) ---- Display full commit message with hunk. ---- • {ignore_whitespace}: (boolean) ---- Ignore whitespace when running blame. ---- ---- Attributes: ~ ---- {async} -M.blame_line = void(function(opts: BlameOpts) - if popup.focus_open('blame') then - return - end - - opts = opts or {} - - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then return end - - local loading = vim.defer_fn(function() - popup.create({{{'Loading...', 'Title'}}}, config.preview_config) - end, 1000) - - scheduler() - local buftext = util.buf_lines(bufnr) - local fileformat = vim.bo[bufnr].fileformat - local lnum = api.nvim_win_get_cursor(0)[1] - local result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) - pcall(function() - loading:close() - end) - - local is_committed = result.sha and tonumber('0x'..result.sha) ~= 0 - - local blame_fmt = create_blame_fmt(is_committed, opts.full) - - local info = result as util.FmtInfo - - if is_committed and opts.full then - info.body = bcache.git_obj:command{ 'show', '-s', '--format=%B', result.sha } as string - - local hunk: Hunk - - hunk, info.hunk_no, info.num_hunks = get_blame_hunk(bcache.git_obj.repo, result) - - info.hunk = gs_hunks.patch_lines(hunk, fileformat) - info.hunk_head = hunk.head - insert_hunk_hlmarks(blame_fmt, hunk) - end - - scheduler() - - popup.create(lines_format(blame_fmt, info), config.preview_config, 'blame') -end) - -local function update_buf_base(buf: integer, bcache: CacheEntry, base: string) - bcache.base = base - bcache:invalidate() - update(buf) -end - ---- Change the base revision to diff against. If {base} is not ---- given, then the original base is used. If {global} is given ---- and true, then change the base revision of all buffers, ---- including any new buffers. ---- ---- Attributes: ~ ---- {async} ---- ---- Parameters:~ ---- {base} string|nil The object/revision to diff against. ---- {global} boolean|nil Change the base of all buffers. ---- ---- Examples: > ---- " Change base to 1 commit behind head ---- :lua require('gitsigns').change_base('HEAD~1') ---- ---- " Also works using the Gitsigns command ---- :Gitsigns change_base HEAD~1 ---- ---- " Other variations ---- :Gitsigns change_base ~1 ---- :Gitsigns change_base ~ ---- :Gitsigns change_base ^ ---- ---- " Commits work too ---- :Gitsigns change_base 92eb3dd ---- ---- " Revert to original base ---- :Gitsigns change_base ---- < ---- ---- For a more complete list of ways to specify bases, see ---- |gitsigns-revision|. -M.change_base = void(function(base: string, global: boolean) - base = util.calc_base(base) - - if global then - config.base = base - - for bufnr, bcache in pairs(cache as {integer:CacheEntry}) do - update_buf_base(bufnr, bcache, base) - end - else - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then return end - - update_buf_base(bufnr, bcache, base) - end -end) - -C.change_base = function(args: table, _: api.UserCmdParams) - M.change_base(args[1] as string, (args[2] or args.global) as boolean) -end - -CP.change_base = complete_heads - ---- Reset the base revision to diff against back to the ---- index. ---- ---- Alias for `change_base(nil, {global})` . -M.reset_base = function(global: boolean) - M.change_base(nil, global) -end - -C.reset_base = function(args: table, _: api.UserCmdParams) - M.change_base(nil, (args[1] or args.global) as boolean) -end - ---- Perform a |vimdiff| on the given file with {base} if it is ---- given, or with the currently set base (index by default). ---- ---- If {base} is the index, then the opened buffer is editable and ---- any written changes will update the index accordingly. ---- ---- Parameters: ~ ---- {base} (string|nil): Revision to diff against. Defaults ---- to index. ---- {opts} (table|nil): ---- Additional options: ---- • {vertical}: {boolean}. Split window vertically. Defaults to ---- config.diff_opts.vertical. If running via command line, then ---- this is taken from the command modifiers. ---- • {split}: {string}. One of: 'aboveleft', 'belowright', ---- 'botright', 'rightbelow', 'leftabove', 'topleft'. Defaults to ---- 'aboveleft'. If running via command line, then this is taken ---- from the command modifiers. ---- ---- Examples: > ---- " Diff against the index ---- :Gitsigns diffthis ---- ---- " Diff against the last commit ---- :Gitsigns diffthis ~1 ---- < ---- ---- For a more complete list of ways to specify bases, see ---- |gitsigns-revision|. ---- ---- Attributes: ~ ---- {async} -M.diffthis = function(base: string, opts: DiffthisOpts) - -- TODO(lewis6991): can't pass numbers as strings from the command line - if base ~= nil then - base = tostring(base) - end - opts = opts or {} - local diffthis = require('gitsigns.diffthis') - if not opts.vertical then - opts.vertical = config.diff_opts.vertical - end - diffthis.diffthis(base, opts as diffthis.DiffthisOpts) -end - -C.diffthis = function(args: table, params: api.UserCmdParams) - -- TODO(lewis6991): validate these - local opts: DiffthisOpts = { - vertical = args.vertical as boolean, - split = args.split as string - } - - if params.smods then - if params.smods.split ~= '' and opts.split == nil then - opts.split = params.smods.split - end - if opts.vertical == nil then - opts.vertical = params.smods.vertical - end - end - - M.diffthis(args[1] as string, opts) -end - -CP.diffthis = complete_heads - --- C.test = function(pos_args: {any}, named_args: {string:any}, params: api.UserCmdParams) --- print('POS ARGS:', vim.inspect(pos_args)) --- print('NAMED ARGS:', vim.inspect(named_args)) --- print('PARAMS:', vim.inspect(params)) --- end - ---- Show revision {base} of the current file, if it is given, or ---- with the currently set base (index by default). ---- ---- If {base} is the index, then the opened buffer is editable and ---- any written changes will update the index accordingly. ---- ---- Examples: > ---- " View the index version of the file ---- :Gitsigns show ---- ---- " View revision of file in the last commit ---- :Gitsigns show ~1 ---- < ---- ---- For a more complete list of ways to specify bases, see ---- |gitsigns-revision|. ---- ---- Attributes: ~ ---- {async} -M.show = function(revision: string) - local diffthis = require('gitsigns.diffthis') - diffthis.show(revision) -end - -CP.show = complete_heads - -local function hunks_to_qflist(buf_or_filename: number|string, hunks: {Hunk}, qflist: {vim.fn.QFItem}) - for i, hunk in ipairs(hunks) do - qflist[#qflist+1] = { - bufnr = buf_or_filename is number and (buf_or_filename as integer) or nil, - filename = buf_or_filename is string and buf_or_filename or nil, - lnum = hunk.added.start, - text = string.format('Lines %d-%d (%d/%d)', - hunk.added.start, hunk.vend, i, #hunks), - } - end -end - -local function buildqflist(target: integer|string): {vim.fn.QFItem} - target = target or current_buf() - if target == 0 then target = current_buf() end - local qflist: {vim.fn.QFItem} = {} - - if type(target) == 'number' then - local bufnr = target as integer - if not cache[bufnr] then return end - hunks_to_qflist(bufnr, cache[bufnr].hunks, qflist) - elseif target == 'attached' then - for bufnr, bcache in pairs(cache as {integer:CacheEntry}) do - hunks_to_qflist(bufnr, bcache.hunks, qflist) - end - elseif target == 'all' then - local repos: {string:git.Repo} = {} - for _, bcache in pairs(cache as {integer:CacheEntry}) do - local repo = bcache.git_obj.repo - if not repos[repo.gitdir] then - repos[repo.gitdir] = repo - end - end - - local repo = git.Repo.new(vim.loop.cwd()) - if not repos[repo.gitdir] then - repos[repo.gitdir] = repo - end - - for _, r in pairs(repos) do - for _, f in ipairs(r:files_changed()) do - local f_abs = r.toplevel..'/'..f - local stat = vim.loop.fs_stat(f_abs) - if stat and stat.type == 'file' then - local a = r:get_show_text(':0:'..f) - scheduler() - local hunks = run_diff(a, util.file_lines(f_abs)) - hunks_to_qflist(f_abs, hunks, qflist) - end - end - end - - end - return qflist -end - ---- Populate the quickfix list with hunks. Automatically opens the ---- quickfix window. ---- ---- Attributes: ~ ---- {async} ---- ---- Parameters: ~ ---- {target} (integer or string): ---- Specifies which files hunks are collected from. ---- Possible values. ---- • [integer]: The buffer with the matching buffer ---- number. `0` for current buffer (default). ---- • `"attached"`: All attached buffers. ---- • `"all"`: All modified files for each git ---- directory of all attached buffers in addition ---- to the current working directory. ---- {opts} (table|nil): ---- Additional options: ---- • {use_location_list}: (boolean) ---- Populate the location list instead of the ---- quickfix list. Default to `false`. ---- • {nr}: (integer) ---- Window number or ID when using location list. ---- Expand folds when navigating to a hunk which is ---- inside a fold. Defaults to `0`. ---- • {open}: (boolean) ---- Open the quickfix/location list viewer. ---- Defaults to `true`. -M.setqflist = void(function(target: integer|string, opts: M.QFListOpts) - opts = opts or {} - if opts.open == nil then - opts.open = true - end - local qfopts = { - items = buildqflist(target), - title = 'Hunks' - } - scheduler() - if opts.use_location_list then - local nr = opts.nr or 0 - vim.fn.setloclist(nr, {}, ' ', qfopts) - if opts.open then - if config.trouble then - require'trouble'.open("loclist") - else - vim.cmd[[lopen]] - end - end - else - vim.fn.setqflist({}, ' ', qfopts) - if opts.open then - if config.trouble then - require'trouble'.open("quickfix") - else - vim.cmd[[copen]] - end - end - end -end) - ---- Populate the location list with hunks. Automatically opens the ---- location list window. ---- ---- Alias for: `setqflist({target}, { use_location_list = true, nr = {nr} }` ---- ---- Attributes: ~ ---- {async} ---- ---- Parameters: ~ ---- {nr} (integer): Window number or the |window-ID|. ---- `0` for the current window (default). ---- {target} (integer or string): See |gitsigns.setqflist()|. -M.setloclist = function(nr: integer, target: integer|string) - M.setqflist(target, { - nr = nr, - use_location_list = true - }) -end - ---- Get all the available line specific actions for the current ---- buffer at the cursor position. ---- ---- Return: ~ ---- Dictionary of action name to function which when called ---- performs action. -M.get_actions = function(): {string:function} - local bufnr = current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - local hunk = get_cursor_hunk() - - local actions_l: {string} = {} - - local function add_action(action: string) - actions_l[#actions_l+1] = action - end - - if hunk then - add_action('stage_hunk') - add_action('reset_hunk') - add_action('preview_hunk') - add_action('select_hunk') - else - add_action('blame_line') - end - - if not vim.tbl_isempty(bcache.staged_diffs) then - add_action('undo_stage_hunk') - end - - local actions: {string:function} = {} - for _, a in ipairs(actions_l) do - actions[a] = (M as {string:function})[a] - end - - return actions -end - ---- Refresh all buffers. ---- ---- Attributes: ~ ---- {async} -M.refresh = void(function() - manager.reset_signs() - require('gitsigns.highlight').setup_highlights() - require('gitsigns.current_line_blame').setup() - for k, v in pairs(cache as {integer:CacheEntry}) do - v:invalidate() - manager.update(k, v) - end -end) - -function M._get_cmd_func(name: string): CmdFunc - return C[name] -end - -function M._get_cmp_func(name: string): CmpFunc - return CP[name] -end - -return M diff --git a/teal/gitsigns/async.tl b/teal/gitsigns/async.tl deleted file mode 100644 index 2d7cc98..0000000 --- a/teal/gitsigns/async.tl +++ /dev/null @@ -1,204 +0,0 @@ -local record Async - -- Order by highest number of return types - - create: function(T, integer): T - - void: function(F): F - - wrap: function (function(A1, function(R1,R2,R3,R4)), integer): function(A1 ): R1,R2,R3,R4 - wrap: function (function(A1,A2, function(R1,R2) ), integer): function(A1,A2 ): R1,R2 - wrap: function (function(A1,A2,A3,A4, function(R1) ), integer): function(A1,A2,A3,A4 ): R1 - wrap: function(function(A1,A2,A3,A4,A5,function(R1) ), integer): function(A1,A2,A3,A4,A5): R1 - wrap: function (function(A1,A2,A3, function() ), integer): function(A1,A2,A3) - - wait: function (integer, function(A1, function(R1,R2,R3,R4)), A1 ): R1,R2,R3,R4 - wait: function (integer, function(A1,A2, function(R1,R2) ), A1, A2 ): R1,R2 - wait: function (integer, function(A1,A2,A3,A4, function(R1) ), A1, A2, A3, A4 ): R1 - wait: function(integer, function(A1,A2,A3,A4,A5,function(R1) ), A1, A2, A3, A4, A5): R1 - wait: function (integer, function(A1,A2,A3, function() ), A1, A2, A3) - - scheduler: function() -end - -local record M - scheduler: function() -end - -local record Async_T - - -- Handle for an object currently running on the event loop. - -- The coroutine is paused while this is active. - -- Must provide methods cancel() and is_cancelled() - -- - -- Handle gets updated on each call to a wrapped functions, so provide access - -- to it via a proxy - _current: Async_T - - cancel: function(Async_T, function) - is_cancelled: function(Async_T): boolean -end - --- Coroutine.running() was changed between Lua 5.1 and 5.2: --- - 5.1: Returns the running coroutine, or nil when called by the main thread. --- - 5.2: Returns the running coroutine plus a boolean, true when the running --- coroutine is the main one. --- --- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT --- --- We need to handle both. - --- Store all the async threads in a weak table so we don't prevent them from --- being garbage collected -local handles = setmetatable({} as {thread:Async_T}, { __mode = 'k' }) - ---- Returns whether the current execution context is async. -function M.running(): boolean - local current = coroutine.running() - if current and handles[current] then - return true - end -end - --- hack: teal doesn't know table.maxn exists -local function maxn(x: table): integer - return ((table as table).maxn as function)(x) as integer -end - -local function is_Async_T(handle: Async_T): boolean - if handle - and type(handle) == 'table' - and vim.is_callable(handle.cancel) - and vim.is_callable(handle.is_cancelled) then - return true - end -end - --- Analogous to uv.close -function Async_T:cancel(cb: function) - -- Cancel anything running on the event loop - if self._current and not self._current:is_cancelled() then - self._current:cancel(cb) - end -end - -function Async_T.new(co: thread): Async_T - local handle = setmetatable({} as Async_T, { __index = Async_T }) - handles[co] = handle - return handle -end - --- Analogous to uv.is_closing -function Async_T:is_cancelled(): boolean - return self._current and self._current:is_cancelled() -end - -local function run(func: function, callback: function, ...: any): Async_T - local co = coroutine.create(func) - local handle = Async_T.new(co) - - local function step(...: any) - local ret = {coroutine.resume(co, ...)} - local stat = ret[1] as boolean - - if not stat then - local err = ret[2] as string - error(string.format("The coroutine failed with this message: %s\n%s", - err, debug.traceback(co))) - end - - if coroutine.status(co) == 'dead' then - if callback then - callback(unpack(ret, 4, maxn(ret))) - end - return - end - - local _, nargs, fn = unpack(ret) as (any, integer, function(...:any): Async_T) - - assert(type(fn) == 'function', "type error :: expected func") - - local args = {select(4, unpack(ret))} - args[nargs] = step - - local r = fn(unpack(args, 1, nargs)) - if is_Async_T(r) then - handle._current = r - end - end - - step(...) - return handle -end - -function M.wait(argc: integer, func: function, ...): any... - -- Always run the wrapped functions in xpcall and re-raise the error in the - -- coroutine. This makes pcall work as normal. - local function pfunc(...: any) - local args = { ... } - local cb = args[argc] as function - args[argc] = function(...: any) - cb(true, ...) - end - xpcall(func, function(err) - cb(false, err, debug.traceback()) - end, unpack(args, 1, argc)) - end - - local ret = {coroutine.yield(argc, pfunc, ...)} - - local ok = ret[1] - if not ok then - local _, err, traceback = unpack(ret) - error(string.format("Wrapped function failed: %s\n%s", err, traceback)) - end - - return unpack(ret, 2, maxn(ret)) -end - ----Creates an async function with a callback style function. ----@param func function: A callback style function to be converted. The last argument must be the callback. ----@param argc number: The number of arguments of func. Must be included. ----@return function: Returns an async function -function M.wrap(func: function, argc: integer): function - assert(argc) - return function(...): any... - if not M.running() then - return func(...) - end - return M.wait(argc, func, ...) - end -end - ----Use this to create a function which executes in an async context but ----called from a non-async context. Inherently this cannot return anything ----since it is non-blocking ----@param func function -function M.create(func: function, argc: integer): function(...: any): Async_T - argc = argc or 0 - return function(...: any): Async_T - if M.running() then - return func(...) as Async_T - end - local callback = select(argc+1, ...) as function - return run(func, callback, unpack({...}, 1, argc)) - end -end - ----Use this to create a function which executes in an async context but ----called from a non-async context. Inherently this cannot return anything ----since it is non-blocking ----@param func function -function M.void(func: function(...:any)): function(...:any): Async_T - return function(...: any): Async_T - if M.running() then - return func(...) - end - return run(func, nil, ...) - end -end - ----An async function that when called will yield to the Neovim scheduler to be ----able to call the API. -M.scheduler = M.wrap(vim.schedule, 1) as function() - -return M as Async diff --git a/teal/gitsigns/attach.tl b/teal/gitsigns/attach.tl deleted file mode 100644 index 86eb5eb..0000000 --- a/teal/gitsigns/attach.tl +++ /dev/null @@ -1,411 +0,0 @@ -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/cache.tl b/teal/gitsigns/cache.tl deleted file mode 100644 index 5fcfcfc..0000000 --- a/teal/gitsigns/cache.tl +++ /dev/null @@ -1,101 +0,0 @@ -local Hunk = require("gitsigns.hunks").Hunk -local GitObj = require('gitsigns.git').Obj -local config = require('gitsigns.config').config - -local record M - record CacheEntry - file : string - base : string - compare_text : {string} - hunks : {Hunk} - force_next_update: boolean - - compare_text_head : {string} - hunks_staged : {Hunk} - - staged_diffs : {Hunk} - gitdir_watcher : vim.loop.FSPollObj -- Timer object watching the gitdir - git_obj : GitObj - commit : string - - get_compare_rev : function(CacheEntry, base: string): string - get_staged_compare_rev : function(CacheEntry): string - get_rev_bufname : function(CacheEntry, rev: string): string - invalidate : function(CacheEntry) - new : function(CacheEntry): CacheEntry - destroy : function(CacheEntry) - end - - record CacheObj - {CacheEntry} - - destroy: function(CacheObj, bufnr: integer) - end - - cache: CacheObj -end - -local CacheEntry = M.CacheEntry - -function CacheEntry:get_compare_rev(base: string): string - base = base or self.base - if base then - return base - end - - if self.commit then - -- Buffer is a fugitive commit so compare against the parent of the commit - if config._signs_staged_enable then - return self.commit - else - return string.format('%s^', self.commit) - end - end - - local stage = self.git_obj.has_conflicts and 1 or 0 - return string.format(':%d', stage) -end - -function CacheEntry:get_staged_compare_rev(): string - return self.commit and string.format('%s^', self.commit) or 'HEAD' -end - -function CacheEntry:get_rev_bufname(rev: string): string - rev = rev or self:get_compare_rev() - return string.format( - 'gitsigns://%s/%s:%s', - self.git_obj.repo.gitdir, - rev, - self.git_obj.relpath - ) -end - -function CacheEntry:invalidate() - self.compare_text = nil - self.compare_text_head = nil - self.hunks = nil - self.hunks_staged = nil -end - -function CacheEntry.new(o: CacheEntry): CacheEntry - o.staged_diffs = o.staged_diffs or {} - return setmetatable(o, {__index = CacheEntry}) -end - -function CacheEntry:destroy() - local w = self.gitdir_watcher - if w and not w:is_closing() then - w:close() - end -end - -function M.CacheObj:destroy(bufnr: integer) - self[bufnr]:destroy() - self[bufnr] = nil -end - -M.cache = setmetatable({}, { - __index = M.CacheObj, -}) - -return M diff --git a/teal/gitsigns/cli.tl b/teal/gitsigns/cli.tl deleted file mode 100644 index 84e7aef..0000000 --- a/teal/gitsigns/cli.tl +++ /dev/null @@ -1,109 +0,0 @@ -local async = require('gitsigns.async') -local void = require('gitsigns.async').void - -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: --- --- 'false' -> false --- 'nil' -> nil --- '100' -> 100 --- 'HEAD~300' -> 'HEAD~300' -local function parse_to_lua(a: string): any - if tonumber(a) then - return tonumber(a) - elseif a == 'false' or a == 'true' then - return a == 'true' - elseif a == 'nil' then - return nil - end - return a -end - -local record M - run: function(params: vim.api.UserCmdParams) -end - -function M.complete(arglead: string, line: string): {string} - local words = vim.split(line, '%s+') - local n: integer = #words - - local matches: {string} = {} - if n == 2 then - 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 - table.insert(matches, func) - end - end - end - elseif n > 2 then - -- Subcommand completion - local cmp_func = actions._get_cmp_func(words[2]) - if cmp_func then - return cmp_func(arglead) - end - end - return matches -end - -local function print_nonnil(x: any) - if x ~= nil then - print(vim.inspect(x)) - end -end - -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('', '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) - - dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, {newline=' ', indent=''})) - - local cmd_func = actions._get_cmd_func(func) - if cmd_func then - -- Action has a specialised mapping function from command form to lua - -- function - print_nonnil(cmd_func(args, params)) - return - end - - 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 - print_nonnil(f(unpack(pos_args), has_named and named_args or nil)) - return - end - end - - message.error('%s is not a valid function or action', func) -end) - -return M diff --git a/teal/gitsigns/cli/argparse.tl b/teal/gitsigns/cli/argparse.tl deleted file mode 100644 index 85a3787..0000000 --- a/teal/gitsigns/cli/argparse.tl +++ /dev/null @@ -1,115 +0,0 @@ -local record M - parse_args: function(x: string): {string}, {string:string|boolean} -end - -local function is_char(x: string): boolean - return x:match('[^=\'"%s]') ~= nil -end - -local enum ArgState - "in_arg" - "in_ws" - "in_value" - "in_quote" - "in_flag" -end - --- Return positional arguments and named arguments -function M.parse_args(x: string): {string}, {string:string|boolean} - local pos_args, named_args: {string}, {string:string|boolean} = {}, {} - - local state: ArgState = 'in_arg' - local cur_arg = '' - local cur_val = '' - local cur_quote = '' - - local function peek(idx: integer): string - return x:sub(idx+1, idx+1) - end - - local i = 1 - while i <= #x do - local ch = x:sub(i, i) - -- dprintf('L(%d)(%s): cur_arg="%s" ch="%s"', i, state, cur_arg, ch) - - if state == 'in_arg' then - if is_char(ch) then - if ch == '-' and peek(i) == '-' then - state = 'in_flag' - cur_arg = '' - i = i + 1 - else - cur_arg = cur_arg .. ch - end - elseif ch:match('%s') then - pos_args[#pos_args+1] = cur_arg - state = 'in_ws' - elseif ch == '=' then - cur_val = '' - local next_ch = peek(i) - if next_ch == "'" or next_ch == '"' then - cur_quote = next_ch - i = i + 1 - state = 'in_quote' - else - state = 'in_value' - end - end - elseif state == 'in_flag' then - if ch:match('%s') then - named_args[cur_arg] = true - state = 'in_ws' - else - cur_arg = cur_arg .. ch - end - elseif state == 'in_ws' then - if is_char(ch) then - if ch == '-' and peek(i) == '-' then - state = 'in_flag' - cur_arg = '' - i = i + 1 - else - state = 'in_arg' - cur_arg = ch - end - end - elseif state == 'in_value' then - if is_char(ch) then - cur_val = cur_val .. ch - elseif ch:match('%s') then - named_args[cur_arg] = cur_val - cur_arg = '' - state = 'in_ws' - end - elseif state == 'in_quote' then - local next_ch = peek(i) - if ch == "\\" and next_ch == cur_quote then - cur_val = cur_val .. next_ch - i = i + 1 - elseif ch == cur_quote then - named_args[cur_arg] = cur_val - state = 'in_ws' - if next_ch ~= '' and not next_ch:match('%s') then - error('malformed argument: '..next_ch) - end - else - cur_val = cur_val .. ch - end - end - i = i + 1 - end - - if #cur_arg > 0 then - if state == 'in_arg' then - pos_args[#pos_args+1] = cur_arg - elseif state == 'in_flag' then - named_args[cur_arg] = true - elseif state == 'in_value' then - named_args[cur_arg] = cur_val - end - end - - return pos_args, named_args -end - -return M diff --git a/teal/gitsigns/config.tl b/teal/gitsigns/config.tl deleted file mode 100644 index ab13830..0000000 --- a/teal/gitsigns/config.tl +++ /dev/null @@ -1,963 +0,0 @@ -local warn: function(string, ...: any) -do - -- this is included in gen_help.lua so don't error if requires fail - local ok, ret = pcall(require, 'gitsigns.message') - if ok then - warn = ret.warn - end -end - ---- @class Gitsigns.SchemaElem ---- @field type string|string[] ---- @field deep_extend boolean ---- @field default any ---- @field deprecated boolean|{new_field:string,message:string,hard:boolean} ---- @field default_help string ---- @field description string - ---- @class Gitsigns.DiffOpts ---- @field algorithm string ---- @field internal boolean ---- @field indent_heuristic boolean ---- @field vertical boolean ---- @field linematch integer - ---- @class Gitsign.SignConfig ---- @field show_count boolean ---- @field hl string ---- @field text string ---- @field numhl string ---- @field linehl string ---- @field keymaps table - ---- @alias Gitsigns.SignType ---- | 'add' ---- | 'change' ---- | 'delete' ---- | 'topdelete' ---- | 'changedelete' ---- | 'untracked' - ---- @alias Gitsigns.CurrentLineBlameFmtOpts { relative_time: boolean } ---- @alias Gitsigns.CurrentLineBlameFmtFun fun(_: string, _: table, _: Gitsigns.CurrentLineBlameFmtOpts): {[1]:string,[2]:string}[] - ---- @class Gitsigns.CurrentLineBlameOpts ---- @field virt_text boolean ---- @field virt_text_pos 'eol'|'overlay'|'right_align' ---- @field delay integer ---- @field ignore_whitespace boolean ---- @field virt_text_priority integer - ---- @class Gitsigns.Config ---- @field debug_mode boolean ---- @field diff_opts Gitsigns.DiffOpts ---- @field base string ---- @field signs table ---- @field _signs_staged table ---- @field _signs_staged_enable boolean ---- @field count_chars table ---- @field signcolumn boolean ---- @field numhl boolean ---- @field linehl boolean ---- @field show_deleted boolean ---- @field sign_priority integer ---- @field _on_attach_pre fun(bufnr: integer, callback: fun(_: table)) ---- @field on_attach fun(bufnr: integer) ---- @field watch_gitdir { enable: boolean, interval: integer, follow_files: boolean } ---- @field max_file_length integer ---- @field update_debounce integer ---- @field status_formatter fun(_: table): string ---- @field current_line_blame boolean ---- @field current_line_blame_formatter_opts { relative_time: boolean } ---- @field current_line_blame_formatter string|Gitsigns.CurrentLineBlameFmtFun ---- @field current_line_blame_formatter_nc string|Gitsigns.CurrentLineBlameFmtFun ---- @field current_line_blame_opts Gitsigns.CurrentLineBlameOpts ---- @field preview_config table ---- @field attach_to_untracked boolean ---- @field yadm { enable: boolean } ---- @field worktrees {toplevel: string, gitdir: string}[] ---- @field word_diff boolean ---- -- Undocumented ---- @field _refresh_staged_on_update boolean ---- @field _blame_cache boolean ---- @field _threaded_diff boolean ---- @field _inline2 boolean ---- @field _extmark_signs boolean ---- @field _git_version string ---- @field _verbose boolean ---- @field _test_mode boolean - -local record SchemaElem - type: string|{string} - deep_extend: boolean - default: any - - record Deprecated - new_field: string - message: string - hard: boolean - end - - deprecated: Deprecated|boolean - - default_help: string - description: string -end - -local record M - record Config - debug_mode: boolean - - record DiffOpts - algorithm: string - internal: boolean - indent_heuristic: boolean - vertical: boolean - linematch: integer - end - - diff_opts: DiffOpts - base: string - - record SignConfig - show_count: boolean - hl: string - text: string - numhl: string - linehl: string - keymaps: {string:string} - end - - enum SignType - 'add' - 'change' - 'delete' - 'topdelete' - 'changedelete' - 'untracked' - end - - type SignsConfig = {SignType: SignConfig} - - signs: SignsConfig - _signs_staged: SignsConfig - _signs_staged_enable: boolean - - count_chars: {string|integer:string} - signcolumn: boolean - numhl: boolean - linehl: boolean - show_deleted: boolean - sign_priority: integer - keymaps: {string:any} - _on_attach_pre: function(bufnr: integer, callback: function(table)) - on_attach: function(bufnr: integer) - record watch_gitdir - enable: boolean - interval: integer - follow_files: boolean - end - max_file_length: integer - update_debounce: integer - status_formatter: function({string:any}): string - - current_line_blame: boolean - - record current_line_blame_formatter_opts - relative_time: boolean - end - - current_line_blame_formatter: string|function(string, {string:any}, current_line_blame_formatter_opts): {{string,string}} - current_line_blame_formatter_nc: string|function(string, {string:any}, current_line_blame_formatter_opts): {{string,string}} - - record current_line_blame_opts - virt_text: boolean - - enum VirtTextPos - 'eol' - 'overlay' - 'right_align' - end - virt_text_pos: VirtTextPos - - delay: integer - ignore_whitespace: boolean - virt_text_priority: integer - end - - preview_config: {string:any} - attach_to_untracked: boolean - - record yadm - enable: boolean - end - - trouble: boolean - - record Worktree - toplevel: string - gitdir: string - end - worktrees: {Worktree} - - -- Undocumented - word_diff: boolean - _refresh_staged_on_update: boolean - _blame_cache: boolean - _threaded_diff: boolean - _inline2: boolean - _extmark_signs: boolean - - _git_version: string - _verbose: boolean - _test_mode: boolean - end - - schema: {string:SchemaElem} - config: Config -end - ---- @type Gitsigns.Config -M.config = {} - ---- @type table -M.schema = { - signs = { - type = 'table', - deep_extend = true, - default = { - add = {hl = 'GitSignsAdd', text = '┃', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn' }, - change = {hl = 'GitSignsChange', text = '┃', numhl = 'GitSignsChangeNr', linehl = 'GitSignsChangeLn' }, - delete = {hl = 'GitSignsDelete', text = '▁', numhl = 'GitSignsDeleteNr', linehl = 'GitSignsDeleteLn' }, - topdelete = {hl = 'GitSignsTopdelete', text = '▔', numhl = 'GitSignsTopdeleteNr', linehl = 'GitSignsTopdeleteLn' }, - changedelete = {hl = 'GitSignsChangedelete', text = '~', numhl = 'GitSignsChangedeleteNr', linehl = 'GitSignsChangedeleteLn' }, - untracked = {hl = 'GitSignsUntracked', text = '┆', numhl = 'GitSignsUntrackedNr', linehl = 'GitSignsUntrackedLn' }, - }, - default_help = [[{ - add = { text = '┃' }, - change = { text = '┃' }, - delete = { text = '▁' }, - topdelete = { text = '▔' }, - changedelete = { text = '~' }, - untracked = { text = '┆' }, - }]], - description = [[ - Configuration for signs: - • `text` specifies the character to use for the sign. - • `show_count` to enable showing count of hunk, e.g. number of deleted - lines. - - The highlights `GitSigns[kind][type]` is used for each kind of sign. E.g. - 'add' signs uses the highlights: - • `GitSignsAdd` (for normal text signs) - • `GitSignsAddNr` (for signs when `config.numhl == true`) - • `GitSignsAddLn `(for signs when `config.linehl == true`) - - See |gitsigns-highlight-groups|. - ]] - }, - - _signs_staged = { - type = 'table', - deep_extend = true, - default = { - add = {hl = 'GitSignsStagedAdd' , text = '┃', numhl='GitSignsStagedAddNr' , linehl='GitSignsStagedAddLn' }, - change = {hl = 'GitSignsStagedChange' , text = '┃', numhl='GitSignsStagedChangeNr' , linehl='GitSignsStagedChangeLn' }, - delete = {hl = 'GitSignsStagedDelete' , text = '▁', numhl='GitSignsStagedDeleteNr' , linehl='GitSignsStagedDeleteLn' }, - topdelete = {hl = 'GitSignsStagedTopdelete' , text = '▔', numhl='GitSignsStagedTopdeleteNr' , linehl='GitSignsStagedTopdeleteLn' }, - changedelete = {hl = 'GitSignsStagedChangedelete', text = '~', numhl='GitSignsStagedChangedeleteNr', linehl='GitSignsStagedChangedeleteLn' }, - }, - default_help = [[{ - add = { text = '┃' }, - change = { text = '┃' }, - delete = { text = '▁' }, - topdelete = { text = '▔' }, - changedelete = { text = '~' }, - }]], - description = [[ - Configuration for signs of staged hunks. - - See |gitsigns-config-signs|. - ]] - }, - - _signs_staged_enable = { - type = 'boolean', - default = false, - description = [[ - Show signs for staged hunks. - - When enabled the signs defined in |git-config-signs_staged|` are used. - ]] - }, - - keymaps = { - deprecated = { - message = "config.keymaps is now deprecated. Please define mappings in config.on_attach() instead." - }, - type = 'table', - default = {}, - description = [[ - Keymaps to set up when attaching to a buffer. - - Each key in the table defines the mode and key (whitespace delimited) - for the mapping and the value defines what the key maps to. The value - can be a table which can contain keys matching the options defined in - |map-arguments| which are: `expr`, `noremap`, `nowait`, `script`, `silent` - and `unique`. These options can also be used in the top level of the - table to define default options for all mappings. - - Since this field is not extended (unlike |gitsigns-config-signs|), - mappings defined in this field can be disabled by setting the whole field - to `{}`, and |gitsigns-config-on_attach| can instead be used to define - mappings. - ]] - }, - - worktrees = { - type = 'table', - default = nil, - description = [[ - Detached working trees. - - Array of tables with the keys `gitdir` and `toplevel`. - - If normal attaching fails, then each entry in the table is attempted - with the work tree details set. - - Example: > - worktrees = { - { - toplevel = vim.env.HOME, - gitdir = vim.env.HOME .. '/projects/dotfiles/.git' - } - } - ]] - }, - - _on_attach_pre = { - type = 'function', - default = nil, - description = [[ - Asynchronous hook called before attaching to a buffer. Mainly used to - configure detached worktrees. - - This callback must call its callback argument. The callback argument can - accept an optional table argument with the keys: 'gitdir' and 'toplevel'. - - Example: > - on_attach_pre = function(bufnr, callback) - ... - callback { - gitdir = ..., - toplevel = ... - } - end -< - ]] - }, - - on_attach = { - type = 'function', - default = nil, - description = [[ - Callback called when attaching to a buffer. Mainly used to setup keymaps - when `config.keymaps` is empty. The buffer number is passed as the first - argument. - - This callback can return `false` to prevent attaching to the buffer. - - Example: > - on_attach = function(bufnr) - if vim.api.nvim_buf_get_name(bufnr):match() then - -- Don't attach to specific buffers whose name matches a pattern - return false - end - - -- Setup keymaps - vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', 'lua require"gitsigns".stage_hunk()', {}) - ... -- More keymaps - end -< - ]] - }, - - watch_gitdir = { - type = 'table', - deep_extend = true, - default = { - enable = true, - interval = 1000, - follow_files = true - }, - description = [[ - When opening a file, a libuv watcher is placed on the respective - `.git` directory to detect when changes happen to use as a trigger to - update signs. - - Fields: ~ - • `enable`: - Whether the watcher is enabled. - - • `interval`: - Interval the watcher waits between polls of the gitdir in milliseconds. - - • `follow_files`: - If a file is moved with `git mv`, switch the buffer to the new location. - ]] - }, - - sign_priority = { - type = 'number', - default = 6, - description = [[ - Priority to use for signs. - ]] - }, - - signcolumn = { - type = 'boolean', - default = true, - description = [[ - Enable/disable symbols in the sign column. - - When enabled the highlights defined in `signs.*.hl` and symbols defined - in `signs.*.text` are used. - ]] - }, - - numhl = { - type = 'boolean', - default = false, - description = [[ - Enable/disable line number highlights. - - When enabled the highlights defined in `signs.*.numhl` are used. If - the highlight group does not exist, then it is automatically defined - and linked to the corresponding highlight group in `signs.*.hl`. - ]] - }, - - linehl = { - type = 'boolean', - default = false, - description = [[ - Enable/disable line highlights. - - When enabled the highlights defined in `signs.*.linehl` are used. If - the highlight group does not exist, then it is automatically defined - and linked to the corresponding highlight group in `signs.*.hl`. - ]] - }, - - show_deleted = { - type = 'boolean', - default = false, - description = [[ - Show the old version of hunks inline in the buffer (via virtual lines). - - Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`. - ]] - }, - - diff_opts = { - type = 'table', - deep_extend = true, - default = function(): {string:any} - local r: M.Config.DiffOpts = { - algorithm = 'myers', - internal = false, - indent_heuristic = false, - vertical = true, - linematch = nil - } - for _, o in ipairs(vim.opt.diffopt:get()) do - if o == 'indent-heuristic' then - r.indent_heuristic = true - elseif o == 'internal' then - if vim.diff then - r.internal = true - end - elseif o == 'horizontal' then - r.vertical = false - elseif vim.startswith(o, 'algorithm:') then - r.algorithm = string.sub(o, ('algorithm:'):len() + 1) - elseif vim.startswith(o, 'linematch:') then - r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1)) as integer - end - end - return r - end, - default_help = "derived from 'diffopt'", - description = [[ - Diff options. - - Fields: ~ - • algorithm: string - Diff algorithm to use. Values: - • "myers" the default algorithm - • "minimal" spend extra time to generate the - smallest possible diff - • "patience" patience diff algorithm - • "histogram" histogram diff algorithm - • internal: boolean - Use Neovim's built in xdiff library for running diffs. - • indent_heuristic: boolean - Use the indent heuristic for the internal - diff library. - • vertical: boolean - Start diff mode with vertical splits. - • linematch: integer - Enable second-stage diff on hunks to align lines. - Requires `internal=true`. - ]] - }, - - base = { - type = 'string', - default = nil, - default_help = 'index', - description = [[ - The object/revision to diff against. - See |gitsigns-revision|. - ]] - }, - - count_chars = { - type = 'table', - default = { - [1] = '1', -- '₁', - [2] = '2', -- '₂', - [3] = '3', -- '₃', - [4] = '4', -- '₄', - [5] = '5', -- '₅', - [6] = '6', -- '₆', - [7] = '7', -- '₇', - [8] = '8', -- '₈', - [9] = '9', -- '₉', - ['+'] = '>', -- '₊', - }, - description = [[ - The count characters used when `signs.*.show_count` is enabled. The - `+` entry is used as a fallback. With the default, any count outside - of 1-9 uses the `>` character in the sign. - - Possible use cases for this field: - • to specify unicode characters for the counts instead of 1-9. - • to define characters to be used for counts greater than 9. - ]] - }, - - status_formatter = { - type = 'function', - default = function(status: {string:number}): string - local added, changed, removed = status.added, status.changed, status.removed - local status_txt = {} - if added and added > 0 then table.insert(status_txt, '+'..added ) end - if changed and changed > 0 then table.insert(status_txt, '~'..changed) end - if removed and removed > 0 then table.insert(status_txt, '-'..removed) end - return table.concat(status_txt, ' ') - end, - default_help = [[function(status) - local added, changed, removed = status.added, status.changed, status.removed - local status_txt = {} - if added and added > 0 then table.insert(status_txt, '+'..added ) end - if changed and changed > 0 then table.insert(status_txt, '~'..changed) end - if removed and removed > 0 then table.insert(status_txt, '-'..removed) end - return table.concat(status_txt, ' ') - end]], - description = [[ - Function used to format `b:gitsigns_status`. - ]] - }, - - max_file_length = { - type = 'number', - default = 40000, - description = [[ - Max file length (in lines) to attach to. - ]] - }, - - preview_config = { - type = 'table', - deep_extend = true, - default = { - border = 'single', - style = 'minimal', - relative = 'cursor', - row = 0, - col = 1 - }, - description = [[ - Option overrides for the Gitsigns preview window. Table is passed directly - to `nvim_open_win`. - ]] - }, - - attach_to_untracked = { - type = 'boolean', - default = true, - description = [[ - Attach to untracked files. - ]] - }, - - update_debounce = { - type = 'number', - default = 100, - description = [[ - Debounce time for updates (in milliseconds). - ]] - }, - - current_line_blame = { - type = 'boolean', - default = false, - description = [[ - Adds an unobtrusive and customisable blame annotation at the end of - the current line. - - The highlight group used for the text is `GitSignsCurrentLineBlame`. - ]] - }, - - current_line_blame_opts = { - type = 'table', - deep_extend = true, - default = { - virt_text = true, - virt_text_pos = 'eol', - virt_text_priority = 100, - delay = 1000 - }, - description = [[ - Options for the current line blame annotation. - - Fields: ~ - • virt_text: boolean - Whether to show a virtual text blame annotation. - • virt_text_pos: string - Blame annotation position. Available values: - `eol` Right after eol character. - `overlay` Display over the specified column, without - shifting the underlying text. - `right_align` Display right aligned in the window. - • delay: integer - Sets the delay (in milliseconds) before blame virtual text is - displayed. - • ignore_whitespace: boolean - Ignore whitespace when running blame. - • virt_text_priority: integer - Priority of virtual text. - ]] - }, - - current_line_blame_formatter_opts = { - type = 'table', - deep_extend = true, - deprecated = true, - default = { - relative_time = false - }, - description = [[ - Options for the current line blame annotation formatter. - - Fields: ~ - • relative_time: boolean - ]] - }, - - current_line_blame_formatter = { - type = {'string', 'function'}, - default = ' , - ', - description = [[ - String or function used to format the virtual text of - |gitsigns-config-current_line_blame|. - - When a string, accepts the following format specifiers: - - • `` - • `` - • `` - • `` - • `` - • `` or `` - • `` - • `` - • `` - • `` or `` - • `` - • `` - • `` - • `` - - For `` and ``, `FORMAT` can - be any valid date format that is accepted by `os.date()` with the - addition of `%R` (defaults to `%Y-%m-%d`): - - • `%a` abbreviated weekday name (e.g., Wed) - • `%A` full weekday name (e.g., Wednesday) - • `%b` abbreviated month name (e.g., Sep) - • `%B` full month name (e.g., September) - • `%c` date and time (e.g., 09/16/98 23:48:10) - • `%d` day of the month (16) [01-31] - • `%H` hour, using a 24-hour clock (23) [00-23] - • `%I` hour, using a 12-hour clock (11) [01-12] - • `%M` minute (48) [00-59] - • `%m` month (09) [01-12] - • `%p` either "am" or "pm" (pm) - • `%S` second (10) [00-61] - • `%w` weekday (3) [0-6 = Sunday-Saturday] - • `%x` date (e.g., 09/16/98) - • `%X` time (e.g., 23:48:10) - • `%Y` full year (1998) - • `%y` two-digit year (98) [00-99] - • `%%` the character `%´ - • `%R` relative (e.g., 4 months ago) - - When a function: - Parameters: ~ - {name} Git user name returned from `git config user.name` . - {blame_info} Table with the following keys: - • `abbrev_sha`: string - • `orig_lnum`: integer - • `final_lnum`: integer - • `author`: string - • `author_mail`: string - • `author_time`: integer - • `author_tz`: string - • `committer`: string - • `committer_mail`: string - • `committer_time`: integer - • `committer_tz`: string - • `summary`: string - • `previous`: string - • `filename`: string - - Note that the keys map onto the output of: - `git blame --line-porcelain` - - {opts} Passed directly from - |gitsigns-config-current_line_blame_formatter_opts|. - - Return: ~ - The result of this function is passed directly to the `opts.virt_text` - field of |nvim_buf_set_extmark| and thus must be a list of - [text, highlight] tuples. - ]] - }, - - current_line_blame_formatter_nc = { - type = {'string', 'function'}, - default = ' ', - description = [[ - String or function used to format the virtual text of - |gitsigns-config-current_line_blame| for lines that aren't committed. - - See |gitsigns-config-current_line_blame_formatter| for more information. - ]] - }, - - trouble = { - type = 'boolean', - default = function(): boolean - local has_trouble = pcall(require, 'trouble') - return has_trouble - end, - default_help = "true if installed", - description = [[ - When using setqflist() or setloclist(), open Trouble instead of the - quickfix/location list window. - ]] - }, - - yadm = { - type = 'table', - default = { enable = false }, - description = [[ - yadm configuration. - ]] - }, - - _git_version = { - type = 'string', - default = 'auto', - description = [[ - Version of git available. Set to 'auto' to automatically detect. - ]] - }, - - _verbose = { - type = 'boolean', - default = false, - description = [[ - More verbose debug message. Requires debug_mode=true. - ]] - }, - - _test_mode = { - type = 'boolean', - default = false, - }, - - word_diff = { - type = 'boolean', - default = false, - description = [[ - Highlight intra-line word differences in the buffer. - Requires `config.diff_opts.internal = true` . - - Uses the highlights: - • For word diff in previews: - • `GitSignsAddInline` - • `GitSignsChangeInline` - • `GitSignsDeleteInline` - • For word diff in buffer: - • `GitSignsAddLnInline` - • `GitSignsChangeLnInline` - • `GitSignsDeleteLnInline` - • For word diff in virtual lines (e.g. show_deleted): - • `GitSignsAddVirtLnInline` - • `GitSignsChangeVirtLnInline` - • `GitSignsDeleteVirtLnInline` - ]] - }, - - _refresh_staged_on_update = { - type = 'boolean', - default = false, - description = [[ - Always refresh the staged file on each update. Disabling this will cause - the staged file to only be refreshed when an update to the index is - detected. - ]] - }, - - _blame_cache = { - type = 'boolean', - default = true, - description = [[ - Cache blame results for current_line_blame - ]] - }, - - _threaded_diff = { - type = 'boolean', - default = false, - description = [[ - Run diffs on a separate thread - ]] - }, - - _inline2 = { - type = 'boolean', - default = false, - description = [[ - Enable enhanced version of preview_hunk_inline() - ]] - }, - - _extmark_signs = { - type = 'boolean', - default = false, - description = [[ - Use extmarks for placing signs. - ]] - }, - - debug_mode = { - type = 'boolean', - default = false, - description = [[ - Enables debug logging and makes the following functions - available: `dump_cache`, `debug_messages`, `clear_debug`. - ]] - }, - -} - -warn = function(s: string, ...: any) - vim.notify(s:format(...), vim.log.levels.WARN, {title = 'gitsigns'}) -end - ---- @param config Gitsigns.Config -local function validate_config(config: {string:any}) - for k, v in pairs(config) do - local kschema = M.schema[k] - if kschema == nil then - warn("gitsigns: Ignoring invalid configuration field '%s'", k) - elseif kschema.type then - if type(kschema.type) == 'string' then - vim.validate { - [k] = { v, kschema.type } as {any}; - } - end - end - end -end - -local function resolve_default(v: SchemaElem): any - if type(v.default) == 'function' and v.type ~= 'function' then - return (v.default as function)() - else - return v.default - end -end - -local function handle_deprecated(cfg: {string:any}) - for k, v in pairs(M.schema) do - local dep = v.deprecated - if dep and cfg[k] ~= nil then - if dep is SchemaElem.Deprecated then - if dep.new_field then - local opts_key, field = dep.new_field:match('(.*)%.(.*)') - if opts_key and field then - -- Field moved to an options table - local opts = (cfg[opts_key] or {}) as {string:any} - opts[field] = cfg[k] - cfg[opts_key] = opts - else - -- Field renamed - cfg[dep.new_field] = cfg[k] - end - end - - if dep.hard then - if dep.message then - warn(dep.message) - elseif dep.new_field then - warn('%s is now deprecated, please use %s', k, dep.new_field) - else - warn('%s is now deprecated; ignoring', k) - end - end - end - end - end -end - ---- @param user_config Gitsigns.Config -function M.build(user_config: {string:any}) - user_config = user_config or {} - - handle_deprecated(user_config) - - validate_config(user_config) - - local config = M.config as {string:any} - for k, v in pairs(M.schema) do - if user_config[k] ~= nil then - if v.deep_extend then - local d = resolve_default(v) - config[k] = vim.tbl_deep_extend('force', d as table, user_config[k] as table) - else - config[k] = user_config[k] - end - else - config[k] = resolve_default(v) - end - end -end - -return M diff --git a/teal/gitsigns/current_line_blame.tl b/teal/gitsigns/current_line_blame.tl deleted file mode 100644 index 386d001..0000000 --- a/teal/gitsigns/current_line_blame.tl +++ /dev/null @@ -1,214 +0,0 @@ -local a = require('gitsigns.async') -local wrap = a.wrap -local void = a.void -local scheduler = a.scheduler - -local cache = require('gitsigns.cache').cache -local config = require('gitsigns.config').config -local BlameInfo = require('gitsigns.git').BlameInfo -local util = require('gitsigns.util') -local uv = require('gitsigns.uv') - -local api = vim.api - -local current_buf = api.nvim_get_current_buf - -local namespace = api.nvim_create_namespace('gitsigns_blame') - -local timer = uv.new_timer(true) - -local record M - setup: function() -end - -local wait_timer = wrap(vim.loop.timer_start, 4) - -local function set_extmark(bufnr: integer, row: integer, opts: {string:any}) - opts = opts or {} - opts.id = 1 - api.nvim_buf_set_extmark(bufnr, namespace, row-1, 0, opts) -end - -local function get_extmark(bufnr: integer): integer - local pos = api.nvim_buf_get_extmark_by_id(bufnr, namespace, 1, {}) as {integer, integer} - if pos[1] then - return pos[1] + 1 - end - return -end - -local function reset(bufnr: integer) - bufnr = bufnr or current_buf() - if not api.nvim_buf_is_valid(bufnr) then - return - end - api.nvim_buf_del_extmark(bufnr, namespace, 1) - vim.b[bufnr].gitsigns_blame_line_dict = nil -end - --- TODO: expose as config -local max_cache_size = 1000 - -local record BlameCache - record Elem - tick: integer - cache: {integer:BlameInfo} - size: integer - end - contents: {integer:Elem} -end - -BlameCache.contents = {} - -function BlameCache:add(bufnr: integer, lnum: integer, x: BlameInfo) - if not config._blame_cache then return end - local scache = self.contents[bufnr] - if scache.size <= max_cache_size then - scache.cache[lnum] = x - scache.size = scache.size + 1 - end -end - -function BlameCache:get(bufnr: integer, lnum: integer): BlameInfo - if not config._blame_cache then return end - - -- init and invalidate - local tick = vim.b[bufnr].changedtick - if not self.contents[bufnr] or self.contents[bufnr].tick ~= tick then - self.contents[bufnr] = {tick = tick, cache = {}, size = 0} - end - - return self.contents[bufnr].cache[lnum] -end - -local function expand_blame_format(fmt: string, name: string, info: BlameInfo): string - if info.author == name then - info.author = 'You' - end - return util.expand_format(fmt, info, config.current_line_blame_formatter_opts.relative_time) -end - -local function flatten_virt_text(virt_text: {{string, string}}): string - local res = {} - for _, part in ipairs(virt_text) do - res[#res+1] = part[1] - end - return table.concat(res) -end - --- Update function, must be called in async context -local update = void(function() - local bufnr = current_buf() - local lnum = api.nvim_win_get_cursor(0)[1] - - local old_lnum = get_extmark(bufnr) - if old_lnum and lnum == old_lnum and BlameCache:get(bufnr, lnum) then - -- Don't update if on the same line and we already have results - return - end - - if api.nvim_get_mode().mode == 'i' then - reset(bufnr) - return - end - - -- Set an empty extmark to save the line number. - -- This will also clear virt_text. - -- Only do this if there was already an extmark to avoid clearing the intro - -- text. - if get_extmark(bufnr) then - reset(bufnr) - set_extmark(bufnr, lnum) - end - - -- Can't show extmarks on folded lines so skip - if vim.fn.foldclosed(lnum) ~= -1 then - return - end - - local opts = config.current_line_blame_opts - - -- Note because the same timer is re-used, this call has a debouncing effect. - wait_timer(timer, opts.delay, 0) - scheduler() - - local bcache = cache[bufnr] - if not bcache or not bcache.git_obj.object_name then - return - end - - local result = BlameCache:get(bufnr, lnum) - if not result then - local buftext = util.buf_lines(bufnr) - result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) - BlameCache:add(bufnr, lnum, result) - scheduler() - end - - local lnum1 = api.nvim_win_get_cursor(0)[1] - if bufnr == current_buf() and lnum ~= lnum1 then - -- Cursor has moved during events; abort - return - end - - if not api.nvim_buf_is_loaded(bufnr) then - -- Buffer is no longer loaded; abort - return - end - - vim.b[bufnr].gitsigns_blame_line_dict = result - - if result then - local virt_text: {{string, string}} - local clb_formatter = result.author == 'Not Committed Yet' and - config.current_line_blame_formatter_nc or - config.current_line_blame_formatter - if clb_formatter is string then - virt_text = {{ - expand_blame_format(clb_formatter, bcache.git_obj.repo.username, result), - 'GitSignsCurrentLineBlame' - }} - else -- function - virt_text = clb_formatter( - bcache.git_obj.repo.username, - result, - config.current_line_blame_formatter_opts - ) - end - - vim.b[bufnr].gitsigns_blame_line = flatten_virt_text(virt_text) - - if opts.virt_text then - set_extmark(bufnr, lnum, { - virt_text = virt_text, - virt_text_pos = opts.virt_text_pos, - priority = opts.virt_text_priority, - hl_mode = 'combine', - }) - end - end -end) - -M.setup = function() - local group = api.nvim_create_augroup('gitsigns_blame', {}) - - for k, _ in pairs(cache as {integer:any}) do - reset(k) - end - - if config.current_line_blame then - api.nvim_create_autocmd({'FocusGained', 'BufEnter', 'CursorMoved', 'CursorMovedI'}, { - group = group, callback = function() update() end - }) - - api.nvim_create_autocmd({'InsertEnter', 'FocusLost', 'BufLeave'}, { - group = group, callback = function() reset() end - }) - - -- Call via vim.schedule to avoid the debounce timer killing the async - -- coroutine - vim.schedule(update) - end -end - -return M diff --git a/teal/gitsigns/debounce.tl b/teal/gitsigns/debounce.tl deleted file mode 100644 index f7fe2ce..0000000 --- a/teal/gitsigns/debounce.tl +++ /dev/null @@ -1,86 +0,0 @@ -local uv = require('gitsigns.uv') - -local record M - throttle_by_id : function(fn: F, schedule: boolean): F - debounce_trailing : function(ms: number, fn: F): F -end - ---- Debounces a function on the trailing edge. ---- ---- @generic F: function ---- @param ms number Timeout in ms ---- @param fn F Function to debounce ---- @return F Debounced function. -function M.debounce_trailing(ms: number, fn: function): function - local timer = uv.new_timer(true) - return function(...) - local argv = {...} - timer:start(ms, 0, function() - timer:stop() - fn(unpack(argv)) - end) - end -end - - ---- Throttles a function on the leading edge. ---- ---- @generic F: function ---- @param ms number Timeout in ms ---- @param fn F Function to throttle ---- @return F throttled function. -function M.throttle_leading(ms: number, fn: function): function - local timer = uv.new_timer(true) - local running = false - return function(...) - if not running then - timer:start(ms, 0, function() - running = false - timer:stop() - end) - running = true - fn(...) - end - end -end - ---- Throttles a function using the first argument as an ID ---- ---- If function is already running then the function will be scheduled to run ---- again once the running call has finished. ---- ---- fn#1 _/‾\__/‾\_/‾\_____________________________ ---- throttled#1 _/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\/‾‾‾‾‾‾‾‾‾‾\____________ --- ---- fn#2 ______/‾\___________/‾\___________________ ---- throttled#2 ______/‾‾‾‾‾‾‾‾‾‾\__/‾‾‾‾‾‾‾‾‾‾\__________ ---- ---- ---- @generic F: function ---- @param fn F Function to throttle ---- @param schedule boolean ---- @return F throttled function. -function M.throttle_by_id(fn: function, schedule: boolean): function - local scheduled: {any:boolean} = {} --- @type table - local running: {any:boolean} = {} --- @type table - return function(id: any, ...) - if scheduled[id] then - -- If fn is already scheduled, then drop - return - end - if not running[id] or schedule then - scheduled[id] = true - end - if running[id] then - return - end - while scheduled[id] do - scheduled[id] = nil - running[id] = true - fn(id, ...) - running[id] = nil - end - end -end - -return M diff --git a/teal/gitsigns/debug.tl b/teal/gitsigns/debug.tl deleted file mode 100644 index 39434e5..0000000 --- a/teal/gitsigns/debug.tl +++ /dev/null @@ -1,50 +0,0 @@ -local log = require'gitsigns.debug.log' - -local M = {} - ---- @param raw_item any ---- @param path string[] ---- @return any -local function process(raw_item: any, path: {string}): any - if path[#path] == vim.inspect.METATABLE then - return nil - elseif raw_item is function then - return nil - elseif raw_item is table then - local key = path[#path] - if key == 'compare_text' or key == 'compare_text_head' then - local item = raw_item as {string} - return { '...', length=#item, head=item[1] } - elseif not vim.tbl_isempty(raw_item) and key == 'staged_diffs' then - return { '...', length=#vim.tbl_keys(raw_item) } - end - end - return raw_item -end - ---- @return any -function M.dump_cache(): any - -- TODO(lewis6991): hack: use package.loaded to avoid circular deps - local cache = (require('gitsigns.cache') as table).cache - local text = vim.inspect(cache, { process = process }) - vim.api.nvim_echo({{text}}, false, {}) - return cache -end - ---- @param noecho boolean ---- @return string[] -function M.debug_messages(noecho: boolean): {string} - if noecho then - return log.messages - else - for _, m in ipairs(log.messages) do - vim.api.nvim_echo({{m}}, false, {}) - end - end -end - -function M.clear_debug() - log.messages = {} -end - -return M diff --git a/teal/gitsigns/debug/log.tl b/teal/gitsigns/debug/log.tl deleted file mode 100644 index 3557f22..0000000 --- a/teal/gitsigns/debug/log.tl +++ /dev/null @@ -1,109 +0,0 @@ -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/diff.tl b/teal/gitsigns/diff.tl deleted file mode 100644 index 5505279..0000000 --- a/teal/gitsigns/diff.tl +++ /dev/null @@ -1,18 +0,0 @@ -local config = require('gitsigns.config').config -local Hunk = require('gitsigns.hunks').Hunk - -return function(a: {string}, b: {string}, linematch: boolean): {Hunk} - local diff_opts = config.diff_opts - local f: function({string}, {string}, string, boolean, integer): {Hunk} - if diff_opts.internal then - f = require('gitsigns.diff_int').run_diff - else - f = require('gitsigns.diff_ext').run_diff - end - - local linematch0: integer - if linematch ~= false then - linematch0 = diff_opts.linematch - end - return f(a, b, diff_opts.algorithm, diff_opts.indent_heuristic, linematch0) -end diff --git a/teal/gitsigns/diff_ext.tl b/teal/gitsigns/diff_ext.tl deleted file mode 100644 index 59b6f2c..0000000 --- a/teal/gitsigns/diff_ext.tl +++ /dev/null @@ -1,80 +0,0 @@ -local git_diff = require('gitsigns.git').diff - -local gs_hunks = require("gitsigns.hunks") -local Hunk = gs_hunks.Hunk -local util = require('gitsigns.util') -local scheduler = require('gitsigns.async').scheduler - -local record M - -- Async function - run_diff: function({string}, {string}, string, boolean): {Hunk} -end - -local function write_to_file(path: string, text: {string}) - local f, err = io.open(path, 'wb') - if f == nil then - error(err) - end - for _, l in ipairs(text) do - f:write(l) - f:write('\n') - end - f:close() -end - -M.run_diff = function( - text_cmp: {string}, - text_buf: {string}, - diff_algo: string, - indent_heuristic: boolean -): {Hunk} - local results: {Hunk} = {} - - -- tmpname must not be called in a callback - if vim.in_fast_event() then - scheduler() - end - - local file_buf = util.tmpname() - local file_cmp = util.tmpname() - - write_to_file(file_buf, text_buf) - write_to_file(file_cmp, text_cmp) - - -- Taken from gitgutter, diff.vim: - -- - -- If a file has CRLF line endings and git's core.autocrlf is true, the file - -- in git's object store will have LF line endings. Writing it out via - -- git-show will produce a file with LF line endings. - -- - -- If this last file is one of the files passed to git-diff, git-diff will - -- convert its line endings to CRLF before diffing -- which is what we want - -- but also by default outputs a warning on stderr. - -- - -- warning: LF will be replace by CRLF in . - -- The file will have its original line endings in your working directory. - -- - -- We can safely ignore the warning, we turn it off by passing the '-c - -- "core.safecrlf=false"' argument to git-diff. - - local out = git_diff(file_cmp, file_buf, indent_heuristic, diff_algo) - - for _, line in ipairs(out) do - if vim.startswith(line, '@@') then - results[#results+1] = gs_hunks.parse_diff_line(line) - elseif #results > 0 then - local r = results[#results] - if line:sub(1, 1) == '-' then - r.removed.lines[#r.removed.lines+1] = line:sub(2) - elseif line:sub(1, 1) == '+' then - r.added.lines[#r.added.lines+1] = line:sub(2) - end - end - end - - os.remove(file_buf) - os.remove(file_cmp) - return results -end - -return M diff --git a/teal/gitsigns/diff_int.tl b/teal/gitsigns/diff_int.tl deleted file mode 100644 index e81b297..0000000 --- a/teal/gitsigns/diff_int.tl +++ /dev/null @@ -1,149 +0,0 @@ -local create_hunk = require("gitsigns.hunks").create_hunk -local Hunk = require('gitsigns.hunks').Hunk -local config = require('gitsigns.config').config -local async = require('gitsigns.async') - -local record M - run_diff: function({string}, {string}, string, boolean, integer): {Hunk} - run_word_diff: function({string}, {string}): {Region}, {Region} -end - -local type DiffFun = function({string}, {string}, string, boolean, integer): {DiffResult} -local type DiffResult = {integer, integer, integer, integer} - -local run_diff_xdl = function( - fa: {string}, fb: {string}, - algorithm: string, indent_heuristic: boolean, - linematch: integer - ): {DiffResult} - - local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n')..'\n' - local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n')..'\n' - - return vim.diff(a, b, { - result_type = 'indices', - algorithm = algorithm, - indent_heuristic = indent_heuristic, - linematch = linematch - }) -end - -local run_diff_xdl_async = async.wrap(function( - fa: {string}, fb: {string}, - algorithm: string, indent_heuristic: boolean, - linematch: integer, - callback: function({DiffResult}) - ) - - local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n')..'\n' - local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n')..'\n' - - vim.loop.new_work(function( - a0: string, b0: string, - algorithm0: string, indent_heuristic0: boolean, - linematch0: integer - ): string - return vim.mpack.encode(vim.diff(a0, b0, { - result_type = 'indices', - algorithm = algorithm0, - indent_heuristic = indent_heuristic0, - linematch = linematch0 - })) - end, function(r: string) - callback(vim.mpack.decode(r) as {DiffResult}) - end):queue(a, b, algorithm, indent_heuristic, linematch) -end, 6) - -M.run_diff = async.void(function( - fa: {string}, fb: {string}, - diff_algo: string, indent_heuristic: boolean, - linematch: integer -): {Hunk} - local run_diff0: DiffFun - if config._threaded_diff and vim.is_thread then - run_diff0 = run_diff_xdl_async - else - run_diff0 = run_diff_xdl - end - - local results = run_diff0(fa, fb, diff_algo, indent_heuristic, linematch) - - local hunks: {Hunk} = {} - - for _, r in ipairs(results) do - local rs, rc, as, ac = unpack(r) - local hunk = create_hunk(rs, rc, as, ac) - if rc > 0 then - for i = rs, rs+rc-1 do - hunk.removed.lines[#hunk.removed.lines+1] = fa[i] or '' - end - end - if ac > 0 then - for i = as, as+ac-1 do - hunk.added.lines[#hunk.added.lines+1] = fb[i] or '' - end - end - hunks[#hunks+1] = hunk - end - - return hunks -end) - -local type Region = {integer, string, integer, integer} - -local gaps_between_regions = 5 - -local function denoise_hunks(hunks: {Hunk}): {Hunk} - -- Denoise the hunks - local ret = {hunks[1]} - for j = 2, #hunks do - local h, n = ret[#ret], hunks[j] - if not h or not n then break end - if n.added.start - h.added.start - h.added.count < gaps_between_regions then - h.added.count = n.added.start + n.added.count - h.added.start - h.removed.count = n.removed.start + n.removed.count - h.removed.start - - if h.added.count > 0 or h.removed.count > 0 then - h.type = 'change' - end - else - ret[#ret+1] = n - end - end - return ret -end - -function M.run_word_diff(removed: {string}, added: {string}): {Region}, {Region} - local adds: {Region} = {} - local rems: {Region} = {} - - if #removed ~= #added then - return rems, adds - end - - for i = 1, #removed do - -- pair lines by position - local a, b = vim.split(removed[i], ''), vim.split(added[i], '') - - local hunks: {Hunk} = {} - for _, r in ipairs(run_diff_xdl(a, b)) do - local rs, rc, as, ac = unpack(r) - - -- Balance of the unknown offset done in hunk_func - if rc == 0 then rs = rs + 1 end - if ac == 0 then as = as + 1 end - - hunks[#hunks+1] = create_hunk(rs, rc, as, ac) - end - - hunks = denoise_hunks(hunks) - - for _, h in ipairs(hunks) do - adds[#adds+1] = {i, h.type, h.added.start , h.added.start + h.added.count} - rems[#rems+1] = {i, h.type, h.removed.start, h.removed.start + h.removed.count} - end - end - return rems, adds -end - -return M diff --git a/teal/gitsigns/diffthis.tl b/teal/gitsigns/diffthis.tl deleted file mode 100644 index 4b9e567..0000000 --- a/teal/gitsigns/diffthis.tl +++ /dev/null @@ -1,201 +0,0 @@ -local api = vim.api - -local void = require('gitsigns.async').void -local scheduler = require('gitsigns.async').scheduler -local awrap = require('gitsigns.async').wrap - -local gs_cache = require('gitsigns.cache') -local cache = gs_cache.cache -local CacheEntry = gs_cache.CacheEntry - -local util = require('gitsigns.util') -local manager = require('gitsigns.manager') -local message = require('gitsigns.message') - -local throttle_by_id = require('gitsigns.debounce').throttle_by_id - -local input = awrap(vim.ui.input, 2) - -local record M - record DiffthisOpts - vertical: boolean - split: string - end - - diffthis: function(base: string, opts: DiffthisOpts) - show: function(base: string) - update: function(bufnr: integer) -end - -local bufread = void(function(bufnr: integer, dbufnr: integer, base: string, bcache: CacheEntry) - local comp_rev = bcache:get_compare_rev(util.calc_base(base)) - local text: {string} - if util.calc_base(base) == util.calc_base(bcache.base) then - text = bcache.compare_text - else - local err: string - text, err = bcache.git_obj:get_show_text(comp_rev) - if err then - error(err, 2) - end - scheduler() - if vim.bo[bufnr].fileformat == 'dos' then - text = util.strip_cr(text) - end - end - - local modifiable = vim.bo[dbufnr].modifiable - vim.bo[dbufnr].modifiable = true - util.set_lines(dbufnr, 0, -1, text) - - vim.bo[dbufnr].modifiable = modifiable - vim.bo[dbufnr].modified = false - vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype - vim.bo[dbufnr].bufhidden = 'wipe' -end) - -local bufwrite = void(function(bufnr: integer, dbufnr: integer, base: string, bcache: CacheEntry) - local buftext = util.buf_lines(dbufnr) - bcache.git_obj:stage_lines(buftext) - scheduler() - vim.bo[dbufnr].modified = false - -- If diff buffer base matches the bcache base then also update the - -- signs. - if util.calc_base(base) == util.calc_base(bcache.base) then - bcache.compare_text = buftext - manager.update(bufnr, bcache) - end -end) - -local function run(base: string, diffthis: boolean, opts: M.DiffthisOpts) - local bufnr = vim.api.nvim_get_current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - opts = opts or {} - - local comp_rev = bcache:get_compare_rev(util.calc_base(base)) - local bufname = bcache:get_rev_bufname(comp_rev) - - local dbuf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_name(dbuf, bufname) - - local ok, err = pcall(bufread as function, bufnr, dbuf, base, bcache) - if not ok then - message.error(err as string) - scheduler() - vim.cmd'bdelete' - if diffthis then - vim.cmd'diffoff' - end - return - end - - if comp_rev == ':0' then - vim.bo[dbuf].buftype = 'acwrite' - - api.nvim_create_autocmd('BufReadCmd', { - group = 'gitsigns', - buffer = dbuf, - callback = function() - bufread(bufnr, dbuf, base, bcache) - if diffthis then - vim.cmd'diffthis' - end - end - }) - - api.nvim_create_autocmd('BufWriteCmd', { - group = 'gitsigns', - buffer = dbuf, - callback = function() - bufwrite(bufnr, dbuf, base, bcache) - end - }) - else - vim.bo[dbuf].buftype = 'nowrite' - vim.bo[dbuf].modifiable = false - end - - if diffthis then - vim.cmd(table.concat({ - 'keepalt', opts.split or 'aboveleft', - opts.vertical and 'vertical' or '', - 'diffsplit', bufname - }, ' ')) - else - vim.cmd('edit '..bufname) - end -end - -M.diffthis = void(function(base: string, opts: M.DiffthisOpts) - if vim.wo.diff then - return - end - - local bufnr = vim.api.nvim_get_current_buf() - local bcache = cache[bufnr] - if not bcache then - return - end - - local cwin = api.nvim_get_current_win() - if not base and bcache.git_obj.has_conflicts then - run(':2', true, opts) - api.nvim_set_current_win(cwin) - opts.split = 'belowright' - run(':3', true, opts) - else - run(base, true, opts) - end - api.nvim_set_current_win(cwin) -end) - -M.show = void(function(base: string) - run(base, false) -end) - -local function should_reload(bufnr: integer): boolean - if not vim.bo[bufnr].modified then - return true - end - local response: string - while not vim.tbl_contains({'O', 'L'}, response) do - response = input{ - prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:' - } - end - return response == 'L' -end - --- This function needs to be throttled as there is a call to vim.ui.input -M.update = throttle_by_id(void(function(bufnr: integer) - if not vim.wo.diff then - return - end - - local bcache = cache[bufnr] - - -- Note this will be the bufname for the currently set base - -- which are the only ones we want to update - local bufname = bcache:get_rev_bufname() - - for _, w in ipairs(api.nvim_list_wins()) do - if api.nvim_win_is_valid(w) then - local b = api.nvim_win_get_buf(w) - local bname = api.nvim_buf_get_name(b) - if bname == bufname or vim.startswith(bname, 'fugitive://') then - if should_reload(b) then - api.nvim_buf_call(b, function(): nil - vim.cmd('doautocmd BufReadCmd') - vim.cmd('diffthis') - end) - end - end - end - end -end)) - -return M diff --git a/teal/gitsigns/git.tl b/teal/gitsigns/git.tl deleted file mode 100644 index cba0cbc..0000000 --- a/teal/gitsigns/git.tl +++ /dev/null @@ -1,725 +0,0 @@ -local async = require('gitsigns.async') -local scheduler = require('gitsigns.async').scheduler - -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.log').dprint -local eprint = require('gitsigns.debug.log').eprint -local err = require('gitsigns.message').error - -local record GJobSpec - command: string - args: {string} - cwd: string - writer: {string} - - -- local extensions - suppress_stderr: boolean -end - -local record M - record BlameInfo - -- Info in header - sha: string - abbrev_sha: string - orig_lnum: integer - final_lnum: integer - - -- Porcelain fields - author: string - author_mail: string - author_time: integer - author_tz: string - committer: string - committer_mail: string - committer_time: integer - committer_tz: string - summary: string - previous: string - previous_filename: string - previous_sha: string - filename: string - end - - record Version - major: integer - minor: integer - patch: integer - end - version: Version - - record RepoInfo - gitdir: string - toplevel: string - detached: boolean - abbrev_head: string - end - - get_repo_info: function(path: string, cmd: string, gitdir: string, toplevel: string): RepoInfo - - diff: function(file_cmp: string, file_buf: string, indent_heuristic: boolean, diff_algo: string): {string}, string - - record Repo - toplevel : string - gitdir : string - detached : boolean - abbrev_head: string - username : string - - command : function(Repo, {string}, GJobSpec): {string}, string - files_changed : function(Repo): {string} - get_show_text : function(Repo, string, string): {string}, string - update_abbrev_head : function(Repo) - new : function(dir: string, gitdir: string, toplevel: string): Repo - end - - record FileProps - relpath : string - orig_relpath : string -- Use for tracking moved files - object_name : string - mode_bits : string - has_conflicts : boolean - i_crlf : boolean -- Object has crlf - w_crlf : boolean -- Working copy has crlf - end - - record Obj - repo : Repo - file : string - relpath : string - orig_relpath : string -- Use for tracking moved files - object_name : string - mode_bits : string - has_conflicts : boolean - i_crlf : boolean -- Object has crlf - w_crlf : boolean -- Working copy has crlf - encoding : string - - command : function(Obj, {string}, GJobSpec): {string}, string - update_file_info : function(Obj, boolean, silent: boolean): boolean - unstage_file : function(Obj, string, string) - run_blame : function(Obj, {string}, number, boolean): BlameInfo - file_info : function(Obj, string, silent: boolean): FileProps - get_show_text : function(Obj, string): {string}, string - ensure_file_in_index : function(Obj) - stage_hunks : function(Obj, {Hunk}, boolean) - stage_lines : function(Obj, {string}) - has_moved : function(Obj): string - new : function(path: string, enc: string, gitdir: string, toplevel: string): Obj - end - -end - -local in_git_dir = function(file: string): boolean - for _, p in ipairs(vim.split(file, util.path_sep)) do - if p == '.git' then - return true - end - end - return false -end - -local Obj = M.Obj -local Repo = M.Repo - -local function parse_version(version: string): M.Version - assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: '..version) - local ret: M.Version = {} - local parts = vim.split(version, '%.') - ret.major = tonumber(parts[1]) as integer - ret.minor = tonumber(parts[2]) as integer - - if parts[3] == 'GIT' then - ret.patch = 0 - else - ret.patch = tonumber(parts[3]) as integer - end - - return ret -end - --- Usage: check_version{2,3} -local function check_version(version: {number,number,number}): boolean - if not M.version then - return false - end - if M.version.major < version[1] then - return false - end - if version[2] and M.version.minor < version[2] then - return false - end - if version[3] and M.version.patch < version[3] then - return false - end - return true -end - ---- @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 { - '--no-pager', - '--literal-pathspecs', - '-c', 'gc.auto=0', -- Disable auto-packing which emits messages to stderr - unpack(args) - } or args - - if not spec.cwd and not uv.cwd() then - spec.cwd = vim.env.HOME - end - - local _, _, stdout, stderr = async.wait(2, subprocess.run_job, spec as subprocess.JobSpec) - - if not spec.suppress_stderr then - if stderr then - local cmd_str = table.concat({spec.command, unpack(args)}, ' ') - log.eprintf("Received stderr when running command\n'%s':\n%s", cmd_str, stderr) - end - end - - local stdout_lines = vim.split(stdout or '', '\n', true) - - -- If stdout ends with a newline, then remove the final empty string after - -- the split - if stdout_lines[#stdout_lines] == '' then - stdout_lines[#stdout_lines] = nil - end - - if log.verbose then - log.vprintf('%d lines:', #stdout_lines) - for i = 1, math.min(10, #stdout_lines) do - log.vprintf('\t%s', stdout_lines[i]) - end - end - - return stdout_lines, stderr -end, 2) - ---- @async -function M.diff(file_cmp: string, file_buf: string, indent_heuristic: boolean, diff_algo: string): {string}, string - return git_command{ - '-c', 'core.safecrlf=false', - 'diff', - '--color=never', - '--'..(indent_heuristic and '' or 'no-')..'indent-heuristic', - '--diff-algorithm='..diff_algo, - '--patch-with-raw', - '--unified=0', - file_cmp, - file_buf, - } -end - ---- @async -local function process_abbrev_head(gitdir: string, head_str: string, path: string, cmd: string): string - if not gitdir then - return head_str - end - if head_str == 'HEAD' then - local short_sha = git_command({'rev-parse', '--short', 'HEAD'}, { - command = cmd or 'git', - suppress_stderr = true, - cwd = path, - })[1] or '' - if log.debug_mode and short_sha ~= '' then - short_sha = 'HEAD' - end - if util.path_exists(gitdir..'/rebase-merge') - or util.path_exists(gitdir..'/rebase-apply') then - return short_sha..'(rebasing)' - end - return short_sha - end - return head_str -end - -local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1 - ---- @async -local cygpath_convert: function(path: string): string - -if has_cygpath then - cygpath_convert = function(path: string): string - return git_command({ '-aw', path }, { command = 'cygpath' })[1] - end -end - -local function normalize_path(path: string): string - if path and has_cygpath and not uv.fs_stat(path) then - -- If on windows and path isn't recognizable as a file, try passing it - -- through cygpath - path = cygpath_convert(path) - end - return path -end - ---- @async -function M.get_repo_info(path: string, cmd: string, gitdir: string, toplevel: string): M.RepoInfo - -- Does git rev-parse have --absolute-git-dir, added in 2.13: - -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ - local has_abs_gd = check_version{2,13} - local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' - - -- Wait for internal scheduler to settle before running command - -- https://github.com/lewis6991/gitsigns.nvim/pull/215 - scheduler() - - local args = {} - - if gitdir then - vim.list_extend(args, {'--git-dir', gitdir}) - end - - if toplevel then - vim.list_extend(args, {'--work-tree', toplevel}) - end - - vim.list_extend(args, { - 'rev-parse', '--show-toplevel', git_dir_opt, '--abbrev-ref', 'HEAD', - }) - - local results = git_command(args, { - command = cmd or 'git', - suppress_stderr = true, - cwd = toplevel or path, - }) - - local ret: M.RepoInfo = { - toplevel = normalize_path(results[1]), - gitdir = normalize_path(results[2]), - } - ret.abbrev_head = process_abbrev_head(ret.gitdir, results[3], path, cmd) - if ret.gitdir and not has_abs_gd then - ret.gitdir = uv.fs_realpath(ret.gitdir) - end - ret.detached = ret.toplevel and ret.gitdir ~= ret.toplevel..'/.git' - return ret -end - --------------------------------------------------------------------------------- --- Git repo object methods --------------------------------------------------------------------------------- - ---- Run git command the with the objects gitdir and toplevel ---- @async -function Repo:command(args: {string}, spec: GJobSpec): {string}, string - spec = spec or {} - spec.cwd = self.toplevel - - local args1 = { - '--git-dir', self.gitdir, - } - - if self.detached then - vim.list_extend(args1, {'--work-tree', self.toplevel}) - end - - vim.list_extend(args1, args) - - return git_command(args1, spec) -end - ---- @async -function Repo:files_changed(): {string} - local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) - - local ret: {string} = {} - for _, line in ipairs(results) do - if line:sub(1, 2):match('^.M') then - ret[#ret+1] = line:sub(4, -1) - end - end - return ret -end - -local function make_bom(...: integer): string - local r = {} - for i, a in ipairs{...} do - r[i] = string.char(a) - end - return table.concat(r) -end - -local BOM_TABLE: {string:string} = { - ['utf-8'] = make_bom(0xef, 0xbb, 0xbf), - ['utf-16le'] = make_bom(0xff, 0xfe), - ['utf-16'] = make_bom(0xfe, 0xff), - ['utf-16be'] = make_bom(0xfe, 0xff), - ['utf-32le'] = make_bom(0xff, 0xfe, 0x00, 0x00), - ['utf-32'] = make_bom(0xff, 0xfe, 0x00, 0x00), - ['utf-32be'] = make_bom(0x00, 0x00, 0xfe, 0xff), - ['utf-7'] = make_bom(0x2b, 0x2f, 0x76), - ['utf-1'] = make_bom(0xf7, 0x54, 0x4c), -} - -local function strip_bom(x: string, encoding: string): string - local bom = BOM_TABLE[encoding] - if bom and vim.startswith(x, bom) then - return x:sub(bom:len() + 1) - end - return x -end - -local function iconv_supported(encoding: string): boolean - -- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924 - if vim.startswith(encoding, 'utf-16') then - return false - elseif vim.startswith(encoding, 'utf-32') then - return false - end - return true -end - ---- Get version of file in the index, return array lines ---- @async -function Repo:get_show_text(object: string, encoding: string): {string}, string - local stdout, stderr = self:command({'show', object}, {suppress_stderr = true}) - - if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then - stdout[1] = strip_bom(stdout[1], encoding) - if vim.iconv then - for i, l in ipairs(stdout) do - stdout[i] = vim.iconv(l, encoding, 'utf-8') - end - else - scheduler() - for i, l in ipairs(stdout) do - -- vimscript will interpret strings containing NUL as blob type - if vim.fn.type(l) == vim.v.t_string then - stdout[i] = vim.fn.iconv(l, encoding, 'utf-8') - end - end - end - end - - return stdout, stderr -end - ---- @async -function Repo:update_abbrev_head() - self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head -end - ---- @async -function Repo.new(dir: string, gitdir: string, toplevel: string): Repo - local self: Repo = setmetatable({}, {__index = Repo}) - - self.username = git_command({'config', 'user.name'})[1] - local info = M.get_repo_info(dir, nil, gitdir, toplevel) - for k, v in pairs(info as table) do - (self as table)[k] = v - end - - -- Try yadm - 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) - local yadm_info = M.get_repo_info(dir, 'yadm', gitdir, toplevel) - for k, v in pairs(yadm_info as {string:any}) do - (self as table)[k] = v - end - end - end - - return self -end - --------------------------------------------------------------------------------- --- Git object methods --------------------------------------------------------------------------------- - ---- Run git command the with the objects gitdir and toplevel ---- @async -function Obj:command(args: {string}, spec: GJobSpec): {string}, string - return self.repo:command(args, spec) -end - ---- @async -function Obj:update_file_info(update_relpath: boolean, silent: boolean): boolean - local old_object_name = self.object_name - local props = self:file_info(self.file, silent) - - if update_relpath then - self.relpath = props.relpath - end - self.object_name = props.object_name - self.mode_bits = props.mode_bits - self.has_conflicts = props.has_conflicts - self.i_crlf = props.i_crlf - self.w_crlf = props.w_crlf - - return old_object_name ~= self.object_name -end - ---- @async -function Obj:file_info(file: string, silent: boolean): M.FileProps - local results, stderr = self:command({ - '-c', 'core.quotepath=off', - 'ls-files', - '--stage', - '--others', - '--exclude-standard', - '--eol', - file or self.file - }, {suppress_stderr = true}) - - if stderr and not silent then - -- 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 - log.eprint(stderr) - end - end - - local result: M.FileProps = {} - for _, line in ipairs(results) do - local parts = vim.split(line, '\t') - if #parts > 2 then -- tracked file - local eol = vim.split(parts[2], '%s+') - result.i_crlf = eol[1] == 'i/crlf' - result.w_crlf = eol[2] == 'w/crlf' - result.relpath = parts[3] - local attrs = vim.split(parts[1], '%s+') - local stage = tonumber(attrs[3]) - if stage <= 1 then - result.mode_bits = attrs[1] - result.object_name = attrs[2] - else - result.has_conflicts = true - end - else -- untracked file - result.relpath = parts[2] - end - end - return result -end - ---- @async -function Obj:get_show_text(revision: string): {string}, string - if not self.relpath then - return {} - end - - local stdout, stderr = self.repo:get_show_text(revision..':'..self.relpath, self.encoding) - - if not self.i_crlf and self.w_crlf then - -- Add cr - for i = 1, #stdout do - stdout[i] = stdout[i]..'\r' - end - end - - return stdout, stderr -end - ---- @async -Obj.unstage_file = function(self: Obj) - self:command{'reset', self.file } -end - ---- @async -function Obj:run_blame(lines: {string}, lnum: number, ignore_whitespace: boolean): M.BlameInfo - local not_committed = { - author = 'Not Committed Yet', - ['author_mail'] = '', - committer = 'Not Committed Yet', - ['committer_mail'] = '', - } - - if not self.object_name or self.repo.abbrev_head == '' then - -- As we support attaching to untracked files we need to return something if - -- the file isn't isn't tracked in git. - -- If abbrev_head is empty, then assume the repo has no commits - return not_committed - end - - local args = { - 'blame', - '--contents', '-', - '-L', lnum..',+1', - '--line-porcelain', - self.file - } - - if ignore_whitespace then - args[#args+1] = '-w' - end - - local ignore_file = self.repo.toplevel ..'/.git-blame-ignore-revs' - if uv.fs_stat(ignore_file) then - vim.list_extend(args, {'--ignore-revs-file', ignore_file}) - end - - local results = self:command(args, { writer = lines }) - if #results == 0 then - return - end - local header = vim.split(table.remove(results, 1), ' ') - - local ret: {string:any} = {} - ret.sha = header[1] - ret.orig_lnum = tonumber(header[2]) as integer - ret.final_lnum = tonumber(header[3]) as integer - ret.abbrev_sha = string.sub(ret.sha as string, 1, 8) - for _, l in ipairs(results) do - if not startswith(l, '\t') then - local cols = vim.split(l, ' ') - local key = table.remove(cols, 1):gsub('-', '_') - ret[key] = table.concat(cols, ' ') - if key == 'previous' then - ret.previous_sha = cols[1] - ret.previous_filename = cols[2] - end - end - end - - -- New in git 2.41: - -- The output given by "git blame" that attributes a line to contents - -- taken from the file specified by the "--contents" option shows it - -- differently from a line attributed to the working tree file. - if ret.author_mail == '' then - ret = vim.tbl_extend('force', ret, not_committed as table) as {string:any} - end - - return ret as M.BlameInfo -end - ---- @async -local function ensure_file_in_index(obj: Obj) - if obj.object_name and not obj.has_conflicts then - return - end - - if not obj.object_name then - -- If there is no object_name then it is not yet in the index so add it - obj:command{'add', '--intent-to-add', obj.file} - else - -- Update the index with the common ancestor (stage 1) which is what bcache - -- stores - local info = string.format('%s,%s,%s', obj.mode_bits, obj.object_name, obj.relpath) - obj:command{'update-index', '--add', '--cacheinfo', info} - end - - obj:update_file_info() -end - --- Stage 'lines' as the entire contents of the file ---- @async ---- @param lines -function Obj:stage_lines(lines: {string}) - local stdout = self:command({ - 'hash-object', '-w', '--path', self.relpath, '--stdin' - }, { writer = lines }) - - local new_object = stdout[1] - - self:command{ - 'update-index', '--cacheinfo', string.format('%s,%s,%s', self.mode_bits, new_object, self.relpath) - } -end - ---- @async -Obj.stage_hunks = function(self: Obj, hunks: {Hunk}, invert: boolean) - ensure_file_in_index(self) - - local patch = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert) - - if not self.i_crlf and self.w_crlf then - -- Remove cr - for i = 1, #patch do - patch[i] = patch[i]:gsub('\r$', '') - end - end - - self:command({ - 'apply', '--whitespace=nowarn', '--cached', '--unidiff-zero', '-' - }, { - writer = patch - }) -end - ---- @async -function Obj:has_moved(): string - local out = self:command{'diff', '--name-status', '-C', '--cached'} - local orig_relpath = self.orig_relpath or self.relpath - for _, l in ipairs(out) do - local parts = vim.split(l, '%s+') - if #parts == 3 then - local orig, new = parts[2], parts[3] - if orig_relpath == orig then - self.orig_relpath = orig_relpath - self.relpath = new - self.file = self.repo.toplevel..'/'..new - return new - end - end - end -end - ---- @async -function Obj.new(file: string, encoding: string, gitdir: string, toplevel: string): Obj - if in_git_dir(file) then - dprint('In git dir') - return nil - end - local self: Obj = setmetatable({}, {__index = Obj}) - - self.file = file - self.encoding = encoding - self.repo = Repo.new(util.dirname(file), gitdir, toplevel) - - if not self.repo.gitdir then - dprint('Not in git repo') - return nil - end - - -- When passing gitdir and toplevel, suppress stderr when resolving the file - local silent = gitdir ~= nil and toplevel ~= nil - - self:update_file_info(true, silent) - - return self -end - -return M diff --git a/teal/gitsigns/highlight.tl b/teal/gitsigns/highlight.tl deleted file mode 100644 index 7c44d94..0000000 --- a/teal/gitsigns/highlight.tl +++ /dev/null @@ -1,223 +0,0 @@ -local record Hldef - {string} - desc: string - hidden: boolean - fg_factor: number - bg_factor: number -end - -local record M - setup_highlights: function() - hls: {{string:Hldef}} -end - --- Use array of dict so we can iterate deterministically --- Export for docgen -M.hls = { - {GitSignsAdd = { 'GitGutterAdd', 'SignifySignAdd', 'DiffAddedGutter', 'diffAdded', 'DiffAdd', - desc = "Used for the text of 'add' signs." - }}, - - {GitSignsChange = { 'GitGutterChange', 'SignifySignChange', 'DiffModifiedGutter', 'diffChanged', 'DiffChange', - desc = "Used for the text of 'change' signs." - }}, - - {GitSignsDelete = { 'GitGutterDelete', 'SignifySignDelete', 'DiffRemovedGutter' , 'diffRemoved', 'DiffDelete', - desc = "Used for the text of 'delete' signs." - }}, - - {GitSignsChangedelete = { 'GitSignsChange', - desc = "Used for the text of 'changedelete' signs." - }}, - - {GitSignsTopdelete = { 'GitSignsDelete', - desc = "Used for the text of 'topdelete' signs." - }}, - - {GitSignsUntracked = { 'GitSignsAdd', - desc = "Used for the text of 'untracked' signs." - }}, - - {GitSignsAddNr = {'GitGutterAddLineNr', 'GitSignsAdd', - desc = "Used for number column (when `config.numhl == true`) of 'add' signs." - }}, - - {GitSignsChangeNr = {'GitGutterChangeLineNr', 'GitSignsChange', - desc = "Used for number column (when `config.numhl == true`) of 'change' signs." - }}, - - {GitSignsDeleteNr = {'GitGutterDeleteLineNr', 'GitSignsDelete', - desc = "Used for number column (when `config.numhl == true`) of 'delete' signs." - }}, - - {GitSignsChangedeleteNr = {'GitSignsChangeNr', - desc = "Used for number column (when `config.numhl == true`) of 'changedelete' signs." - }}, - - {GitSignsTopdeleteNr = {'GitSignsDeleteNr', - desc = "Used for number column (when `config.numhl == true`) of 'topdelete' signs." - }}, - - {GitSignsUntrackedNr = {'GitSignsAddNr', - desc = "Used for number column (when `config.numhl == true`) of 'untracked' signs." - }}, - - {GitSignsAddLn = {'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd', - desc = "Used for buffer line (when `config.linehl == true`) of 'add' signs." - }}, - - {GitSignsChangeLn = {'GitGutterChangeLine', 'SignifyLineChange', 'DiffChange', - desc = "Used for buffer line (when `config.linehl == true`) of 'change' signs." - }}, - - {GitSignsChangedeleteLn = {'GitSignsChangeLn', - desc = "Used for buffer line (when `config.linehl == true`) of 'changedelete' signs." - }}, - - {GitSignsUntrackedLn = {'GitSignsAddLn', - desc = "Used for buffer line (when `config.linehl == true`) of 'untracked' signs." - }}, - - -- Don't set GitSignsDeleteLn by default - -- {GitSignsDeleteLn = {}}, - - {GitSignsStagedAdd = {'GitSignsAdd' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedChange = {'GitSignsChange' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedDelete = {'GitSignsDelete' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedChangedelete = {'GitSignsChangedelete' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedTopdelete = {'GitSignsTopdelete' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedAddNr = {'GitSignsAddNr' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedChangeNr = {'GitSignsChangeNr' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedDeleteNr = {'GitSignsDeleteNr' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedChangedeleteNr = {'GitSignsChangedeleteNr', fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedTopdeleteNr = {'GitSignsTopdeleteNr' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedAddLn = {'GitSignsAddLn' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedChangeLn = {'GitSignsChangeLn' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedDeleteLn = {'GitSignsDeleteLn' , fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedChangedeleteLn = {'GitSignsChangedeleteLn', fg_factor = 0.5, hidden = true, }}, - {GitSignsStagedTopdeleteLn = {'GitSignsTopdeleteLn' , fg_factor = 0.5, hidden = true, }}, - - {GitSignsAddPreview = {'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd', - desc = "Used for added lines in previews." - }}, - - {GitSignsDeletePreview = {'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete', - desc = "Used for deleted lines in previews." - }}, - - {GitSignsCurrentLineBlame = {'NonText', - desc = "Used for current line blame." - }}, - - {GitSignsAddInline = {'TermCursor', - desc = "Used for added word diff regions in inline previews." - }}, - - {GitSignsDeleteInline = {'TermCursor', - desc = "Used for deleted word diff regions in inline previews." - }}, - - {GitSignsChangeInline = {'TermCursor', - desc = "Used for changed word diff regions in inline previews." - }}, - - {GitSignsAddLnInline = {'GitSignsAddInline', - desc = "Used for added word diff regions when `config.word_diff == true`." - }}, - - {GitSignsChangeLnInline = {'GitSignsChangeInline', - desc = "Used for changed word diff regions when `config.word_diff == true`." - }}, - - {GitSignsDeleteLnInline = {'GitSignsDeleteInline', - desc = "Used for deleted word diff regions when `config.word_diff == true`." - }}, - - -- Currently unused - -- {GitSignsAddLnVirtLn = {'GitSignsAddLn'}}, - -- {GitSignsChangeVirtLn = {'GitSignsChangeLn'}}, - -- {GitSignsAddLnVirtLnInLine = {'GitSignsAddLnInline', }}, - -- {GitSignsChangeVirtLnInLine = {'GitSignsChangeLnInline', }}, - - {GitSignsDeleteVirtLn = {'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete', - desc = "Used for deleted lines shown by inline `preview_hunk_inline()` or `show_deleted()`." - }}, - - {GitSignsDeleteVirtLnInLine = {'GitSignsDeleteLnInline', - desc = "Used for word diff regions in lines shown by inline `preview_hunk_inline()` or `show_deleted()`." - }}, - - {GitSignsVirtLnum = {'GitSignsDeleteVirtLn', - desc = 'Used for line numbers in inline hunks previews.' - }}, - -} - -local function is_hl_set(hl_name: string): boolean - -- TODO: this only works with `set termguicolors` - local exists, hl = pcall(vim.api.nvim_get_hl_by_name, hl_name, true) - local color = hl.foreground or hl.background or hl.reverse - return exists and color ~= nil -end - -local function cmul(x: integer, factor: number): integer - if not x or factor == 1 then - return x - end - - local r = math.floor(x / 2^16) - local x1 = x - (r * 2^16) - local g = math.floor(x1 / 2^8) - local b = math.floor(x1 - (g * 2^8)) - return math.floor(math.floor(r*factor) * 2^16 + math.floor(g*factor) * 2^8 + math.floor(b*factor)) -end - -local function dprintf(fmt: string, ...:any) - require('gitsigns.debug.log').dprintf(fmt, ...) -end - -local function derive(hl: string, hldef: Hldef) - for _, d in ipairs(hldef) do - if is_hl_set(d) then - dprintf('Deriving %s from %s', hl, d) - if hldef.fg_factor or hldef.bg_factor then - hldef.fg_factor = hldef.fg_factor or 1 - hldef.bg_factor = hldef.bg_factor or 1 - local dh = vim.api.nvim_get_hl_by_name(d, true) - vim.api.nvim_set_hl(0, hl, { - default = true, - fg = cmul(dh.foreground, hldef.fg_factor), - bg = cmul(dh.background, hldef.bg_factor), - }) - else - vim.api.nvim_set_hl(0, hl, {default = true, link = d }) - end - return - end - end - if hldef[1] and not hldef.bg_factor and not hldef.fg_factor then - -- No fallback found which is set. Just link to the first fallback - -- if there are no modifiers - dprintf('Deriving %s from %s', hl, hldef[1]) - vim.api.nvim_set_hl(0, hl, {default = true, link = hldef[1] }) - else - dprintf('Could not derive %s', hl) - end -end - --- Setup a GitSign* highlight by deriving it from other potentially present --- highlights. -M.setup_highlights = function() - for _, hlg in ipairs(M.hls) do - for hl, hldef in pairs(hlg) do - if is_hl_set(hl) then - -- Already defined - dprintf('Highlight %s is already defined', hl) - else - derive(hl, hldef) - end - end - end -end - -return M diff --git a/teal/gitsigns/hunks.tl b/teal/gitsigns/hunks.tl deleted file mode 100644 index 23f7e4d..0000000 --- a/teal/gitsigns/hunks.tl +++ /dev/null @@ -1,472 +0,0 @@ -local Sign = require('gitsigns.signs').Sign -local StatusObj = require('gitsigns.status').StatusObj - -local util = require('gitsigns.util') - -local min, max = math.min, math.max - ---- @alias Gitsigns.Hunk.Type ---- | "add" ---- | "change" ---- | "delete" - ---- @class Gitsigns.Hunk.Node ---- @field start integer ---- @field count integer ---- @field lines string[] - ---- @class Gitsigns.Hunk.Hunk ---- @field type Gitsigns.Hunk.Type ---- @field head string ---- @field added Gitsigns.Hunk.Node ---- @field removed Gitsigns.Hunk.Node ---- @field vend integer - ---- @class Gitsigns.Hunk.Hunk_Public ---- @field type Gitsigns.Hunk.Type ---- @field head string ---- @field lines string[] ---- @field added Gitsigns.Hunk.Node ---- @field removed Gitsigns.Hunk.Node - -local record M - enum Type - "add" - "change" - "delete" - end - - record Node - start: integer - count: integer - lines: {string} - end - - -- For internal use - record Hunk - type: Type - head: string - added: Node - removed: Node - vend: integer - end - - record Hunk_Public - type: Type - head: string - lines: {string} - added: Node - removed: Node - end -end - -local Hunk = M.Hunk - ---- @param old_start integer ---- @param old_count integer ---- @param new_start integer ---- @param new_count integer ---- @return Gitsigns.Hunk.Hunk -function M.create_hunk(old_start: integer, old_count: integer, new_start: integer, new_count: integer): Hunk - return { - removed = { start = old_start, count = old_count, lines = {} }, - added = { start = new_start, count = new_count, lines = {} }, - head = ('@@ -%d%s +%d%s @@'):format( - old_start, old_count > 0 and ',' .. old_count or '', - new_start, new_count > 0 and ',' .. new_count or '' - ), - vend = new_start + math.max(new_count - 1, 0), - type = new_count == 0 and 'delete' or - old_count == 0 and 'add' or - 'change' - } -end - ---- @param hunks Gitsigns.Hunk.Hunk[] ---- @param top integer ---- @param bot integer ---- @return Gitsigns.Hunk.Hunk -function M.create_partial_hunk(hunks: {Hunk}, top: integer, bot: integer): Hunk - local pretop, precount = top, bot - top + 1 - for _, h in ipairs(hunks) do - local added_in_hunk = h.added.count - h.removed.count - - local added_in_range = 0 - if h.added.start >= top and h.vend <= bot then - -- Range contains hunk - added_in_range = added_in_hunk - else - local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count)) - local added_above_top = max(0, top - (h.added.start + h.removed.count)) - - if h.added.start >= top and h.added.start <= bot then - -- Range top intersects hunk - added_in_range = added_above_bot - elseif h.vend >= top and h.vend <= bot then - -- Range bottom intersects hunk - added_in_range = added_in_hunk - added_above_top - pretop = pretop - added_above_top - elseif h.added.start <= top and h.vend >= bot then - -- Range within hunk - added_in_range = added_above_bot - added_above_top - pretop = pretop - added_above_top - end - - if top > h.vend then - pretop = pretop - added_in_hunk - end - end - - precount = precount - added_in_range - end - - if precount == 0 then - pretop = pretop - 1 - end - - return M.create_hunk(pretop, precount, top, bot - top + 1) -end - ---- @param hunk Gitsigns.Hunk.Hunk ---- @param fileformat string ---- @return string[] -function M.patch_lines(hunk: Hunk, fileformat: string): {string} - local lines: {string} = {} --- @type string[] - for _, l in ipairs(hunk.removed.lines) do - lines[#lines+1] = '-'..l - end - for _, l in ipairs(hunk.added.lines) do - lines[#lines+1] = '+'..l - end - - if fileformat == 'dos' then - lines = util.strip_cr(lines) - end - return lines -end - ---- @param line string ---- @return Gitsigns.Hunk.Hunk -function M.parse_diff_line(line: string): Hunk - local diffkey = vim.trim(vim.split(line, '@@', true)[2]) - - -- diffKey: "-xx,n +yy" - -- pre: {xx, n}, now: {yy} - local pre, now = unpack(vim.tbl_map(function(s: string): {string} - return vim.split(string.sub(s, 2), ',') - end, vim.split(diffkey, ' ')) as {{string}}) - - local hunk = M.create_hunk( - tonumber(pre[1]) as integer, (tonumber(pre[2]) or 1) as integer, - tonumber(now[1]) as integer, (tonumber(now[2]) or 1) as integer - ) - hunk.head = line - - return hunk -end - ---- @param hunk Gitsigns.Hunk.Hunk ---- @return integer -local function change_end(hunk: Hunk): integer - if hunk.added.count == 0 then - -- delete - return hunk.added.start - elseif hunk.removed.count == 0 then - -- add - return hunk.added.start + hunk.added.count - 1 - else - -- change - return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1 - end -end - ---- Calculate signs needed to be applied from a hunk for a specified line range. ---- @param hunk Gitsigns.Hunk.Hunk ---- @param min_lnum integer ---- @param max_lnum integer ---- @param untracked boolean ---- @return Gitsigns.Sign[] -function M.calc_signs(hunk: Hunk, min_lnum: integer, max_lnum: integer, untracked: boolean): {Sign} - assert(not untracked or hunk.type == 'add') - min_lnum = min_lnum or 1 - max_lnum = max_lnum or math.huge as integer - local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count - - if hunk.type == 'delete' and start == 0 then - if min_lnum <= 1 then - -- topdelete signs get placed one row lower - return {{ type = 'topdelete', count = removed, lnum = 1 }} - else - return {} - end - end - - local signs: {Sign} = {} - - local cend = change_end(hunk) - - for lnum = max(start, min_lnum), min(cend, max_lnum) do - local changedelete = hunk.type == 'change' and removed > added and lnum == cend - - signs[#signs+1] = { - type = changedelete and 'changedelete' or - untracked and 'untracked' or hunk.type, - count = lnum == start and (hunk.type == 'add' and added or removed), - lnum = lnum - } - end - - if hunk.type == "change" and added > removed and - hunk.vend >= min_lnum and cend <= max_lnum then - for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do - signs[#signs+1] = { - type = 'add', - count = lnum == hunk.vend and (added - removed), - lnum = lnum - } - end - end - - return signs -end - ---- @param relpath string ---- @param hunks Gitsigns.Hunk.Hunk[] ---- @param mode_bits string ---- @param invert boolean ---- @return string[] -function M.create_patch(relpath: string, hunks: {Hunk}, mode_bits: string, invert: boolean): {string} - invert = invert or false - - local results = { - string.format('diff --git a/%s b/%s', relpath, relpath), - 'index 000000..000000 '..mode_bits, - '--- a/'..relpath, - '+++ b/'..relpath, - } - - local offset = 0 - - for _, process_hunk in ipairs(hunks) do - local start, pre_count, now_count = - process_hunk.removed.start, process_hunk.removed.count, process_hunk.added.count - - if process_hunk.type == 'add' then - start = start + 1 - end - - local pre_lines = process_hunk.removed.lines - local now_lines = process_hunk.added.lines - - if invert then - pre_count, now_count = now_count, pre_count - pre_lines, now_lines = now_lines, pre_lines - end - - table.insert(results, string.format('@@ -%s,%s +%s,%s @@', start, pre_count, start + offset, now_count)) - for _, l in ipairs(pre_lines) do - results[#results+1] = '-'..l - end - for _, l in ipairs(now_lines) do - results[#results+1] = '+'..l - end - - process_hunk.removed.start = start + offset - offset = offset + (now_count - pre_count) - end - - return results -end - ---- @param hunks Gitsigns.Hunk.Hunk[] ---- @return Gitsigns.StatusObj -function M.get_summary(hunks: {Hunk}): StatusObj - local status = { added = 0, changed = 0, removed = 0 } - - for _, hunk in ipairs(hunks or {}) do - if hunk.type == 'add' then - status.added = status.added + hunk.added.count - elseif hunk.type == 'delete' then - status.removed = status.removed + hunk.removed.count - elseif hunk.type == 'change' then - local add, remove = hunk.added.count, hunk.removed.count - local delta = min(add, remove) - status.changed = status.changed + delta - status.added = status.added + add - delta - status.removed = status.removed + remove - delta - end - end - - return status -end - ---- @param lnum integer ---- @param hunks Gitsigns.Hunk.Hunk[] ---- @return Gitsigns.Hunk.Hunk?, integer? -function M.find_hunk(lnum: integer, hunks: {Hunk}): Hunk, integer - for i, hunk in ipairs(hunks or {}) do - if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then - return hunk, i - end - - if hunk.added.start <= lnum and hunk.vend >= lnum then - return hunk, i - end - end -end - ---- @param lnum integer ---- @param hunks Gitsigns.Hunk.Hunk[] ---- @param forwards boolean ---- @param wrap boolean ---- @return Gitsigns.Hunk.Hunk, integer -function M.find_nearest_hunk(lnum: integer, hunks: {Hunk}, forwards: boolean, wrap: boolean): Hunk, integer - local ret: Hunk --- @type Gitsigns.Hunk.Hunk - local index: integer --- @type integer - local distance: integer = math.huge as integer - if forwards then - for i = 1, #hunks do - local hunk = hunks[i] - local dist = hunk.added.start - lnum - if dist > 0 and dist < distance then - distance = dist - ret = hunk - index = i - end - end - else - for i = #hunks, 1, -1 do - local hunk = hunks[i] - local dist = lnum - hunk.vend - if dist > 0 and dist < distance then - distance = dist - ret = hunk - index = i - end - end - end - if not ret and wrap then - index = forwards and 1 or #hunks - ret = hunks[index as integer] - end - return ret, index -end - ---- @param a Gitsigns.Hunk.Hunk[] ---- @param b Gitsigns.Hunk.Hunk[] ---- @return boolean -function M.compare_heads(a: {Hunk}, b: {Hunk}): boolean - if (a == nil) ~= (b == nil) then - return true - elseif a and #a ~= #b then - return true - end - for i, ah in ipairs(a or {}) do - if b[i].head ~= ah.head then - return true - end - end - return false -end - ---- @param a Gitsigns.Hunk.Hunk ---- @param b Gitsigns.Hunk.Hunk ---- @return boolean -local function compare_new(a: Hunk, b: Hunk): boolean - if a.added.start ~= b.added.start then - return false - end - - if a.added.count ~= b.added.count then - return false - end - - for i = 1, a.added.count do - if a.added.lines[i] ~= b.added.lines[i] then - return false - end - end - - return true -end - ---- Return hunks in a using b's hunks as a filter. Only compare the 'new' section ---- of the hunk. ---- ---- Eg. Given: ---- ---- a = { ---- 1 = '@@ -24 +25,1 @@', ---- 2 = '@@ -32 +34,1 @@', ---- 3 = '@@ -37 +40,1 @@' ---- } ---- ---- b = { ---- 1 = '@@ -26 +25,1 @@' ---- } ---- ---- Since a[1] and b[1] introduce the same changes to the buffer (both have ---- +25,1), we exclude this hunk in the output so we return: ---- ---- { ---- 1 = '@@ -32 +34,1 @@', ---- 2 = '@@ -37 +40,1 @@' ---- } ---- ---- @param a Gitsigns.Hunk.Hunk[] ---- @param b Gitsigns.Hunk.Hunk[] ---- @return Gitsigns.Hunk.Hunk[]? -function M.filter_common(a: {Hunk}, b: {Hunk}): {Hunk} - if not a and not b then - return - end - - a, b = a or {}, b or {} - local max_iter = math.max(#a, #b) - - local a_i = 1 - local b_i = 1 - - --- @type Gitsigns.Hunk.Hunk[] - local ret: {Hunk} = {} - - for _ = 1, max_iter do - local a_h, b_h = a[a_i], b[b_i] - - if not a_h then - -- Reached the end of a - break - end - - if not b_h then - -- Reached the end of b, add remainder of a - for i = a_i, #a do - ret[#ret+1] = a[i] - end - break - end - - if a_h.added.start > b_h.added.start then - -- a pointer is ahead of b; increment b pointer - b_i = b_i + 1 - elseif a_h.added.start < b_h.added.start then - -- b pointer is ahead of a; add a_h to ret and increment a pointer - ret[#ret+1] = a_h - a_i = a_i + 1 - else -- a_h.start == b_h.start - -- a_h and b_h start on the same line, if hunks have the same changes then - -- skip (filtered) otherwise add a_h to ret. Increment both hunk - -- pointers - -- TODO(lewis6991): Be smarter; if bh intercepts then break down ah. - if not compare_new(a_h, b_h) then - ret[#ret+1] = a_h - end - a_i = a_i + 1 - b_i = b_i + 1 - end - end - - return ret -end - -return M diff --git a/teal/gitsigns/manager.tl b/teal/gitsigns/manager.tl deleted file mode 100644 index 3ce4e0d..0000000 --- a/teal/gitsigns/manager.tl +++ /dev/null @@ -1,582 +0,0 @@ -local void = require('gitsigns.async').void -local awrap = require('gitsigns.async').wrap -local scheduler = require('gitsigns.async').scheduler - -local gs_cache = require('gitsigns.cache') -local CacheEntry = gs_cache.CacheEntry -local cache = gs_cache.cache - -local Signs = require('gitsigns.signs') -local Status = require("gitsigns.status") - -local debounce_trailing = require('gitsigns.debounce').debounce_trailing -local throttle_by_id = require('gitsigns.debounce').throttle_by_id - -local log = require('gitsigns.debug.log') -local dprint = log.dprint -local dprintf = log.dprintf -local eprint = log.eprint - -local subprocess = require('gitsigns.subprocess') -local util = require('gitsigns.util') -local run_diff = require('gitsigns.diff') -local uv = require('gitsigns.uv') - -local gs_hunks = require("gitsigns.hunks") -local Hunk = gs_hunks.Hunk - -local config = require('gitsigns.config').config - -local api = vim.api - -local signs_normal: Signs -local signs_staged: Signs - -local record M - update : function(bufnr: integer, CacheEntry) - 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 - detach : function(bufnr: integer, keep_signs: boolean) - reset_signs : function() - setup : function() -end - -local scheduler_if_buf_valid = awrap(function(buf: integer, cb: function) - vim.schedule(function() - if vim.api.nvim_buf_is_valid(buf) then - cb() - end - end) -end, 2) - -local function apply_win_signs0(bufnr: integer, signs: Signs, hunks: {Hunk}, top: integer, bot: integer, clear: boolean, untracked: boolean) - if clear then - signs:remove(bufnr) -- Remove all signs - end - - for i, hunk in ipairs(hunks or {}) do - -- To stop the sign column width changing too much, if there are signs to be - -- added but none of them are visible in the window, then make sure to add at - -- least one sign. Only do this on the first call after an update when we all - -- the signs have been cleared. - if clear and i == 1 then - signs:add(bufnr, gs_hunks.calc_signs(hunk, hunk.added.start, hunk.added.start, untracked)) - end - - if top <= hunk.vend and bot >= hunk.added.start then - signs:add(bufnr, gs_hunks.calc_signs(hunk, top, bot, untracked)) - end - if hunk.added.start > bot then - break - end - end -end - -local function apply_win_signs(bufnr: integer, top: integer, bot: integer, clear: boolean, untracked: boolean) - local bcache = cache[bufnr] - if not bcache then - return - end - apply_win_signs0(bufnr, signs_normal, bcache.hunks, top, bot, clear, untracked) - if signs_staged then - apply_win_signs0(bufnr, signs_staged, bcache.hunks_staged, top, bot, clear, false) - end -end - -M.on_lines = function(buf: integer, first: integer, last_orig: integer, last_new: integer): boolean - local bcache = cache[buf] - if not bcache then - dprint('Cache for buffer was nil. Detaching') - return true - end - - signs_normal:on_lines(buf, first, last_orig, last_new) - if signs_staged then - signs_staged:on_lines(buf, first, last_orig, last_new) - end - - -- Signs in changed regions get invalidated so we need to force a redraw if - -- any signs get removed. - if bcache.hunks and signs_normal:contains(buf, first, last_new) then - -- Force a sign redraw on the next update (fixes #521) - bcache.force_next_update = true - end - - if signs_staged then - if bcache.hunks_staged and signs_staged:contains(buf, first, last_new) then - -- Force a sign redraw on the next update (fixes #521) - bcache.force_next_update = true - end - end - - M.update_debounced(buf, cache[buf]) -end - -local ns = api.nvim_create_namespace('gitsigns') - -local function apply_word_diff(bufnr: integer, row: integer) - -- Don't run on folded lines - if vim.fn.foldclosed(row+1) ~= -1 then - return - end - - local bcache = cache[bufnr] - - if not bcache or not bcache.hunks then - return - end - - local line = api.nvim_buf_get_lines(bufnr, row, row+1, false)[1] - if not line then - -- Invalid line - return - end - - local lnum = row + 1 - - local hunk = gs_hunks.find_hunk(lnum, bcache.hunks) - if not hunk then - -- No hunk at line - return - end - - if hunk.added.count ~= hunk.removed.count then - -- Only word diff if added count == removed - return - end - - local pos = lnum - hunk.added.start + 1 - - local added_line = hunk.added.lines[pos] - local removed_line = hunk.removed.lines[pos] - - local _, added_regions = require('gitsigns.diff_int').run_word_diff({removed_line}, {added_line}) - - local cols = #line - - for _, region in ipairs(added_regions) do - local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1 - if ecol == scol then - -- Make sure region is at least 1 column wide so deletes can be shown - ecol = scol + 1 - end - - local hl_group = rtype == 'add' and 'GitSignsAddLnInline' - or rtype == 'change' and 'GitSignsChangeLnInline' - or 'GitSignsDeleteLnInline' - - local opts: {string:any} = { - ephemeral = true, - priority = 1000 - } - - if ecol > cols and ecol == scol + 1 then - -- delete on last column, use virtual text instead - opts.virt_text = {{' ', hl_group}} - opts.virt_text_pos = 'overlay' - else - opts.end_col = ecol - opts.hl_group = hl_group - end - - api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts) - api.nvim__buf_redraw_range(bufnr, row, row+1) - end -end - -local ns_rm = api.nvim_create_namespace('gitsigns_removed') - -local VIRT_LINE_LEN = 300 - -local function clear_deleted(bufnr: integer) - local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {}) - for _, mark in ipairs(marks as {{integer, integer, integer}}) do - api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1]) - end -end - -function M.show_deleted(bufnr: integer, nsd: integer, hunk: Hunk) - local virt_lines = {} - - for i, line in ipairs(hunk.removed.lines) do - local vline = {} - local last_ecol = 1 - - if config.word_diff then - local regions = require('gitsigns.diff_int').run_word_diff( - {hunk.removed.lines[i]}, {hunk.added.lines[i]}) - - for _, region in ipairs(regions) do - local rline, scol, ecol = region[1], region[3], region[4] - if rline > 1 then - break - end - vline[#vline+1] = { line:sub(last_ecol, scol-1), 'GitSignsDeleteVirtLn'} - vline[#vline+1] = { line:sub(scol, ecol-1), 'GitSignsDeleteVirtLnInline'} - last_ecol = ecol - end - end - - if #line > 0 then - vline[#vline+1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn'} - end - - -- Add extra padding so the entire line is highlighted - local padding = string.rep(' ', VIRT_LINE_LEN-#line) - vline[#vline+1] = { padding, 'GitSignsDeleteVirtLn'} - - virt_lines[i] = vline - end - - local topdelete = hunk.added.start == 0 and hunk.type == 'delete' - - local row = topdelete and 0 or hunk.added.start - 1 - api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { - virt_lines = virt_lines, - -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 - virt_lines_above = hunk.type ~= 'delete' or topdelete, - }) -end - -function M.show_deleted_in_float(bufnr: integer, nsd: integer, hunk: Hunk): integer - local virt_lines = {} - for i = 1, hunk.removed.count do - virt_lines[i] = { { '', 'Normal' } } - end - - local topdelete = hunk.added.start == 0 and hunk.type == 'delete' - local virt_lines_above = hunk.type ~= 'delete' or topdelete - - local row = topdelete and 0 or hunk.added.start - 1 - api.nvim_buf_set_extmark(bufnr, nsd, row, -1, { - virt_lines = virt_lines, - -- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166 - virt_lines_above = virt_lines_above - }) - - local bcache = cache[bufnr] - local pbufnr = api.nvim_create_buf(false, true) - api.nvim_buf_set_lines(pbufnr, 0, -1, false, bcache.compare_text) - - local cwin = api.nvim_get_current_win() - local width = api.nvim_win_get_width(0) - - local opts: vim.api.WinConfig = { - relative = 'win', - win = cwin, - width = width, - height = hunk.removed.count, - anchor = 'SW', - bufpos = { hunk.added.start, 0 } - } - - local bufpos_offset = virt_lines_above and not topdelete and 1 or 0 - opts.bufpos[1] = opts.bufpos[1] - bufpos_offset - - local winid = api.nvim_open_win(pbufnr, false, opts) - - -- Align buffer text by accounting for differences in the statuscolumn - local textoff = vim.fn.getwininfo(cwin)[1].textoff - local ptextoff = vim.fn.getwininfo(winid)[1].textoff - local col_offset = textoff - ptextoff - - if col_offset ~= 0 then - opts.width = opts.width - col_offset - opts.bufpos[2] = opts.bufpos[2] + col_offset - api.nvim_win_set_config(winid, opts) - end - - vim.bo[pbufnr].filetype = vim.bo[bufnr].filetype - vim.bo[pbufnr].bufhidden = 'wipe' - vim.wo[winid].scrolloff = 0 - vim.wo[winid].relativenumber = false - - api.nvim_win_call(winid, function(): nil - -- Expand folds - vim.cmd('normal '..'zR') - - -- Navigate to hunk - vim.cmd('normal '..tostring(hunk.removed.start)..'gg') - vim.cmd('normal '..vim.api.nvim_replace_termcodes('z', true, false, true)) - end) - - -- Apply highlights - - for i = hunk.removed.start, hunk.removed.start + hunk.removed.count do - api.nvim_buf_set_extmark(pbufnr, nsd, i - 1, 0, { - hl_group = 'GitSignsDeleteVirtLn', - hl_eol = true, - end_row = i, - priority = 1000 - }) - end - - local removed_regions = - require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) - - for _, region in ipairs(removed_regions) do - local start_row = (hunk.removed.start - 1) + (region[1] - 1) - local start_col = region[3] - 1 - local end_col = region[4] - 1 - api.nvim_buf_set_extmark(pbufnr, nsd, start_row, start_col, { - hl_group = 'GitSignsDeleteVirtLnInline', - end_col = end_col, - end_row = start_row, - priority = 1001 - }) - end - - return winid -end - -function M.show_added(bufnr: integer, nsw: integer, hunk: Hunk) - local start_row = hunk.added.start - 1 - - for offset = 0, hunk.added.count - 1 do - local row = start_row + offset - api.nvim_buf_set_extmark(bufnr, nsw, row, 0, { - end_row = row + 1, - hl_group = 'GitSignsAddPreview', - hl_eol = true, - priority = 1000 - }) - end - - local _, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines) - - for _, region in ipairs(added_regions) do - local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1 - api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, { - end_col = ecol, - hl_group = rtype == 'add' and 'GitSignsAddInline' - or rtype == 'change' and 'GitSignsChangeInline' - or 'GitSignsDeleteInline', - priority = 1001 - }) - end -end - -local function update_show_deleted(bufnr: integer) - local bcache = cache[bufnr] - - clear_deleted(bufnr) - if config.show_deleted then - for _, hunk in ipairs(bcache.hunks or {}) do - M.show_deleted(bufnr, ns_rm, hunk) - end - end -end - -local update_cnt = 0 - --- Ensure updates cannot be interleaved. --- Since updates are asynchronous we need to make sure an update isn't performed --- whilst another one is in progress. If this happens then schedule another --- update after the current one has completed. -M.update = throttle_by_id(function(bufnr: integer, bcache: CacheEntry) - local __FUNC__ = 'update' - bcache = bcache or cache[bufnr] - if not bcache then - eprint('Cache for buffer '..bufnr..' was nil') - return - end - local old_hunks, old_hunks_staged = bcache.hunks, bcache.hunks_staged - bcache.hunks, bcache.hunks_staged = nil, nil - - scheduler_if_buf_valid(bufnr) - local buftext = util.buf_lines(bufnr) - local git_obj = bcache.git_obj - - if not bcache.compare_text or config._refresh_staged_on_update then - bcache.compare_text = git_obj:get_show_text(bcache:get_compare_rev()) - end - - bcache.hunks = run_diff(bcache.compare_text, buftext) - - if config._signs_staged_enable then - if not bcache.compare_text_head or config._refresh_staged_on_update then - bcache.compare_text_head = git_obj:get_show_text(bcache:get_staged_compare_rev()) - end - local hunks_head = run_diff(bcache.compare_text_head, buftext) - bcache.hunks_staged = gs_hunks.filter_common(hunks_head, bcache.hunks) - end - - scheduler_if_buf_valid(bufnr) - - -- Note the decoration provider may have invalidated bcache.hunks at this - -- point - if bcache.force_next_update or gs_hunks.compare_heads(bcache.hunks, old_hunks) or - gs_hunks.compare_heads(bcache.hunks_staged, old_hunks_staged) then - -- Apply signs to the window. Other signs will be added by the decoration - -- provider as they are drawn. - apply_win_signs(bufnr, vim.fn.line('w0'), vim.fn.line('w$'), true, git_obj.object_name == nil) - - update_show_deleted(bufnr) - bcache.force_next_update = false - - api.nvim_exec_autocmds('User', { - pattern = 'GitSignsUpdate', - modeline = false, - }) - end - - local summary = gs_hunks.get_summary(bcache.hunks) - summary.head = git_obj.repo.abbrev_head - Status:update(bufnr, summary) - - update_cnt = update_cnt + 1 - - dprintf('updates: %s, jobs: %s', update_cnt, subprocess.job_cnt) -end, true) - -M.detach = function(bufnr: integer, keep_signs: boolean) - if not keep_signs then - -- Remove all signs - signs_normal:remove(bufnr) - if signs_staged then - signs_staged:remove(bufnr) - end - end -end - -local function handle_moved(bufnr: integer, bcache: CacheEntry, old_relpath: string) - local git_obj = bcache.git_obj - local do_update = false - - local new_name = git_obj:has_moved() - if new_name then - dprintf('File moved to %s', new_name) - git_obj.relpath = new_name - if not git_obj.orig_relpath then - git_obj.orig_relpath = old_relpath - end - do_update = true - elseif git_obj.orig_relpath then - local orig_file = git_obj.repo.toplevel..util.path_sep..git_obj.orig_relpath - if git_obj:file_info(orig_file).relpath then - dprintf('Moved file reset') - git_obj.relpath = git_obj.orig_relpath - git_obj.orig_relpath = nil - do_update = true - end - else - -- File removed from index, do nothing - end - - if do_update then - git_obj.file = git_obj.repo.toplevel..util.path_sep..git_obj.relpath - bcache.file = git_obj.file - git_obj:update_file_info() - scheduler() - - local bufexists = vim.fn.bufexists(bcache.file) == 1 - local old_name = api.nvim_buf_get_name(bufnr) - - if not bufexists then - util.buf_rename(bufnr, bcache.file) - end - - local msg = bufexists and 'Cannot rename' or 'Renamed' - dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) - end -end - - -function M.watch_gitdir(bufnr: integer, gitdir: string): vim.loop.FSPollObj - if not config.watch_gitdir.enable then - return - end - - dprintf('Watching git dir') - local w = uv.new_fs_poll(true) - w:start(gitdir, config.watch_gitdir.interval, void(function(err: string) - local __FUNC__ = 'watcher_cb' - if err then - dprintf('Git dir update error: %s', err) - return - end - dprint('Git dir update') - - local bcache = cache[bufnr] - - if not bcache then - -- Very occasionally an external git operation may cause the buffer to - -- detach and update the git dir simultaneously. When this happens this - -- handler will trigger but there will be no cache. - dprint('Has detached, aborting') - return - end - - local git_obj = bcache.git_obj - - git_obj.repo:update_abbrev_head() - - scheduler() - Status:update(bufnr, { head = git_obj.repo.abbrev_head}) - - local was_tracked = git_obj.object_name ~= nil - local old_relpath = git_obj.relpath - - git_obj:update_file_info() - - if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then - -- File was tracked but is no longer tracked. Must of been removed or - -- moved. Check if it was moved and switch to it. - handle_moved(bufnr, bcache, old_relpath) - end - - bcache:invalidate() - - M.update(bufnr, bcache) - end)) - return w -end - -function M.reset_signs() - -- Remove all signs - if signs_normal then - signs_normal:reset() - end - if signs_staged then - signs_staged:reset() - end -end - -local function on_win(_, _, bufnr: integer, topline: integer, botline_guess: integer): boolean - local bcache = cache[bufnr] - if not bcache or not bcache.hunks then - return false - end - local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr)) - - local untracked = bcache.git_obj.object_name == nil - - apply_win_signs(bufnr, topline+1, botline+1, false, untracked) - - if not (config.word_diff and config.diff_opts.internal) then - return false - end -end - -local function on_line(_, _, bufnr: integer, row: integer) - apply_word_diff(bufnr, row) -end - -function M.setup() - -- Calling this before any await calls will stop nvim's intro messages being - -- displayed - api.nvim_set_decoration_provider(ns, { - on_win = on_win, - on_line = on_line, - }) - - signs_normal = Signs.new(config.signs) - if config._signs_staged_enable then - signs_staged = Signs.new(config._signs_staged, 'staged') - end - - M.update_debounced = debounce_trailing(config.update_debounce, void(M.update)) as function(integer, CacheEntry) -end - -return M diff --git a/teal/gitsigns/mappings.tl b/teal/gitsigns/mappings.tl deleted file mode 100644 index fb61b00..0000000 --- a/teal/gitsigns/mappings.tl +++ /dev/null @@ -1,87 +0,0 @@ --- Originated from: --- https://github.com/norcalli/neovim-plugin/blob/master/lua/neovim-plugin/apply_mappings.lua - -local validate = vim.validate -local api = vim.api - -local valid_modes: {string:string} = { - n = 'n'; v = 'v'; x = 'x'; i = 'i'; o = 'o'; t = 't'; c = 'c'; s = 's'; - -- :map! and :map - ['!'] = '!'; [' '] = ''; -} - -local valid_options: {string:string} = { - buffer = 'boolean', - expr = 'boolean', - noremap = 'boolean', - nowait = 'boolean', - script = 'boolean', - silent = 'boolean', - unique = 'boolean', -} - -local function validate_option_keywords(options: table) - for option_name, expected_type in pairs(valid_options) do - local value = options[option_name] - if value then - validate { - [option_name] = { value, expected_type }; - } - end - end -end - -local function apply_mappings(mappings: {string:any}, bufnr: integer) - validate { - mappings = { mappings, 'table' }; - } - - local default_options = {} - for key, val in pairs(mappings) do - -- Skip any inline default keywords. - if valid_options[key] then - default_options[key] = val - end - end - - for key, opts in pairs(mappings) do - repeat - -- Skip any inline default keywords. - if valid_options[key] then - break - end - - local rhs: string - local options: {string:any} - if opts is string then - rhs = opts - options = {} - elseif opts is table then - rhs = opts[1] as string - local boptions = {} - for k in pairs(valid_options) do - boptions[k] = opts[k] - end - options = boptions - else - error(("Invalid type for option rhs: %q = %s"):format(type(opts), vim.inspect(opts))) - end - options = vim.tbl_extend('keep', default_options, options) as {string:any} - - validate_option_keywords(options) - - local mode, mapping = key:match("^(.)[ ]*(.+)$") - - if not mode or not valid_modes[mode] then - error("Invalid mode specified for keymapping. mode="..mode) - end - - -- In case users haven't updated their config. - options.buffer = nil - - api.nvim_buf_set_keymap(bufnr, mode, mapping, rhs, options) - until true - end -end - -return apply_mappings diff --git a/teal/gitsigns/message.tl b/teal/gitsigns/message.tl deleted file mode 100644 index 342ff57..0000000 --- a/teal/gitsigns/message.tl +++ /dev/null @@ -1,16 +0,0 @@ -local type MsgFun = function(string, ...: any) - -local record M - warn : MsgFun - error : MsgFun -end - -M.warn = vim.schedule_wrap(function(s: string, ...:any) - vim.notify(s:format(...), vim.log.levels.WARN, {title = 'gitsigns'}) -end) as MsgFun - -M.error = vim.schedule_wrap(function(s: string, ...:any) - vim.notify(s:format(...), vim.log.levels.ERROR, {title = 'gitsigns'}) -end) as MsgFun - -return M diff --git a/teal/gitsigns/popup.tl b/teal/gitsigns/popup.tl deleted file mode 100644 index a9c57e2..0000000 --- a/teal/gitsigns/popup.tl +++ /dev/null @@ -1,245 +0,0 @@ -local record popup - type LinesSpec = {{{string,string|{HlMark}}}} - record HlMark - hl_group : string - start_row : integer - end_row : integer - start_col : integer - end_col : integer - end -end - -local HlMark = popup.HlMark - -local api = vim.api - -local function bufnr_calc_width(bufnr: integer, lines: {string}): integer - return api.nvim_buf_call(bufnr, function(): integer - local width = 0 - for _, l in ipairs(lines) do - if vim.fn.type(l) == vim.v.t_string then - local len = vim.fn.strdisplaywidth(l) - if len > width then - width = len - end - end - end - return width + 1 -- Add 1 for some miinor padding - end) -end - --- Expand height until all lines are visible to account for wrapped lines. -local function expand_height(winid: integer, nlines: integer) - local newheight = 0 - for _ = 0, 50 do - local winheight = api.nvim_win_get_height(winid) - if newheight > winheight then - -- Window must be max height - break - end - local wd = api.nvim_win_call(winid, function(): integer - return vim.fn.line('w$') - end) - if wd >= nlines then - break - end - newheight = winheight+nlines-wd - api.nvim_win_set_height(winid, newheight) - end -end - -local function offset_hlmarks(hlmarks: {HlMark}, row_offset: integer) - for _, h in ipairs(hlmarks) do - if h.start_row then - h.start_row = h.start_row + row_offset - end - if h.end_row then - h.end_row = h.end_row + row_offset - end - end -end - -local function process_linesspec(fmt: popup.LinesSpec): {string}, {HlMark} - local lines: {string} = {} - local hls: {HlMark} = {} - - local row = 0 - for _, section in ipairs(fmt) do - local sec = {} - local pos = 0 - for _, part in ipairs(section) do - local text = part[1] - local hl = part[2] - - sec[#sec+1] = text - - local srow = row - local scol = pos - - local ts = vim.split(text, '\n') - - if #ts > 1 then - pos = 0 - row = row + #ts - 1 - else - pos = pos + #text - end - - if hl is string then - hls[#hls+1] = { - hl_group = hl, - start_row = srow, - end_row = row, - start_col = scol, - end_col = pos, - } - else -- hl is {HlMark} - offset_hlmarks(hl, srow) - vim.list_extend(hls, hl) - end - end - for _, l in ipairs(vim.split(table.concat(sec, ''), '\n')) do - lines[#lines+1] = l - end - row = row + 1 - end - - return lines, hls -end - -local function close_all_but(id: string) - for _, winid in ipairs(api.nvim_list_wins()) do - if vim.w[winid].gitsigns_preview ~= nil and - vim.w[winid].gitsigns_preview ~= id then - pcall(api.nvim_win_close, winid, true) - end - end -end - -function popup.close(id: string) - for _, winid in ipairs(api.nvim_list_wins()) do - if vim.w[winid].gitsigns_preview == id then - pcall(api.nvim_win_close, winid, true) - end - end -end - -function popup.create0(lines: {string}, opts: {string:any}, id: string): integer, integer - -- Close any popups not matching id - close_all_but(id) - - local ts = vim.bo.tabstop - local bufnr = api.nvim_create_buf(false, true) - assert(bufnr, "Failed to create buffer") - - -- In case nvim was opened with '-M' - vim.bo[bufnr].modifiable = true - api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) - vim.bo[bufnr].modifiable = false - - -- Set tabstop before calculating the buffer width so that the correct width - -- is calculated - vim.bo[bufnr].tabstop = ts - - local opts1 = vim.deepcopy(opts or {}) - opts1.height = opts1.height or #lines -- Guess, adjust later - opts1.width = opts1.width or bufnr_calc_width(bufnr, lines) - - local winid = api.nvim_open_win(bufnr, false, opts1) - - vim.w[winid].gitsigns_preview = id or true - - if not opts.height then - expand_height(winid, #lines) - end - - if opts1.style == 'minimal' then - -- If 'signcolumn' = auto:1-2, then a empty signcolumn will appear and cause - -- line wrapping. - vim.wo[winid].signcolumn = 'no' - end - - -- Close the popup when navigating to any window which is not the preview - -- itself. - local group = 'gitsigns_popup' - local group_id = api.nvim_create_augroup(group, {}) - local old_cursor = api.nvim_win_get_cursor(0) - - api.nvim_create_autocmd({'CursorMoved', 'CursorMovedI'}, { - group = group_id, - callback = function() - local cursor = api.nvim_win_get_cursor(0) - -- Did the cursor REALLY change (neovim/neovim#12923) - if (old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2]) - and api.nvim_get_current_win() ~= winid then - -- Clear the augroup - api.nvim_create_augroup(group, {}) - pcall(api.nvim_win_close, winid, true) - return - end - old_cursor = cursor - end - }) - - api.nvim_create_autocmd('WinClosed', { - pattern = tostring(winid), - group = group_id, - callback = function() - -- Clear the augroup - api.nvim_create_augroup(group, {}) - end - }) - - -- update window position to follow the cursor when scrolling - api.nvim_create_autocmd('WinScrolled', { - buffer = api.nvim_get_current_buf(), - group = group_id, - callback = function() - if api.nvim_win_is_valid(winid) then - api.nvim_win_set_config(winid, opts1) - end - end - }) - - return winid, bufnr -end - -local ns = api.nvim_create_namespace('gitsigns_popup') - -function popup.create(lines_spec: popup.LinesSpec, opts: {string:any}, id: string): integer, integer - local lines, highlights = process_linesspec(lines_spec) - local winid, bufnr = popup.create0(lines, opts, id) - - for _, hl in ipairs(highlights) do - local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, { - hl_group = hl.hl_group, - end_row = hl.end_row, - end_col = hl.end_col, - hl_eol = true, - }) - if not ok then - error(vim.inspect(hl)..'\n'..err) - end - end - - return winid, bufnr -end - -function popup.is_open(id: string): integer - for _, winid in ipairs(api.nvim_list_wins()) do - if vim.w[winid].gitsigns_preview == id then - return winid - end - end - return nil -end - -function popup.focus_open(id: string): integer - local winid = popup.is_open(id) - if winid then - api.nvim_set_current_win(winid) - end - return winid -end - -return popup diff --git a/teal/gitsigns/repeat.tl b/teal/gitsigns/repeat.tl deleted file mode 100644 index 8a05b2f..0000000 --- a/teal/gitsigns/repeat.tl +++ /dev/null @@ -1,28 +0,0 @@ -local api = vim.api - -local record M - mk_repeatable: function(fn: F): F - repeat_action: function() -end - -function M.mk_repeatable(fn: function): function - return function(...: any) - local args = {...} - local nargs = select('#', ...) - vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action" - - M.repeat_action = function() - fn(unpack(args, 1, nargs)) - if vim.fn.exists('*repeat#set') == 1 then - local action = api.nvim_replace_termcodes( - string.format('call %s()', vim.go.operatorfunc), - true, true, true) - vim.fn['repeat#set'](action, -1) - end - end - - vim.cmd'normal! g@l' - end -end - -return M diff --git a/teal/gitsigns/signs.tl b/teal/gitsigns/signs.tl deleted file mode 100644 index 953b164..0000000 --- a/teal/gitsigns/signs.tl +++ /dev/null @@ -1,43 +0,0 @@ -local config = require('gitsigns.config').config -local SignsConfig = require('gitsigns.config').Config.SignsConfig - -local dprint = require('gitsigns.debug.log').dprint - -local B = require('gitsigns.signs.base') - --- local function capitalise_word(x: string): string --- return x:sub(1, 1):upper()..x:sub(2) --- end - -function B.new(cfg: SignsConfig, name: string): B - local __FUNC__ = 'signs.init' - local C: B - if config._extmark_signs then - dprint('Using extmark signs') - C = require('gitsigns.signs.extmarks') - else - dprint('Using vimfn signs') - C = require('gitsigns.signs.vimfn') - end - - local hls = (name == 'staged' and config._signs_staged or config.signs) as {B.SignType:B.HlDef} - -- Add when config.signs.*.[hl,numhl,linehl] are removed - -- for _, t in ipairs { - -- 'add', - -- 'change', - -- 'delete', - -- 'topdelete', - -- 'changedelete', - -- 'untracked', - -- } do - -- local hl = string.format('GitSigns%s%s', name, capitalise_word(t)) - -- obj.hls[t] = { - -- hl = hl, - -- numhl = hl..'Nr', - -- linehl = hl..'Ln', - -- } - -- end - return C._new(cfg, hls, name) -end - -return B diff --git a/teal/gitsigns/signs/base.tl b/teal/gitsigns/signs/base.tl deleted file mode 100644 index 9f3a21e..0000000 --- a/teal/gitsigns/signs/base.tl +++ /dev/null @@ -1,46 +0,0 @@ -local SignsConfig = require('gitsigns.config').Config.SignsConfig - -local record M - enum SignType - "add" - "delete" - "change" - "topdelete" - "changedelete" - "untracked" - end - - record Sign - type: SignType - count: integer - lnum: integer - end - - record HlDef - hl: string - numhl: string - linehl: string - end - - hls: {SignType:HlDef} - - name: string - group: string - config: SignsConfig - - -- Used by signs/extmarks.tl - ns: integer - - -- Used by signs/vimfn.tl - placed: {integer:{integer:Sign}} - - new : function(cfg: SignsConfig, name: string): M - _new : function(cfg: SignsConfig, hls: {SignType:HlDef}, name: string): M - remove : function(M, bufnr: integer, start_lnum: integer, end_lnum: integer) - add : function(M, bufnr: integer, signs: {M.Sign}) - contains : function(M, bufnr: integer, start: integer, last: integer): boolean - on_lines : function(M, bufnr: integer, first: integer, last_orig: integer, last_new: integer) - reset : function(M) -end - -return M diff --git a/teal/gitsigns/signs/extmarks.tl b/teal/gitsigns/signs/extmarks.tl deleted file mode 100644 index d41ef3d..0000000 --- a/teal/gitsigns/signs/extmarks.tl +++ /dev/null @@ -1,89 +0,0 @@ -local api = vim.api - -local SignsConfig = require('gitsigns.config').Config.SignsConfig -local config = require('gitsigns.config').config - -local B = require('gitsigns.signs.base') - -local M: B = {} - -local group_base = 'gitsigns_extmark_signs_' - -function M._new(cfg: SignsConfig, hls: {M.SignType:M.HlDef}, name: string): B - local self = setmetatable({} as B, {__index = M}) - self.config = cfg - self.hls = hls - self.group = group_base..(name or '') - self.ns = api.nvim_create_namespace(self.group) - return self -end - -function M:on_lines(buf: integer, _: integer, last_orig: integer, last_new: integer) - -- Remove extmarks on line deletions to mimic - -- the behaviour of vim signs. - if last_orig > last_new then - self:remove(buf, last_new+1, last_orig) - end -end - -function M:remove(bufnr: integer, start_lnum: integer, end_lnum: integer) - if start_lnum then - api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum-1, end_lnum or start_lnum) - else - api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1) - end -end - -function M:add(bufnr: integer, signs: {M.Sign}) - if not config.signcolumn and not config.numhl and not config.linehl then - -- Don't place signs if it won't show anything - return - end - - for _, s in ipairs(signs) do - if not self:contains(bufnr, s.lnum) then - local cs = self.config[s.type] - local text = cs.text - if config.signcolumn and cs.show_count and s.count then - local count = s.count - local cc = config.count_chars - local count_char = cc[count] or cc['+'] or '' - text = cs.text..count_char - end - - local hls = self.hls[s.type] - - local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum-1, -1, { - id = s.lnum, - sign_text = config.signcolumn and text or '', - priority = config.sign_priority, - sign_hl_group = hls.hl, - number_hl_group = config.numhl and hls.numhl or nil, - line_hl_group = config.linehl and hls.linehl or nil, - }) - - if not ok and config.debug_mode then - vim.schedule(function() - error(table.concat({ - string.format('Error placing extmark on line %d', s.lnum), - err - }, '\n')) - end) - end - end - end -end - -function M:contains(bufnr: integer, start: integer, last: integer): boolean - local marks = api.nvim_buf_get_extmarks( - bufnr, self.ns, {start-1, 0}, {last or start, 0}, {limit=1}) - return #marks > 0 -end - -function M:reset() - for _, buf in ipairs(api.nvim_list_bufs()) do - self:remove(buf) - end -end - -return M diff --git a/teal/gitsigns/signs/vimfn.tl b/teal/gitsigns/signs/vimfn.tl deleted file mode 100644 index 677b1cc..0000000 --- a/teal/gitsigns/signs/vimfn.tl +++ /dev/null @@ -1,163 +0,0 @@ -local fn = vim.fn - -local SignsConfig = require('gitsigns.config').Config.SignsConfig -local config = require('gitsigns.config').config - -local emptytable = require('gitsigns.util').emptytable - -local B = require('gitsigns.signs.base') - -local M: B = {} - --- The internal representation of signs in Neovim is a linked list which is slow --- to index. To improve efficiency we add an abstraction layer to the signs API --- which keeps track of which signs have already been placed in the buffer. --- --- This allows us to: --- - efficiently query placed signs. --- - skip adding a sign if it has already been placed. - -local function capitalise_word(x: string): string - return x:sub(1, 1):upper()..x:sub(2) -end - -local sign_define_cache: {string:table} = {} -local sign_name_cache: {string:string} = {} - -local function get_sign_name(name: string, stype: string): string - local key = name..stype - if not sign_name_cache[key] then - sign_name_cache[key] = string.format( - '%s%s%s', 'GitSigns', capitalise_word(key), capitalise_word(stype)) - end - - return sign_name_cache[key] -end - -local function sign_get(name: string): table - if not sign_define_cache[name] then - local s = fn.sign_getdefined(name) - if not vim.tbl_isempty(s) then - sign_define_cache[name] = s - end - end - return sign_define_cache[name] -end - -local function define_sign(name: string, opts: {string:any}, redefine: boolean) - if redefine then - sign_define_cache[name] = nil - fn.sign_undefine(name) - fn.sign_define(name, opts) - elseif not sign_get(name) then - fn.sign_define(name, opts) - end -end - -local function define_signs(obj: B, redefine: boolean) - -- Define signs - for stype, cs in pairs(obj.config) do - local hls = obj.hls[stype] - define_sign(get_sign_name(obj.name, stype), { - texthl = hls.hl, - text = config.signcolumn and cs.text or nil, - numhl = config.numhl and hls.numhl or nil, - linehl = config.linehl and hls.linehl or nil - }, redefine) - end -end - -local group_base = 'gitsigns_vimfn_signs_' - -function M._new(cfg: SignsConfig, hls: {M.SignType:M.HlDef}, name: string): B - local self = setmetatable({} as B, {__index = M}) - self.name = name or '' - self.group = group_base..(name or '') - self.config = cfg - self.hls = hls - self.placed = emptytable() - - define_signs(self, false) - - return self -end - -function M:on_lines(_: integer, _: integer, _: integer, _: integer) -end - -function M:remove(bufnr: integer, start_lnum: integer, end_lnum: integer) - end_lnum = end_lnum or start_lnum - - if start_lnum then - for lnum = start_lnum, end_lnum do - self.placed[bufnr][lnum] = nil - fn.sign_unplace(self.group, {buffer = bufnr, id = lnum}) - end - else - self.placed[bufnr] = nil - fn.sign_unplace(self.group, {buffer = bufnr}) - end -end - -function M:add(bufnr: integer, signs: {M.Sign}) - if not config.signcolumn and not config.numhl and not config.linehl then - -- Don't place signs if it won't show anything - return - end - - local to_place = {} - - for _, s in ipairs(signs) do - local sign_name = get_sign_name(self.name, s.type) - - local cs = self.config[s.type] - if config.signcolumn and cs.show_count and s.count then - local count = s.count - local cc = config.count_chars - local count_suffix = cc[count] and tostring(count) or (cc['+'] and 'Plus') or '' - local count_char = cc[count] or cc['+'] or '' - local hls = self.hls[s.type] - sign_name = sign_name..count_suffix - define_sign(sign_name, { - texthl = hls.hl, - text = config.signcolumn and cs.text..count_char or '', - numhl = config.numhl and hls.numhl or nil, - linehl = config.linehl and hls.linehl or nil - }) - end - - if not self.placed[bufnr][s.lnum] then - local sign = { - id = s.lnum, - group = self.group, - name = sign_name, - buffer = bufnr, - lnum = s.lnum, - priority = config.sign_priority - } - self.placed[bufnr][s.lnum] = s - to_place[#to_place+1] = sign - end - end - - if #to_place > 0 then - fn.sign_placelist(to_place) - end -end - -function M:contains(bufnr: integer, start: integer, last: integer): boolean - for i = start+1, last+1 do - if self.placed[bufnr][i] then - return true - end - end - return false -end - -function M:reset() - self.placed = emptytable() - fn.sign_unplace(self.group) - define_signs(self, true) -end - -return M diff --git a/teal/gitsigns/status.tl b/teal/gitsigns/status.tl deleted file mode 100644 index b6992d9..0000000 --- a/teal/gitsigns/status.tl +++ /dev/null @@ -1,46 +0,0 @@ -local api = vim.api - - -local record StatusObj - added : integer - removed : integer - changed : integer - head : string - root : string - gitdir : string -end - -local Status = { - StatusObj = StatusObj, -} - -function Status:update(bufnr: integer, status: StatusObj) - if not api.nvim_buf_is_loaded(bufnr) then - return - end - local bstatus = vim.b[bufnr].gitsigns_status_dict - if bstatus then - status = vim.tbl_extend('force', bstatus as table, status as table) as StatusObj - end - vim.b[bufnr].gitsigns_head = status.head or '' - vim.b[bufnr].gitsigns_status_dict = status - - local config = require('gitsigns.config').config - - vim.b[bufnr].gitsigns_status = config.status_formatter(status) -end - -function Status:clear(bufnr: integer) - if not api.nvim_buf_is_loaded(bufnr) then - return - end - vim.b[bufnr].gitsigns_head = nil - vim.b[bufnr].gitsigns_status_dict = nil - vim.b[bufnr].gitsigns_status = nil -end - -function Status:clear_diff(bufnr: integer) - self:update(bufnr, { added = 0, removed = 0, changed = 0 }) -end - -return Status diff --git a/teal/gitsigns/subprocess.tl b/teal/gitsigns/subprocess.tl deleted file mode 100644 index 2eaf675..0000000 --- a/teal/gitsigns/subprocess.tl +++ /dev/null @@ -1,119 +0,0 @@ -local log = require("gitsigns.debug.log") -local guv = require("gitsigns.uv") -local uv = vim.loop - -local record M - job_cnt: integer - - record JobSpec - command: string - args: {string} - cwd: string - writer: {string} | string - end -end - -M.job_cnt = 0 - ---- @param ... uv_pipe_t -local function try_close(...: uv.Pipe) - for i = 1, select('#', ...) do - local pipe = select(i, ...) - if pipe and not pipe:is_closing() then - pipe:close() - end - end -end - ---- @param pipe uv_pipe_t ---- @param x string[]|string -local function handle_writer(pipe: uv.Pipe, x: {string} | string) - if x is {string} then - for i, v in ipairs(x) do - pipe:write(v) - if i ~= #(x as {string}) then - pipe:write("\n") - else - pipe:write("\n", function() - try_close(pipe) - end) - end - end - elseif x then - -- write is string - pipe:write(x, function() - try_close(pipe) - end) - end -end - ---- @param pipe uv_pipe_t ---- @param output string[] -local function handle_reader(pipe: uv.Pipe, output: {string}) - pipe:read_start(function(err: string, data: string) - if err then - log.eprint(err) - end - if data then - output[#output+1] = data - else - try_close(pipe) - end - end) -end - ---- @param obj table ---- @param callback fun(_: integer, _: integer, _: string?, _: string?) -function M.run_job(obj: M.JobSpec, callback: function(integer, integer, string, string)) - local __FUNC__ = 'run_job' - if log.debug_mode then - local cmd: string = obj.command..' '..table.concat(obj.args, ' ') - log.dprint(cmd) - end - - local stdout_data: {string} = {} - local stderr_data: {string} = {} - - local stdout = guv.new_pipe(false) - local stderr = guv.new_pipe(false) - local stdin: uv.Pipe - if obj.writer then - stdin = guv.new_pipe(false) - end - - --- @type uv_process_t?, integer|string - local handle, _pid: uv.Process, integer - handle, _pid = vim.loop.spawn(obj.command, { - args = obj.args, - stdio = { stdin, stdout, stderr }, - cwd = obj.cwd - }, - function(code: integer, signal: integer) - if handle then - handle:close() - end - stdout:read_stop() - stderr:read_stop() - - try_close(stdin, stdout, stderr) - - local stdout_result = #stdout_data > 0 and table.concat(stdout_data) or nil - local stderr_result = #stderr_data > 0 and table.concat(stderr_data) or nil - - callback(code, signal, stdout_result, stderr_result) - end - ) - - if not handle then - try_close(stdin, stdout, stderr) - error(debug.traceback("Failed to spawn process: " .. vim.inspect(obj))) - end - - handle_reader(stdout, stdout_data) - handle_reader(stderr, stderr_data) - handle_writer(stdin, obj.writer) - - M.job_cnt = M.job_cnt + 1 -end - -return M diff --git a/teal/gitsigns/test.tl b/teal/gitsigns/test.tl deleted file mode 100644 index c7f529d..0000000 --- a/teal/gitsigns/test.tl +++ /dev/null @@ -1,35 +0,0 @@ - -local record M - _tests: {string:function} -end - -local function eq(act: any, exp: any) - assert(act == exp, string.format('%s != %s', act, exp)) -end - -M._tests = {} - -M._tests.expand_format = function() - local util = require'gitsigns.util' - assert('hello % world % 2021' == util.expand_format(' % % ' , { - var1 = 'hello', var2 = 'world', var_time = 1616838297 })) -end - - -M._tests.test_args = function() - local parse_args = require'gitsigns.cli.argparse'.parse_args - - local pos_args, named_args = parse_args('hello there key=value, key1="a b c"') - - eq(pos_args[1], 'hello') - eq(pos_args[2], 'there') - eq(named_args.key, 'value,') - eq(named_args.key1, 'a b c') - - pos_args, named_args = parse_args('base=HEAD~1 posarg') - - eq(named_args.base, 'HEAD~1') - eq(pos_args[1], 'posarg') -end - -return M diff --git a/teal/gitsigns/util.tl b/teal/gitsigns/util.tl deleted file mode 100644 index 5012eaa..0000000 --- a/teal/gitsigns/util.tl +++ /dev/null @@ -1,223 +0,0 @@ -local record M - type FmtInfo = {string:string|integer|{string}} - - path_sep: string -end - -function M.path_exists(path: string): boolean - return vim.loop.fs_stat(path) and true or false -end - -local jit_os: string --- @type string - -if jit then - jit_os = jit.os:lower() -end - -local is_unix: boolean = false -if jit_os then - is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd' -else - local binfmt = package.cpath:match("%p[\\|/]?%p(%a+)") - is_unix = binfmt ~= "dll" -end - ---- @param file string ---- @return string -function M.dirname(file: string): string - return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep)) -end - ---- @param file string ---- @return string[] -function M.file_lines(file: string): {string} - local text: {string} = {} --- @type string[] - for line in io.lines(file) do - text[#text+1] = line - end - return text -end - -M.path_sep = package.config:sub(1, 1) - ---- @param bufnr integer ---- @return string[] -function M.buf_lines(bufnr: integer): {string} - -- nvim_buf_get_lines strips carriage returns if fileformat==dos - local buftext: {string} = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - if vim.bo[bufnr].fileformat == 'dos' then - for i = 1, #buftext do - buftext[i] = buftext[i]..'\r' - end - end - return buftext -end - ---- @param buf integer -local function delete_alt(buf: integer) - local alt = vim.api.nvim_buf_call(buf, function(): integer - return vim.fn.bufnr('#') - end) as integer - if alt ~= buf and alt ~= -1 then - pcall(vim.api.nvim_buf_delete, alt, {force=true}) - end -end - ---- @param bufnr integer ---- @param name string -function M.buf_rename(bufnr: integer, name: string) - vim.api.nvim_buf_set_name(bufnr, name) - delete_alt(bufnr) -end - ---- @param bufnr integer ---- @param start_row integer ---- @param end_row integer ---- @param lines string[] -function M.set_lines(bufnr: integer, start_row: integer, end_row: integer, lines: {string}) - if vim.bo[bufnr].fileformat == 'dos' then - for i = 1, #lines do - lines[i] = lines[i]:gsub('\r$', '') - end - end - vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines) -end - ---- @return string -function M.tmpname(): string - if is_unix then - return os.tmpname() - end - return vim.fn.tempname() -end - ---- @param timestamp number ---- @return string -function M.get_relative_time(timestamp: number): string - local current_timestamp = os.time() - local elapsed = current_timestamp - timestamp - - if elapsed == 0 then - return 'a while ago' - end - - local minute_seconds = 60 - local hour_seconds = minute_seconds * 60 - local day_seconds = hour_seconds * 24 - local month_seconds = day_seconds * 30 - local year_seconds = month_seconds * 12 - - local to_relative_string = function(time: number, divisor: number, time_word: string): string - local num = math.floor(time / divisor) - if num > 1 then - time_word = time_word .. 's' - end - - return num .. ' ' .. time_word .. ' ago' - end - - if elapsed < minute_seconds then - return to_relative_string(elapsed, 1, 'second') - elseif elapsed < hour_seconds then - return to_relative_string(elapsed, minute_seconds, 'minute') - elseif elapsed < day_seconds then - return to_relative_string(elapsed, hour_seconds, 'hour') - elseif elapsed < month_seconds then - return to_relative_string(elapsed, day_seconds, 'day') - elseif elapsed < year_seconds then - return to_relative_string(elapsed, month_seconds, 'month') - else - return to_relative_string(elapsed, year_seconds, 'year') - end -end - ---- @generic T ---- @param x T[] ---- @return T[] -function M.copy_array(x: {T}): {T} - local r = {} - for i, e in ipairs(x) do - r[i] = e - end - return r -end - ---- Strip '\r' from the EOL of each line only if all lines end with '\r' ---- @param xs0 string[] ---- @return string[] -function M.strip_cr(xs0: {string}): {string} - for i = 1, #xs0 do - if xs0[i]:sub(-1) ~= '\r' then - -- don't strip, return early - return xs0 - end - end - -- all lines end with '\r', need to strip - local xs = vim.deepcopy(xs0) - for i = 1, #xs do - xs[i] = xs[i]:sub(1, -2) - end - return xs -end - -function M.calc_base(base: string): string - if base and base:sub(1, 1):match('[~\\^]') then - base = 'HEAD'..base - end - return base -end - -function M.emptytable(): T - return setmetatable({} as T, { - __index = function(t: table, k: any): any - t[k] = {} - return t[k] - end - }) -end - -local function expand_date(fmt: string, time: integer): string - if fmt == '%R' then - return M.get_relative_time(time) - end - return os.date(fmt, time) -end - ----@param fmt string ----@param info table ----@param reltime boolean Use relative time as the default date format ----@return string -function M.expand_format(fmt: string, info: M.FmtInfo, reltime: boolean): string - local ret = {} --- @type string[] - - for _ = 1, 20 do -- loop protection - -- Capture or - local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)') - if not match then - break - end - - ret[#ret+1], fmt = fmt:sub(1, scol-1), fmt:sub(ecol+1) - - local v = info[key] - - if v then - if v is {string} then - v = table.concat(v, '\n') - end - if vim.endswith(key, '_time') then - if time_fmt == '' then - time_fmt = reltime and '%R' or '%Y-%m-%d' - end - v = expand_date(time_fmt, v as integer) - end - match = tostring(v) - end - ret[#ret+1] = match - end - - ret[#ret+1] = fmt - return table.concat(ret, '') -end - -return M diff --git a/teal/gitsigns/uv.tl b/teal/gitsigns/uv.tl deleted file mode 100644 index f09e741..0000000 --- a/teal/gitsigns/uv.tl +++ /dev/null @@ -1,80 +0,0 @@ -local uv = vim.loop - -local record M - handles: {integer:{uv.Handle, boolean, string}} -end - ---- @type table -local handles: {integer:{uv.Handle, boolean, string}} = {} - -M.handles = handles - -function M.print_handles() - local none = true - for _, e in pairs(handles) do - local handle, longlived, tr = e[1], e[2], e[3] - if handle and not longlived and not handle:is_closing() then - print('') - print(tr) - none = false - end - end - if none then - print('No active handles') - end -end - -vim.api.nvim_create_autocmd('VimLeavePre', { - callback = function() - for _, e in pairs(handles) do - local handle = e[1] - if handle and not handle:is_closing() then - handle:close() - end - end - end -}) - ---- @param longlived boolean ---- @return uv_timer_t? -function M.new_timer(longlived: boolean): uv.Timer - local r = uv.new_timer() - if r then - table.insert(handles, {r as uv.Handle, longlived, debug.traceback()}) - end - return r -end - ---- @param longlived boolean ---- @return uv_fs_poll_t? -function M.new_fs_poll(longlived: boolean): uv.FSPollObj - local r = uv.new_fs_poll() - if r then - table.insert(handles, {r as uv.Handle, longlived, debug.traceback()}) - end - return r -end - ---- @param ipc boolean ---- @return uv_pipe_t? -function M.new_pipe(ipc: boolean): uv.Pipe - local r = uv.new_pipe(ipc) - if r then - table.insert(handles, {r as uv.Handle, false, debug.traceback()}) - end - return r -end - ---- @param cmd string ---- @param opts uv.aliases.spawn_options ---- @param on_exit fun(_: integer, _, integer): uv_process_t, integer ---- @return uv_process_t?, string|integer -function M.spawn(cmd: string, opts: uv.SpawnOpts, on_exit: function(integer, integer)): uv.Process, integer - local handle, pid = uv.spawn(cmd, opts, on_exit) - if handle then - table.insert(handles, {handle as uv.Handle, false, cmd..' '..vim.inspect(opts)}) - end - return handle, pid -end - -return M diff --git a/tlconfig.lua b/tlconfig.lua deleted file mode 100644 index eee9a92..0000000 --- a/tlconfig.lua +++ /dev/null @@ -1,10 +0,0 @@ -return { - gen_target = '5.1', - gen_compat = 'off', - global_env_def = 'types', - include_dir = { - 'types', 'teal', - }, - source_dir = 'teal', - build_dir = "lua", -} diff --git a/types/ffi.d.tl b/types/ffi.d.tl deleted file mode 100644 index 82ce840..0000000 --- a/types/ffi.d.tl +++ /dev/null @@ -1,25 +0,0 @@ - -local record Cdefs - xdl_diff: (function(...:any): number) -end - --- C callback -local record CCB - free: function(CCB) - set: function(CCB, function) -end - -local record ffi - cdef: function(string) - load: function(string): any - new: function(string, ...:any): any - string: function(any, number): string - gc: function(T, function): T - C: Cdefs - cast: function(string, any): CCB - errno: function(): integer - copy: function - metatype: function(string, any) -end - -return ffi diff --git a/types/trouble.d.tl b/types/trouble.d.tl deleted file mode 100644 index 17c77f1..0000000 --- a/types/trouble.d.tl +++ /dev/null @@ -1,5 +0,0 @@ -local record Trouble - open: function(mode: string) -end - -return Trouble diff --git a/types/types.d.tl b/types/types.d.tl deleted file mode 100644 index 147de23..0000000 --- a/types/types.d.tl +++ /dev/null @@ -1,22 +0,0 @@ -global _: any - -global _TEST: boolean - -global loadstring: function(string): (function(): any) - -global unpack: function({T}, number, number): T... - -global getfenv: function(function): table - -global record jit - arch: string - os: string - version: string - version_num: number - off: function() - off: function(function) - on: function() - status: function(): boolean -end - -global vim = require('vim') diff --git a/types/vim.d.tl b/types/vim.d.tl deleted file mode 100644 index 0e6780d..0000000 --- a/types/vim.d.tl +++ /dev/null @@ -1,237 +0,0 @@ -local api = require 'vim.api' -local fn = require 'vim.fn' -local uv = require 'vim.uv' - -local record M - api: api - fn: fn - - call: function(string, ...:any) - cmd: function(string): any - - deepcopy: function(T): T - - defer_fn: function(function, integer): uv.Timer - - type DiffResult = {integer, integer, integer, integer} - - -- Assume result_type == 'indices' - diff: function(string|{string}, string|{string}, table): {DiffResult} - - env: {string:string} - - iconv: function(string, string, string): string - - is_callable: function(any): boolean - - record go - operatorfunc: string - end - - record o - diffopt: string - eventignore: string - shortmess: string - splitright: boolean - updatetime: number - wrapscan: boolean - end - - record WinOption - {WinOption} - diff: boolean - signcolumn: string - scrolloff: integer - relativenumber: boolean - end - - wo: WinOption - - record BufOption - {BufOption} - fileformat: string - fileencoding: string - filetype: string - modifiable: boolean - modified: boolean - tabstop: integer - - enum BufHidden - '' 'hide' 'unload' 'delete' 'wipe' - end - - bufhidden: BufHidden - - enum BufType - '' 'acwrite' 'help' 'nofile' 'nowrite' 'quickfix' 'terminal' 'prompt' - end - buftype: BufType - end - - bo: BufOption - - record BufVar - {BufVar} - changedtick: integer - - gitsigns_head: string - gitsigns_status_dict: {string:any} - gitsigns_status: string - gitsigns_blame_line_dict: {string:any} - gitsigns_blame_line: string - end - - b: BufVar - - record WinVar - {WinVar} - - gitsigns_preview: string|boolean - end - - w: WinVar - - record g - gitsigns_head: string - loaded_fugitive: integer - end - - record v - vim_did_enter: integer - t_string: integer - end - - record opt - record Opt - get: function(Opt): T - end - - diffopt: Opt<{string}> - foldopen: Opt<{string}> - shortmess: Opt<{string:boolean}> - wrapscan: Opt - end - - record lsp - record util - close_preview_autocmd: function ({string}, number) - end - end - - loop: uv - - in_fast_event: function(): boolean - - list_extend: function({T}, {T}, integer, integer): {T} - list_slice: function({T}, integer, integer): {T} - - record keymap - record Options - buffer: boolean|integer - expr: boolean - end - set: function(string|{string}, string, string|function, Options) - end - - record log - record levels - WARN: integer - ERROR: integer - INFO: integer - DEBUG: integer - end - end - notify: function(string, integer, table) - pretty_print: function(any) - - split: function(string, string): {string} - split: function(string, string, boolean): {string} - - gsplit: function(string, string, boolean): function(): string - - pesc: function(string): string - - record Regex - userdata - - match_str: function(Regex, string): integer, integer - end - - regex: function(string): Regex - - startswith: function(string, string): boolean - endswith: function(string, string): boolean - - schedule_wrap: function(function()): function() - schedule_wrap: function(function(...:any): any...): function(...:any): any... - - schedule: function(function) - validate: function({string:{any}}) - trim: function(string): string - - enum ExtendBehavior - 'error' - 'keep' - 'force' - end - - tbl_add_reverse_lookup: function({K:I}): {I:K} - tbl_contains: function(table, any): boolean - tbl_count: function(table): integer - tbl_deep_extend: function(ExtendBehavior, table, table, ...: table): table - tbl_extend: function(ExtendBehavior, table, table, ...: table): table - tbl_filter: function((function(any): boolean), {T}): {T} - tbl_isempty: function(table): boolean - tbl_islist: function(table): boolean - tbl_keys: function({K:V}): {K} - tbl_map: function(function, table): table - - record InspectOptions - depth: number - newline: string - indent: string - process: function - end - record inspect - METATABLE: any - KEY: any - metamethod __call: function(inspect, any, InspectOptions): string - metamethod __call: function(inspect, any): string - end - - wait: function(number, function, number, boolean) - - record ui - input: function({string:any}, function(string)) - record SelectOpts - prompt: string - format_item: function(T): string - kind: string - end - select: function({T}, SelectOpts, on_choice: function(T, idx: integer)) - end - - record VersionDetails - api_compatible: integer - api_level: integer - api_prerelease: boolean - major: integer - minor: integer - patch: integer - end - - version: function(): VersionDetails - - record mpack - encode: function(any): string - decode: function(string): any - 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 deleted file mode 100644 index 03cdfd1..0000000 --- a/types/vim/api.d.tl +++ /dev/null @@ -1,288 +0,0 @@ -local record M - record UserCmdParams - args: string - bang: boolean - line1: integer - line2: integer - range: integer - count: number - reg: string - mods: string - - record Mods - browse : boolean - confirm : boolean - emsg_silent : boolean - hide : boolean - keepalt : boolean - keepjumps : boolean - keepmarks : boolean - keeppatterns : boolean - lockmarks : boolean - noautocmd : boolean - noswapfile : boolean - sandbox : boolean - silent : boolean - tab : integer - verbose : integer - vertical : boolean - - enum Split '' - 'aboveleft' - 'belowright' - 'topleft' - 'botright' - end - - split : Split - end - smods: Mods - end - - record UserCmdOpts - nargs: string|integer - range: boolean|string|integer - count: boolean|integer - addr: string - bang: boolean - bar: boolean - register: boolean - force: boolean - complete: string|function(arglead: string, line: string): {string} - end - nvim_create_user_command : function(string, function(UserCmdParams), UserCmdOpts) - nvim_buf_attach : function(integer, boolean, {string:any}): boolean - nvim_buf_call : function(integer, function(): T): T - nvim_buf_clear_namespace : function(integer, number, number, number) - nvim_buf_del_extmark : function(integer, number, number): boolean - nvim_buf_delete : function(integer, {string:boolean}) - nvim_buf_get_extmark_by_id : function(integer, number, number, table): {number} - - record GetExtmarOpts - limit: integer - details: boolean - end - - nvim_buf_get_extmarks: function( - buf: integer, - ns: integer, - start: integer | {integer, integer}, - eend: integer | {integer, integer}, - GetExtmarOpts - ): {{integer, integer, integer}} - - nvim_buf_get_lines : function(integer, number, number, boolean): {string} - nvim_buf_get_name : function(integer): string - nvim_buf_is_loaded : function(integer): boolean - nvim_buf_is_valid : function(integer): boolean - nvim_buf_line_count : function(integer): integer - nvim_buf_set_extmark : function(integer, integer, integer, integer, {string:any}): integer - nvim_buf_set_keymap : function(integer, string, string, string, {string:any}) - nvim_buf_set_lines : function(integer, number, number, boolean, {string}) - nvim_buf_set_name : function(integer, string) - nvim_create_buf : function(boolean, boolean): integer - nvim_create_namespace : function(string): integer - - record AugroupOpts - clear: boolean - end - - nvim__buf_redraw_range : function(integer, number, number) - - nvim_create_augroup: function(string, AugroupOpts): integer - - record AutoCmdOpts - 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} - once: boolean - nested: boolean - desc: string - buffer: integer - end - - nvim_create_autocmd: function(string|{string}, AutoCmdOpts): integer - nvim_del_current_line: function() - nvim_del_keymap: function(string, string) - nvim_del_var: function(string) - nvim_echo: function({{string}}, boolean, {string:any}) - nvim_err_write: function(string) - nvim_err_writeln: function(string) - nvim_eval: function(string): any - nvim_exec: function(string, boolean): string - - record ExecAutoCmdOpts - group: string|integer - pattern: string|{string} - buffer: integer - modeline: boolean - data: any - end - - nvim_exec_autocmds: function(string|{string}, ExecAutoCmdOpts): any - nvim_exec_lua: function(string, any): any - nvim_feedkeys: function(string, string, boolean) - nvim_get_api_info: function(): any - nvim_get_chan_info: function(number): {string:any} - nvim_get_color_by_name: function(string): number - nvim_get_color_map: function(): {string:any} - nvim_get_commands: function({string:any}): {string:any} - nvim_get_context: function({string:any}): {string:any} - nvim_get_current_buf: function(): integer - nvim_get_current_line: function(): string - nvim_get_current_tabpage: function(): any - nvim_get_current_win: function(): integer - nvim_get_hl_by_id: function(number, boolean): {string:any} - - record HlAttrs - foreground: integer - background: integer - reverse: boolean - end - - nvim_get_hl_by_name: function(string, boolean): HlAttrs - - nvim_get_hl_id_by_name: function(string): number - nvim_get_keymap: function(string): {{string:any}} - nvim_get_mode: function(): {string:any} - nvim_get_namespaces: function(): {string:any} - nvim_get_option: function(string): any - nvim_get_proc: function(number): any - nvim_get_proc_children: function(number): any - nvim_get_runtime_file: function(string, boolean): {string} - nvim_get_var: function(string): any - nvim_get_vvar: function(string): any - nvim_input: function(string): number - nvim_input_mouse: function(string, string, string, number, number, number) - nvim_list_bufs: function(): {integer} - nvim_list_chans: function(): any - nvim_list_runtime_paths: function(): {string} - nvim_list_tabpages: function(): {any} - nvim_list_uis: function(): any - nvim_list_wins: function(): {integer} - nvim_load_context: function({string:any}): any - nvim_open_win: function(number, boolean, {string:any}): integer - nvim_out_write: function(string) - nvim_parse_expression: function(string, string, boolean): {string:any} - nvim_paste: function(string, boolean, number): boolean - nvim_put: function({string}, string, boolean, boolean) - nvim_replace_termcodes: function(string, boolean, boolean, boolean): string - nvim_select_popupmenu_item: function(number, boolean, boolean, {string:any}) - nvim_set_client_info: function(string, {string:any}, string, {string:any}, {string:any}) - nvim_set_current_buf: function(number) - nvim_set_current_dir: function(string) - nvim_set_current_line: function(string) - nvim_set_current_tabpage: function(any) - nvim_set_current_win: function(number) - nvim_set_decoration_provider: function(number, {string:function}) - nvim_set_hl: function(integer, string, {string:any}) - nvim_set_keymap: function(string, string, string, {string:any}) - nvim_set_option: function(string, any) - nvim_set_var: function(string, any) - nvim_set_vvar: function(string, any) - nvim_strwidth: function(string): number - nvim_subscribe: function(string) - nvim_tabpage_del_var: function(any, string) - nvim_tabpage_get_number: function(any): number - nvim_tabpage_get_var: function(any, string): any - nvim_tabpage_get_win: function(any): number - nvim_tabpage_is_valid: function(any): boolean - nvim_tabpage_list_wins: function(any): {number} - nvim_tabpage_set_var: function(any, string, any) - nvim_ui_attach: function(number, number, {string:any}) - nvim_ui_detach: function() - nvim_ui_pum_set_bounds: function(number, number, number, number) - nvim_ui_pum_set_height: function(number) - nvim_ui_set_option: function(string, any) - nvim_ui_try_resize: function(number, number) - nvim_ui_try_resize_grid: function(number, number, number) - nvim_unsubscribe: function(string) - nvim_win_call: function(number, (function(): T)): T - nvim_win_close: function(number, boolean) - nvim_win_del_var: function(number, string) - nvim_win_get_buf: function(integer): integer - nvim_win_get_config: function(number): {string:any} - nvim_win_get_cursor: function(number): {integer} - nvim_win_get_height: function(integer): integer - nvim_win_get_number: function(number): number - nvim_win_get_option: function(number, string): any - nvim_win_get_position: function(number): {number} - nvim_win_get_tabpage: function(number): any - nvim_win_get_var: function(number, string): any - nvim_win_get_width: function(integer): integer - nvim_win_is_valid: function(number): boolean - nvim_win_set_buf: function(number, number) - - record WinConfig - bufpos: {integer, integer} - virt_lines_above: boolean - width: integer - height: integer - relative: string - row: integer - col: integer - win: integer - enum Anchor - 'NW' - 'NE' - 'SW' - 'SE' - end - anchor: Anchor - end - - nvim_win_set_config: function(number, WinConfig) - - nvim_win_set_cursor: function(number, {number}) - nvim_win_set_height: function(number, number) - nvim_win_set_option: function(number, string, any) - nvim_win_set_var: function(number, string, any) - nvim_win_set_width: function(number, number) - - nvim__buf_redraw_range: function(number, number, number) - nvim_create_autocmd : function(string|{string}, AutoCmdOpts): integer - nvim_echo : function({{string}}, boolean, {string:any}) - nvim_get_color_by_name : function(string): number - nvim_get_current_buf : function(): integer - nvim_get_current_line : function(): string - nvim_get_current_tabpage : function(): any - nvim_get_current_win : function(): integer - nvim_get_hl_by_name : function(string, boolean): {string:any} - nvim_get_mode : function(): {string:any} - nvim_list_bufs : function(): {integer} - nvim_list_wins : function(): {integer} - nvim_open_win : function(integer, boolean, {string:any}): integer - nvim_replace_termcodes : function(string, boolean, boolean, boolean): string - nvim_set_current_buf : function(integer) - nvim_set_current_line : function(string) - nvim_set_current_win : function(integer) - nvim_set_decoration_provider : function(integer, {string:function}) - nvim_set_hl : function(integer, string, {string:any}) - nvim_set_keymap : function(string, string, string, {string:any}) - nvim_strwidth : function(string): number - nvim_win_call : function(integer, (function(): T)): T - nvim_win_close : function(integer, boolean) - nvim_win_get_buf : function(integer): integer - nvim_win_get_config : function(integer): {string:any} - nvim_win_get_cursor : function(integer): {integer} - nvim_win_get_height : function(integer): integer - nvim_win_get_width : function(integer): integer - nvim_win_is_valid : function(integer): boolean - nvim_win_set_buf : function(integer, number) - nvim_win_set_config : function(integer, {string:any}) - nvim_win_set_cursor : function(integer, {number}) - nvim_win_set_height : function(integer, number) - nvim_win_set_width : function(integer, number) -end - -return M diff --git a/types/vim/fn.d.tl b/types/vim/fn.d.tl deleted file mode 100644 index 90d5224..0000000 --- a/types/vim/fn.d.tl +++ /dev/null @@ -1,87 +0,0 @@ -local record M - bufexists: function(string): integer - bufnr: function(string): integer - iconv: function(string, string, string): string - line: function(string): integer - join: function({any}, string): string - getpos: function(string): {integer} - executable: function(string): integer - exists: function(string): integer - expand: function(string): string - foldclosed: function(integer): integer - foldclosedend: function(integer): integer - getcwd: function(): string - input: function(string, string): string - - ['repeat#set']: function(string, integer) - - record QFItem - bufnr: integer - filename: string - lnum: integer - nr: integer - text: string - type: string - end - - record QFWhat - context: any - efm: string - id: integer - idx: integer - items: {QFItem} - lines: {string} - nr: integer - quickfixtextfunc: string - title: string - end - - setqflist: function(list: {QFItem}, action: string, what: QFWhat) - setloclist: function(nr: integer, list: {QFItem}, action: string, what: QFWhat) - - sign_unplace: function(string, {string:any}) - sign_place: function(number, string, string, string | number, {string:any}) - - record SignPlaceItem - buffer: integer - group: string - id: integer - lnum: integer - name: string - priority: integer - end - sign_placelist: function({SignPlaceItem}) - sign_getdefined: function(string): table - - record SignPlacedInfo - bufnr: integer - record SignPlacedSigns - id: integer - name: string - group: string - lnum: integer - priority: integer - end - signs: {SignPlacedSigns} - end - sign_getplaced: function(integer, table): {SignPlacedInfo} - - sign_define: function(string, table): number - sign_undefine: function(string): number - strdisplaywidth: function(string, integer): integer - stridx: function(haystack: string, needle: string, start: integer): integer - string: function(any): string - systemlist: function({string}): {string} - tempname: function(): string - type: function(any): integer - - FugitiveReal: function(...: any): string - FugitiveParse: function(...: any): {string, string} - - record WinInfo - textoff: integer - end - getwininfo: function(integer): {WinInfo} -end - -return M diff --git a/types/vim/uv.d.tl b/types/vim/uv.d.tl deleted file mode 100644 index 1c484c4..0000000 --- a/types/vim/uv.d.tl +++ /dev/null @@ -1,109 +0,0 @@ -local record M - cwd: function(): string - record Timer - userdata - - start: function(Timer, number, number, function): number - stop: function(Timer): number - close: function(Timer): number - is_closing: function(Timer): boolean - again: function(Timer): number - set_repeat: function(Timer, number): number - get_repeat: function(Timer): number - get_due_in: function(Timer): number - end - hrtime: function(): number - new_timer: function(): Timer - timer_start: function(Timer, integer, integer, function()): integer | string - - new_fs_event: function() - - record FSPollObj - userdata - is_closing: function(FSPollObj): boolean | string - close: function(FSPollObj) - start: function(FSPollObj, string, integer, function) - stop: function(FSPollObj) - getpath: function(FSPollObj): string - end - new_fs_poll: function(): FSPollObj - - record FsStatRet - dev : number - mode : number - nlink : number - uid : number - gid : number - rdev : number - ino : number - size : number - blksize : number - blocks : number - flags : number - gen : number - record Time - sec : number - nsec : number - end - atime: Time - mtime: Time - ctime: Time - birthtime: Time - type : string - end - - fs_stat: function(string, function): FsStatRet - - fs_realpath: function(string): string - - new_tcp: function() - - sleep: function(integer) - - record Handle - userdata - - close: function(Handle) - is_closing: function(Handle): boolean | string - end - - record Pipe - userdata - - close: function(Pipe) - is_closing: function(Pipe): boolean | string - read_start: function(Pipe, err: any, data: string) - read_stop: function(Pipe) - write: function(Pipe, string, function()) - - open: function(any) - end - - record Process - userdata - - close: function(Process) - end - - record SpawnOpts - stdio: {Pipe, Pipe, Pipe} - args: {string} - cwd: string - env: {string} - end - - spawn: function(string, SpawnOpts, function(integer, integer)): Process, integer - - read_start: function(Pipe, function) - new_pipe: function(boolean): Pipe - shutdown: function(any, function) - close: function(any, function) - - record WorkCtx - queue: function(WorkCtx, ...:any) - end - - new_work: function(function, function): WorkCtx -end - -return M