refactor: remove teal

This commit is contained in:
Lewis Russell 2023-06-10 17:15:48 +01:00 committed by Lewis Russell
parent 4455bb5364
commit 4d63d996b0
84 changed files with 4589 additions and 13442 deletions

View File

@ -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

6
.stylua.toml Normal file
View File

@ -0,0 +1,6 @@
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferSingle"
call_parentheses = "Always"

View File

@ -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 {}
```

View File

@ -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

View File

@ -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

View File

@ -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: `' <author>, <author_time> - <summary> '`
Type: `string|function`, Default: `" <author>, <author_time> - <summary> "`
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: `' <author>'`
Type: `string|function`, Default: `" <author>"`
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*

View File

@ -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

View File

@ -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

View File

@ -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',

3
lua/README.md generated
View File

@ -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.

252
lua/gitsigns.lua generated
View File

@ -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,
})

1632
lua/gitsigns/actions.lua generated

File diff suppressed because it is too large Load Diff

219
lua/gitsigns/async.lua generated
View File

@ -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

551
lua/gitsigns/attach.lua generated
View File

@ -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

100
lua/gitsigns/cache.lua generated
View File

@ -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

126
lua/gitsigns/cli.lua generated
View File

@ -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

View File

@ -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

907
lua/gitsigns/config.lua generated

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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<any,boolean>
local running = {} --- @type table<any,boolean>
return function(id, ...)
if scheduled[id] then
-- If fn is already scheduled, then drop
return
end
if not running[id] or schedule then
scheduled[id] = true
end
if running[id] then
return
end
while scheduled[id] do
scheduled[id] = nil
running[id] = true
fn(id, ...)
running[id] = nil
end
end
local scheduled = {} --- @type table<any,boolean>
local running = {} --- @type table<any,boolean>
return function(id, ...)
if scheduled[id] then
-- If fn is already scheduled, then drop
return
end
if not running[id] or schedule then
scheduled[id] = true
end
if running[id] then
return
end
while scheduled[id] do
scheduled[id] = nil
running[id] = true
fn(id, ...)
running[id] = nil
end
end
end
return M

54
lua/gitsigns/debug.lua generated
View File

@ -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

View File

@ -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

25
lua/gitsigns/diff.lua generated
View File

@ -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

View File

@ -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 <temp file>.
-- The file will have its original line endings in your working directory.
--
-- We can safely ignore the warning, we turn it off by passing the '-c
-- "core.safecrlf=false"' argument to git-diff.
-- Taken from gitgutter, diff.vim:
--
-- If a file has CRLF line endings and git's core.autocrlf is true, the file
-- in git's object store will have LF line endings. Writing it out via
-- git-show will produce a file with LF line endings.
--
-- If this last file is one of the files passed to git-diff, git-diff will
-- convert its line endings to CRLF before diffing -- which is what we want
-- but also by default outputs a warning on stderr.
--
-- warning: LF will be replace by CRLF in <temp file>.
-- The file will have its original line endings in your working directory.
--
-- We can safely ignore the warning, we turn it off by passing the '-c
-- "core.safecrlf=false"' argument to git-diff.
local 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

View File

@ -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

View File

@ -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

953
lua/gitsigns/git.lua generated

File diff suppressed because it is too large Load Diff

View File

@ -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

540
lua/gitsigns/hunks.lua generated
View File

@ -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

760
lua/gitsigns/manager.lua generated
View File

@ -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<CR>', true, false, true))
end)
-- Navigate to hunk
vim.cmd('normal ' .. tostring(hunk.removed.start) .. 'gg')
vim.cmd('normal ' .. vim.api.nvim_replace_termcodes('z<CR>', true, false, true))
end)
-- 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

View File

@ -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

View File

@ -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

358
lua/gitsigns/popup.lua generated
View File

@ -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

View File

@ -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('<cmd>call %s()<cr>', 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('<cmd>call %s()<cr>', 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

55
lua/gitsigns/signs.lua generated
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

33
lua/gitsigns/test.lua generated
View File

@ -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> % <var2> % <var_time:%Y>', {
var1 = 'hello', var2 = 'world', var_time = 1616838297, }))
local util = require('gitsigns.util')
assert('hello % world % 2021' == util.expand_format('<var1> % <var2> % <var_time:%Y>', {
var1 = 'hello',
var2 = 'world',
var_time = 1616838297,
}))
end
M._tests.test_args = function()
local parse_args = require('gitsigns.cli.argparse').parse_args
local 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

252
lua/gitsigns/util.lua generated
View File

@ -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