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
pcall(vim.api.nvim_buf_delete, alt, { force = true })
end
local alt = vim.api.nvim_buf_call(buf, function()
return vim.fn.bufnr('#')
end)
if alt ~= buf and alt ~= -1 then
pcall(vim.api.nvim_buf_delete, alt, { force = true })
end
end
--- @param bufnr integer
--- @param name string
function M.buf_rename(bufnr, name)
vim.api.nvim_buf_set_name(bufnr, name)
delete_alt(bufnr)
vim.api.nvim_buf_set_name(bufnr, name)
delete_alt(bufnr)
end
--- @param bufnr integer
@ -75,112 +71,112 @@ end
--- @param end_row integer
--- @param lines string[]
function M.set_lines(bufnr, start_row, end_row, lines)
if vim.bo[bufnr].fileformat == 'dos' then
for i = 1, #lines do
lines[i] = lines[i]:gsub('\r$', '')
end
end
vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines)
if vim.bo[bufnr].fileformat == 'dos' then
for i = 1, #lines do
lines[i] = lines[i]:gsub('\r$', '')
end
end
vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines)
end
--- @return string
function M.tmpname()
if is_unix then
return os.tmpname()
end
return vim.fn.tempname()
if is_unix then
return os.tmpname()
end
return vim.fn.tempname()
end
--- @param timestamp number
--- @return string
function M.get_relative_time(timestamp)
local current_timestamp = os.time()
local elapsed = current_timestamp - timestamp
local current_timestamp = os.time()
local elapsed = current_timestamp - timestamp
if elapsed == 0 then
return 'a while ago'
end
if elapsed == 0 then
return 'a while ago'
end
local minute_seconds = 60
local hour_seconds = minute_seconds * 60
local day_seconds = hour_seconds * 24
local month_seconds = day_seconds * 30
local year_seconds = month_seconds * 12
local minute_seconds = 60
local hour_seconds = minute_seconds * 60
local day_seconds = hour_seconds * 24
local month_seconds = day_seconds * 30
local year_seconds = month_seconds * 12
local to_relative_string = function(time, divisor, time_word)
local num = math.floor(time / divisor)
if num > 1 then
time_word = time_word .. 's'
end
local to_relative_string = function(time, divisor, time_word)
local num = math.floor(time / divisor)
if num > 1 then
time_word = time_word .. 's'
end
return num .. ' ' .. time_word .. ' ago'
end
return num .. ' ' .. time_word .. ' ago'
end
if elapsed < minute_seconds then
return to_relative_string(elapsed, 1, 'second')
elseif elapsed < hour_seconds then
return to_relative_string(elapsed, minute_seconds, 'minute')
elseif elapsed < day_seconds then
return to_relative_string(elapsed, hour_seconds, 'hour')
elseif elapsed < month_seconds then
return to_relative_string(elapsed, day_seconds, 'day')
elseif elapsed < year_seconds then
return to_relative_string(elapsed, month_seconds, 'month')
else
return to_relative_string(elapsed, year_seconds, 'year')
end
if elapsed < minute_seconds then
return to_relative_string(elapsed, 1, 'second')
elseif elapsed < hour_seconds then
return to_relative_string(elapsed, minute_seconds, 'minute')
elseif elapsed < day_seconds then
return to_relative_string(elapsed, hour_seconds, 'hour')
elseif elapsed < month_seconds then
return to_relative_string(elapsed, day_seconds, 'day')
elseif elapsed < year_seconds then
return to_relative_string(elapsed, month_seconds, 'month')
else
return to_relative_string(elapsed, year_seconds, 'year')
end
end
--- @generic T
--- @param x T[]
--- @return T[]
function M.copy_array(x)
local r = {}
for i, e in ipairs(x) do
r[i] = e
end
return r
local r = {}
for i, e in ipairs(x) do
r[i] = e
end
return r
end
--- Strip '\r' from the EOL of each line only if all lines end with '\r'
--- @param xs0 string[]
--- @return string[]
function M.strip_cr(xs0)
for i = 1, #xs0 do
if xs0[i]:sub(-1) ~= '\r' then
-- don't strip, return early
return xs0
end
end
-- all lines end with '\r', need to strip
local xs = vim.deepcopy(xs0)
for i = 1, #xs do
xs[i] = xs[i]:sub(1, -2)
end
return xs
for i = 1, #xs0 do
if xs0[i]:sub(-1) ~= '\r' then
-- don't strip, return early
return xs0
end
end
-- all lines end with '\r', need to strip
local xs = vim.deepcopy(xs0)
for i = 1, #xs do
xs[i] = xs[i]:sub(1, -2)
end
return xs
end
function M.calc_base(base)
if base and base:sub(1, 1):match('[~\\^]') then
base = 'HEAD' .. base
end
return base
if base and base:sub(1, 1):match('[~\\^]') then
base = 'HEAD' .. base
end
return base
end
function M.emptytable()
return setmetatable({}, {
__index = function(t, k)
t[k] = {}
return t[k]
end,
})
return setmetatable({}, {
__index = function(t, k)
t[k] = {}
return t[k]
end,
})
end
local function expand_date(fmt, time)
if fmt == '%R' then
return M.get_relative_time(time)
end
return os.date(fmt, time)
if fmt == '%R' then
return M.get_relative_time(time)
end
return os.date(fmt, time)
end
---@param fmt string
@ -188,36 +184,36 @@ end
---@param reltime boolean Use relative time as the default date format
---@return string
function M.expand_format(fmt, info, reltime)
local ret = {} --- @type string[]
local ret = {} --- @type string[]
for _ = 1, 20 do -- loop protection
-- Capture <name> or <name:format>
local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)')
if not match then
break
for _ = 1, 20 do -- loop protection
-- Capture <name> or <name:format>
local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)')
if not match then
break
end
ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1)
local v = info[key]
if v then
if type(v) == 'table' then
v = table.concat(v, '\n')
end
ret[#ret + 1], fmt = fmt:sub(1, scol - 1), fmt:sub(ecol + 1)
local v = info[key]
if v then
if type(v) == "table" then
v = table.concat(v, '\n')
end
if vim.endswith(key, '_time') then
if time_fmt == '' then
time_fmt = reltime and '%R' or '%Y-%m-%d'
end
v = expand_date(time_fmt, v)
end
match = tostring(v)
if vim.endswith(key, '_time') then
if time_fmt == '' then
time_fmt = reltime and '%R' or '%Y-%m-%d'
end
v = expand_date(time_fmt, v)
end
ret[#ret + 1] = match
end
match = tostring(v)
end
ret[#ret + 1] = match
end
ret[#ret + 1] = fmt
return table.concat(ret, '')
ret[#ret + 1] = fmt
return table.concat(ret, '')
end
return M

80
lua/gitsigns/uv.lua generated
View file

@ -2,67 +2,65 @@ local uv = vim.loop
local M = {}
--- @type table<integer,{[1]: uv_handle_t, [2]: boolean, [3]: string}>
local handles = {}
M.handles = handles
function M.print_handles()
local none = true
for _, e in pairs(handles) do
local handle, longlived, tr = e[1], e[2], e[3]
if handle and not longlived and not handle:is_closing() then
print('')
print(tr)
none = false
end
end
if none then
print('No active handles')
end
local none = true
for _, e in pairs(handles) do
local handle, longlived, tr = e[1], e[2], e[3]
if handle and not longlived and not handle:is_closing() then
print('')
print(tr)
none = false
end
end
if none then
print('No active handles')
end
end
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
for _, e in pairs(handles) do
local handle = e[1]
if handle and not handle:is_closing() then
handle:close()
end
callback = function()
for _, e in pairs(handles) do
local handle = e[1]
if handle and not handle:is_closing() then
handle:close()
end
end,
end
end,
})
--- @param longlived boolean
--- @return uv_timer_t?
function M.new_timer(longlived)
local r = uv.new_timer()
if r then
table.insert(handles, { r, longlived, debug.traceback() })
end
return r
local r = uv.new_timer()
if r then
table.insert(handles, { r, longlived, debug.traceback() })
end
return r
end
--- @param longlived boolean
--- @return uv_fs_poll_t?
function M.new_fs_poll(longlived)
local r = uv.new_fs_poll()
if r then
table.insert(handles, { r, longlived, debug.traceback() })
end
return r
local r = uv.new_fs_poll()
if r then
table.insert(handles, { r, longlived, debug.traceback() })
end
return r
end
--- @param ipc boolean
--- @return uv_pipe_t?
function M.new_pipe(ipc)
local r = uv.new_pipe(ipc)
if r then
table.insert(handles, { r, false, debug.traceback() })
end
return r
local r = uv.new_pipe(ipc)
if r then
table.insert(handles, { r, false, debug.traceback() })
end
return r
end
--- @param cmd string
@ -70,11 +68,11 @@ end
--- @param on_exit fun(_: integer, _, integer): uv_process_t, integer
--- @return uv_process_t?, string|integer
function M.spawn(cmd, opts, on_exit)
local handle, pid = uv.spawn(cmd, opts, on_exit)
if handle then
table.insert(handles, { handle, false, cmd .. ' ' .. vim.inspect(opts) })
end
return handle, pid
local handle, pid = uv.spawn(cmd, opts, on_exit)
if handle then
table.insert(handles, { handle, false, cmd .. ' ' .. vim.inspect(opts) })
end
return handle, pid
end
return M

View file

@ -1,206 +0,0 @@
local void = require('gitsigns.async').void
local scheduler = require('gitsigns.async').scheduler
local gs_config = require('gitsigns.config')
local Config = gs_config.Config
local config = gs_config.config
local log = require('gitsigns.debug.log')
local dprintf = log.dprintf
local dprint = log.dprint
local api = vim.api
local uv = require('gitsigns.uv')
local record M
setup: function(cfg: Config)
-- from attach.tl
attach: function(cbuf: integer, ctx: table, trigger: string)
_setup_done: boolean
end
local cwd_watcher: vim.loop.FSPollObj
local update_cwd_head = void(function()
local paths = vim.fs.find('.git', {
limit = 1,
upward = true,
type = 'directory'
})
if #paths == 0 then
return
end
if cwd_watcher then
cwd_watcher:stop()
else
cwd_watcher = uv.new_fs_poll(true)
end
local cwd = vim.loop.cwd()
local gitdir, head: string, string
local gs_cache = require('gitsigns.cache')
-- Look in the cache first
for _, bcache in pairs(gs_cache.cache as {number:gs_cache.CacheEntry}) do
local repo = bcache.git_obj.repo
if repo.toplevel == cwd then
head = repo.abbrev_head
gitdir = repo.gitdir
break
end
end
local git = require('gitsigns.git')
if not head or not gitdir then
local info = git.get_repo_info(cwd)
gitdir = info.gitdir
head = info.abbrev_head
end
scheduler()
vim.g.gitsigns_head = head
if not gitdir then
return
end
local towatch = gitdir..'/HEAD'
if cwd_watcher:getpath() == towatch then
-- Already watching
return
end
-- Watch .git/HEAD to detect branch changes
cwd_watcher:start(
towatch,
config.watch_gitdir.interval,
void(function(err: string)
local __FUNC__ = 'cwd_watcher_cb'
if err then
dprintf('Git dir update error: %s', err)
return
end
dprint('Git cwd dir update')
local new_head = git.get_repo_info(cwd).abbrev_head
scheduler()
vim.g.gitsigns_head = new_head
end)
)
end)
local function setup_cli()
api.nvim_create_user_command('Gitsigns', function(params: api.UserCmdParams)
require'gitsigns.cli'.run(params)
end, {
force = true,
nargs = '*',
range = true,
complete = function(arglead: string, line: string): {string}
return require'gitsigns.cli'.complete(arglead, line)
end})
end
local exported = {
'attach',
'actions'
}
local function setup_debug()
log.debug_mode = config.debug_mode
log.verbose = config._verbose
if config.debug_mode then
exported[#exported+1] = 'debug'
end
end
local function setup_attach()
scheduler()
-- Attach to all open buffers
for _, buf in ipairs(api.nvim_list_bufs()) do
if api.nvim_buf_is_loaded(buf)
and api.nvim_buf_get_name(buf) ~= '' then
M.attach(buf, nil, 'setup')
scheduler()
end
end
api.nvim_create_autocmd({'BufRead', 'BufNewFile', 'BufWritePost'}, {
group = 'gitsigns',
callback = function(data: vim.api.AutoCmdOpts.CallbackData)
M.attach(nil, nil, data.event)
end
})
end
local function setup_cwd_head()
scheduler()
update_cwd_head()
-- Need to debounce in case some plugin changes the cwd too often
-- (like vim-grepper)
api.nvim_create_autocmd('DirChanged', {
group = 'gitsigns',
callback = function()
local debounce = require("gitsigns.debounce").debounce_trailing
debounce(100, update_cwd_head)
end
})
end
--- Setup and start Gitsigns.
---
--- Attributes: ~
--- {async}
---
--- Parameters: ~
--- {cfg} Table object containing configuration for
--- Gitsigns. See |gitsigns-usage| for more details.
M.setup = void(function(cfg: Config)
gs_config.build(cfg)
if vim.fn.executable('git') == 0 then
print('gitsigns: git not in path. Aborting setup')
return
end
if config.yadm.enable and vim.fn.executable('yadm') == 0 then
print("gitsigns: yadm not in path. Ignoring 'yadm.enable' in config")
config.yadm.enable = false
return
end
setup_debug()
setup_cli()
api.nvim_create_augroup('gitsigns', {})
if config._test_mode then
require'gitsigns.attach'._setup()
require'gitsigns.git'._set_version(config._git_version)
end
setup_attach()
setup_cwd_head()
M._setup_done = true
end)
return setmetatable(M, {
__index = function(_, f: string): any
for _, mod in ipairs(exported) do
local m = (require as function)('gitsigns.'..mod) as table
if m[f] then
return m[f]
end
end
end
})

File diff suppressed because it is too large Load diff

View file

@ -1,204 +0,0 @@
local record Async
-- Order by highest number of return types
create: function<T>(T, integer): T
void: function<F>(F): F
wrap: function<A1,R1,R2,R3,R4> (function(A1, function(R1,R2,R3,R4)), integer): function(A1 ): R1,R2,R3,R4
wrap: function<A1,A2,R1,R2> (function(A1,A2, function(R1,R2) ), integer): function(A1,A2 ): R1,R2
wrap: function<A1,A2,A3,A4,R1> (function(A1,A2,A3,A4, function(R1) ), integer): function(A1,A2,A3,A4 ): R1
wrap: function<A1,A2,A3,A4,A5,R1>(function(A1,A2,A3,A4,A5,function(R1) ), integer): function(A1,A2,A3,A4,A5): R1
wrap: function<A1,A2,A3> (function(A1,A2,A3, function() ), integer): function(A1,A2,A3)
wait: function<A1,R1,R2,R3,R4> (integer, function(A1, function(R1,R2,R3,R4)), A1 ): R1,R2,R3,R4
wait: function<A1,A2,R1,R2> (integer, function(A1,A2, function(R1,R2) ), A1, A2 ): R1,R2
wait: function<A1,A2,A3,A4,R1> (integer, function(A1,A2,A3,A4, function(R1) ), A1, A2, A3, A4 ): R1
wait: function<A1,A2,A3,A4,A5,R1>(integer, function(A1,A2,A3,A4,A5,function(R1) ), A1, A2, A3, A4, A5): R1
wait: function<A1,A2,A3> (integer, function(A1,A2,A3, function() ), A1, A2, A3)
scheduler: function()
end
local record M
scheduler: function()
end
local record Async_T
-- Handle for an object currently running on the event loop.
-- The coroutine is paused while this is active.
-- Must provide methods cancel() and is_cancelled()
--
-- Handle gets updated on each call to a wrapped functions, so provide access
-- to it via a proxy
_current: Async_T
cancel: function(Async_T, function)
is_cancelled: function(Async_T): boolean
end
-- Coroutine.running() was changed between Lua 5.1 and 5.2:
-- - 5.1: Returns the running coroutine, or nil when called by the main thread.
-- - 5.2: Returns the running coroutine plus a boolean, true when the running
-- coroutine is the main one.
--
-- For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
--
-- We need to handle both.
-- Store all the async threads in a weak table so we don't prevent them from
-- being garbage collected
local handles = setmetatable({} as {thread:Async_T}, { __mode = 'k' })
--- Returns whether the current execution context is async.
function M.running(): boolean
local current = coroutine.running()
if current and handles[current] then
return true
end
end
-- hack: teal doesn't know table.maxn exists
local function maxn(x: table): integer
return ((table as table).maxn as function)(x) as integer
end
local function is_Async_T(handle: Async_T): boolean
if handle
and type(handle) == 'table'
and vim.is_callable(handle.cancel)
and vim.is_callable(handle.is_cancelled) then
return true
end
end
-- Analogous to uv.close
function Async_T:cancel(cb: function)
-- Cancel anything running on the event loop
if self._current and not self._current:is_cancelled() then
self._current:cancel(cb)
end
end
function Async_T.new(co: thread): Async_T
local handle = setmetatable({} as Async_T, { __index = Async_T })
handles[co] = handle
return handle
end
-- Analogous to uv.is_closing
function Async_T:is_cancelled(): boolean
return self._current and self._current:is_cancelled()
end
local function run(func: function, callback: function, ...: any): Async_T
local co = coroutine.create(func)
local handle = Async_T.new(co)
local function step(...: any)
local ret = {coroutine.resume(co, ...)}
local stat = ret[1] as boolean
if not stat then
local err = ret[2] as string
error(string.format("The coroutine failed with this message: %s\n%s",
err, debug.traceback(co)))
end
if coroutine.status(co) == 'dead' then
if callback then
callback(unpack(ret, 4, maxn(ret)))
end
return
end
local _, nargs, fn = unpack(ret) as (any, integer, function(...:any): Async_T)
assert(type(fn) == 'function', "type error :: expected func")
local args = {select(4, unpack(ret))}
args[nargs] = step
local r = fn(unpack(args, 1, nargs))
if is_Async_T(r) then
handle._current = r
end
end
step(...)
return handle
end
function M.wait(argc: integer, func: function, ...): any...
-- Always run the wrapped functions in xpcall and re-raise the error in the
-- coroutine. This makes pcall work as normal.
local function pfunc(...: any)
local args = { ... }
local cb = args[argc] as function
args[argc] = function(...: any)
cb(true, ...)
end
xpcall(func, function(err)
cb(false, err, debug.traceback())
end, unpack(args, 1, argc))
end
local ret = {coroutine.yield(argc, pfunc, ...)}
local ok = ret[1]
if not ok then
local _, err, traceback = unpack(ret)
error(string.format("Wrapped function failed: %s\n%s", err, traceback))
end
return unpack(ret, 2, maxn(ret))
end
---Creates an async function with a callback style function.
---@param func function: A callback style function to be converted. The last argument must be the callback.
---@param argc number: The number of arguments of func. Must be included.
---@return function: Returns an async function
function M.wrap(func: function, argc: integer): function
assert(argc)
return function(...): any...
if not M.running() then
return func(...)
end
return M.wait(argc, func, ...)
end
end
---Use this to create a function which executes in an async context but
---called from a non-async context. Inherently this cannot return anything
---since it is non-blocking
---@param func function
function M.create(func: function, argc: integer): function(...: any): Async_T
argc = argc or 0
return function(...: any): Async_T
if M.running() then
return func(...) as Async_T
end
local callback = select(argc+1, ...) as function
return run(func, callback, unpack({...}, 1, argc))
end
end
---Use this to create a function which executes in an async context but
---called from a non-async context. Inherently this cannot return anything
---since it is non-blocking
---@param func function
function M.void(func: function(...:any)): function(...:any): Async_T
return function(...: any): Async_T
if M.running() then
return func(...)
end
return run(func, nil, ...)
end
end
---An async function that when called will yield to the Neovim scheduler to be
---able to call the API.
M.scheduler = M.wrap(vim.schedule, 1) as function()
return M as Async

View file

@ -1,411 +0,0 @@
local async = require('gitsigns.async')
local git = require('gitsigns.git')
local log = require("gitsigns.debug.log")
local dprintf = log.dprintf
local dprint = log.dprint
local manager = require('gitsigns.manager')
local hl = require('gitsigns.highlight')
local gs_cache = require('gitsigns.cache')
local cache = gs_cache.cache
local CacheEntry = gs_cache.CacheEntry
local Status = require("gitsigns.status")
local gs_config = require('gitsigns.config')
local config = gs_config.config
local void = require('gitsigns.async').void
local util = require('gitsigns.util')
local throttle_by_id = require("gitsigns.debounce").throttle_by_id
local api = vim.api
local uv = vim.loop
local record GitContext
toplevel: string
gitdir: string
file: string
commit: string
base: string
end
local record M
attach: function(cbuf: integer, ctx: GitContext, trigger: string)
detach: function(bufnr: integer, _keep_signs: boolean)
detach_all: function()
end
local vimgrep_running = false
-- @return (string, string) Tuple of buffer name and commit
local function parse_fugitive_uri(name: string): string, string
if vim.fn.exists('*FugitiveReal') == 0 then
dprint("Fugitive not installed")
return
end
local path = vim.fn.FugitiveReal(name)
local commit = vim.fn.FugitiveParse(name)[1]:match('([^:]+):.*')
if commit == '0' then
-- '0' means the index so clear commit so we attach normally
commit = nil
end
return path, commit
end
local function parse_gitsigns_uri(name: string): string, string
-- TODO(lewis6991): Support submodules
local _, _, root_path, commit, rel_path =
name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]])
if commit == ':0' then
-- ':0' means the index so clear commit so we attach normally
commit = nil
end
if root_path then
name = root_path .. '/' .. rel_path
end
return name, commit
end
local function get_buf_path(bufnr: integer): string, string
local file =
uv.fs_realpath(api.nvim_buf_get_name(bufnr))
or
api.nvim_buf_call(bufnr, function(): string
return vim.fn.expand('%:p')
end)
if not vim.wo.diff then
if vim.startswith(file, 'fugitive://') then
local path, commit = parse_fugitive_uri(file)
dprintf("Fugitive buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
if vim.startswith(file, 'gitsigns://') then
local path, commit = parse_gitsigns_uri(file)
dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
end
return file
end
local function on_lines(_, bufnr: integer, _, first: integer, last_orig: integer, last_new: integer, byte_count: integer): boolean
if first == last_orig and last_orig == last_new and byte_count == 0 then
-- on_lines can be called twice for undo events; ignore the second
-- call which indicates no changes.
return
end
return manager.on_lines(bufnr, first, last_orig, last_new)
end
local function on_reload(_, bufnr: integer)
local __FUNC__ = 'on_reload'
dprint('Reload')
manager.update_debounced(bufnr)
end
local function on_detach(_, bufnr: integer)
M.detach(bufnr, true)
end
local function on_attach_pre(bufnr: integer): string, string
local gitdir, toplevel: string, string
if config._on_attach_pre then
local res: any = async.wrap(config._on_attach_pre, 2)(bufnr)
dprintf('ran on_attach_pre with result %s', vim.inspect(res))
if res is table then
if type(res.gitdir) == 'string' then
gitdir = res.gitdir as string
end
if type(res.toplevel) == 'string' then
toplevel = res.toplevel as string
end
end
end
return gitdir, toplevel
end
local function try_worktrees(_bufnr: integer, file: string, encoding: string): git.Obj
if not config.worktrees then
return
end
for _, wt in ipairs(config.worktrees) do
local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel)
if git_obj and git_obj.object_name then
dprintf('Using worktree %s', vim.inspect(wt))
return git_obj
end
end
end
local done_setup = false
function M._setup()
if done_setup then
return
end
done_setup = true
manager.setup()
hl.setup_highlights()
api.nvim_create_autocmd('ColorScheme', {
group = 'gitsigns',
callback = hl.setup_highlights
})
api.nvim_create_autocmd('OptionSet', {
group = 'gitsigns',
pattern = 'fileformat',
callback = function()
require('gitsigns.actions').refresh()
end}
)
-- vimpgrep creates and deletes lots of buffers so attaching to each one will
-- waste lots of resource and even slow down vimgrep.
api.nvim_create_autocmd('QuickFixCmdPre', {
group = 'gitsigns',
pattern ='*vimgrep*',
callback = function()
vimgrep_running = true
end
})
api.nvim_create_autocmd('QuickFixCmdPost', {
group = 'gitsigns',
pattern ='*vimgrep*',
callback = function()
vimgrep_running = false
end
})
require('gitsigns.current_line_blame').setup()
api.nvim_create_autocmd('VimLeavePre' , {
group = 'gitsigns',
callback = M.detach_all
})
end
-- Ensure attaches cannot be interleaved.
-- Since attaches are asynchronous we need to make sure an attach isn't
-- performed whilst another one is in progress.
local attach_throttled = throttle_by_id(function(cbuf: integer, ctx: GitContext, aucmd: string)
local __FUNC__ = 'attach'
M._setup()
if vimgrep_running then
dprint('attaching is disabled')
return
end
if cache[cbuf] then
dprint('Already attached')
return
end
if aucmd then
dprintf('Attaching (trigger=%s)', aucmd)
else
dprint('Attaching')
end
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Non-loaded buffer')
return
end
local encoding = vim.bo[cbuf].fileencoding
if encoding == '' then
encoding = 'utf-8'
end
local file: string
local commit: string
local gitdir_oap: string
local toplevel_oap: string
if ctx then
gitdir_oap = ctx.gitdir
toplevel_oap = ctx.toplevel
file = ctx.toplevel .. util.path_sep .. ctx.file
commit = ctx.commit
else
if api.nvim_buf_line_count(cbuf) > config.max_file_length then
dprint('Exceeds max_file_length')
return
end
if vim.bo[cbuf].buftype ~= '' then
dprint('Non-normal buffer')
return
end
file, commit = get_buf_path(cbuf)
local file_dir = util.dirname(file)
if not file_dir or not util.path_exists(file_dir) then
dprint('Not a path')
return
end
gitdir_oap, toplevel_oap = on_attach_pre(cbuf)
end
local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap)
if not git_obj and not ctx then
git_obj = try_worktrees(cbuf, file, encoding)
async.scheduler()
end
if not git_obj then
dprint('Empty git obj')
return
end
local repo = git_obj.repo
async.scheduler()
Status:update(cbuf, {
head = repo.abbrev_head,
root = repo.toplevel,
gitdir = repo.gitdir,
})
if vim.startswith(file, repo.gitdir..util.path_sep) then
dprint('In non-standard git dir')
return
end
if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then
dprint('Not a file')
return
end
if not git_obj.relpath then
dprint('Cannot resolve file in repo')
return
end
if not config.attach_to_untracked and git_obj.object_name == nil then
dprint('File is untracked')
return
end
-- On windows os.tmpname() crashes in callback threads so initialise this
-- variable on the main thread.
async.scheduler()
if config.on_attach and config.on_attach(cbuf) == false then
dprint('User on_attach() returned false')
return
end
cache[cbuf] = CacheEntry.new {
base = ctx and ctx.base or config.base,
file = file,
commit = commit,
gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir),
git_obj = git_obj
}
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Un-loaded buffer')
return
end
-- Make sure to attach before the first update (which is async) so we pick up
-- changes from BufReadCmd.
api.nvim_buf_attach(cbuf, false, {
on_lines = on_lines,
on_reload = on_reload,
on_detach = on_detach
})
-- Initial update
manager.update(cbuf, cache[cbuf])
if config.keymaps and not vim.tbl_isempty(config.keymaps) then
require('gitsigns.mappings')(config.keymaps as {string:any}, cbuf)
end
end)
--- Detach Gitsigns from all buffers it is attached to.
function M.detach_all()
for k, _ in pairs(cache as {integer:CacheEntry}) do
M.detach(k)
end
end
--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not
--- provided then the current buffer is used.
---
--- Parameters: ~
--- {bufnr} (number): Buffer number
function M.detach(bufnr: integer, _keep_signs: boolean)
-- When this is called interactively (with no arguments) we want to remove all
-- the signs, however if called via a detach event (due to nvim_buf_attach)
-- then we don't want to clear the signs in case the buffer is just being
-- updated due to the file externally changing. When this happens a detach and
-- attach event happen in sequence and so we keep the old signs to stop the
-- sign column width moving about between updates.
bufnr = bufnr or api.nvim_get_current_buf()
dprint('Detached')
local bcache = cache[bufnr]
if not bcache then
dprint('Cache was nil')
return
end
manager.detach(bufnr, _keep_signs)
-- Clear status variables
Status:clear(bufnr)
cache:destroy(bufnr)
end
--- Attach Gitsigns to the buffer.
---
--- Attributes: ~
--- {async}
---
--- Parameters: ~
--- {bufnr} (number): Buffer number
--- {ctx} (table|nil):
--- Git context data that may optionally be used to attach to any
--- buffer that represents a real git object.
--- • {file}: (string)
--- Path to the file represented by the buffer, relative to the
--- top-level.
--- • {toplevel}: (string)
--- Path to the top-level of the parent git repository.
--- • {gitdir}: (string)
--- Path to the git directory of the parent git repository
--- (typically the ".git/" directory).
--- • {commit}: (string)
--- The git revision that the file belongs to.
--- • {base}: (string|nil)
--- The git revision that the file should be compared to.
M.attach = void(function(bufnr: integer, ctx: GitContext, _trigger: string)
attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger)
end)
return M

View file

@ -1,101 +0,0 @@
local Hunk = require("gitsigns.hunks").Hunk
local GitObj = require('gitsigns.git').Obj
local config = require('gitsigns.config').config
local record M
record CacheEntry
file : string
base : string
compare_text : {string}
hunks : {Hunk}
force_next_update: boolean
compare_text_head : {string}
hunks_staged : {Hunk}
staged_diffs : {Hunk}
gitdir_watcher : vim.loop.FSPollObj -- Timer object watching the gitdir
git_obj : GitObj
commit : string
get_compare_rev : function(CacheEntry, base: string): string
get_staged_compare_rev : function(CacheEntry): string
get_rev_bufname : function(CacheEntry, rev: string): string
invalidate : function(CacheEntry)
new : function(CacheEntry): CacheEntry
destroy : function(CacheEntry)
end
record CacheObj
{CacheEntry}
destroy: function(CacheObj, bufnr: integer)
end
cache: CacheObj
end
local CacheEntry = M.CacheEntry
function CacheEntry:get_compare_rev(base: string): string
base = base or self.base
if base then
return base
end
if self.commit then
-- Buffer is a fugitive commit so compare against the parent of the commit
if config._signs_staged_enable then
return self.commit
else
return string.format('%s^', self.commit)
end
end
local stage = self.git_obj.has_conflicts and 1 or 0
return string.format(':%d', stage)
end
function CacheEntry:get_staged_compare_rev(): string
return self.commit and string.format('%s^', self.commit) or 'HEAD'
end
function CacheEntry:get_rev_bufname(rev: string): string
rev = rev or self:get_compare_rev()
return string.format(
'gitsigns://%s/%s:%s',
self.git_obj.repo.gitdir,
rev,
self.git_obj.relpath
)
end
function CacheEntry:invalidate()
self.compare_text = nil
self.compare_text_head = nil
self.hunks = nil
self.hunks_staged = nil
end
function CacheEntry.new(o: CacheEntry): CacheEntry
o.staged_diffs = o.staged_diffs or {}
return setmetatable(o, {__index = CacheEntry})
end
function CacheEntry:destroy()
local w = self.gitdir_watcher
if w and not w:is_closing() then
w:close()
end
end
function M.CacheObj:destroy(bufnr: integer)
self[bufnr]:destroy()
self[bufnr] = nil
end
M.cache = setmetatable({}, {
__index = M.CacheObj,
})
return M

View file

@ -1,109 +0,0 @@
local async = require('gitsigns.async')
local void = require('gitsigns.async').void
local log = require('gitsigns.debug.log')
local dprintf = log.dprintf
local message = require'gitsigns.message'
local parse_args = require('gitsigns.cli.argparse').parse_args
local actions = require('gitsigns.actions')
local attach = require('gitsigns.attach')
local gs_debug = require('gitsigns.debug')
local sources = {
[actions] = true,
[attach] = false,
[gs_debug] = false
} as {{string:function}:boolean}
-- try to parse each argument as a lua boolean, nil or number, if fails then
-- keep argument as a string:
--
-- 'false' -> false
-- 'nil' -> nil
-- '100' -> 100
-- 'HEAD~300' -> 'HEAD~300'
local function parse_to_lua(a: string): any
if tonumber(a) then
return tonumber(a)
elseif a == 'false' or a == 'true' then
return a == 'true'
elseif a == 'nil' then
return nil
end
return a
end
local record M
run: function(params: vim.api.UserCmdParams)
end
function M.complete(arglead: string, line: string): {string}
local words = vim.split(line, '%s+')
local n: integer = #words
local matches: {string} = {}
if n == 2 then
for m, _ in pairs(sources) do
for func, _ in pairs(m as {string:function}) do
if not func:match('^[a-z]') then
-- exclude
elseif vim.startswith(func, arglead) then
table.insert(matches, func)
end
end
end
elseif n > 2 then
-- Subcommand completion
local cmp_func = actions._get_cmp_func(words[2])
if cmp_func then
return cmp_func(arglead)
end
end
return matches
end
local function print_nonnil(x: any)
if x ~= nil then
print(vim.inspect(x))
end
end
M.run = void(function(params: vim.api.UserCmdParams)
local __FUNC__ = 'cli.run'
local pos_args_raw, named_args_raw = parse_args(params.args)
local func = pos_args_raw[1]
if not func then
func = async.wrap(vim.ui.select, 3)(M.complete('', 'Gitsigns '), {})
end
local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2)) as {any}
local named_args = vim.tbl_map(parse_to_lua, named_args_raw) as {string:any}
local args = vim.tbl_extend('error', pos_args, named_args)
dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, {newline=' ', indent=''}))
local cmd_func = actions._get_cmd_func(func)
if cmd_func then
-- Action has a specialised mapping function from command form to lua
-- function
print_nonnil(cmd_func(args, params))
return
end
for m, has_named in pairs(sources) do
local f = (m as {string:any})[func]
if f is function then
-- Note functions here do not have named arguments
print_nonnil(f(unpack(pos_args), has_named and named_args or nil))
return
end
end
message.error('%s is not a valid function or action', func)
end)
return M

View file

@ -1,115 +0,0 @@
local record M
parse_args: function(x: string): {string}, {string:string|boolean}
end
local function is_char(x: string): boolean
return x:match('[^=\'"%s]') ~= nil
end
local enum ArgState
"in_arg"
"in_ws"
"in_value"
"in_quote"
"in_flag"
end
-- Return positional arguments and named arguments
function M.parse_args(x: string): {string}, {string:string|boolean}
local pos_args, named_args: {string}, {string:string|boolean} = {}, {}
local state: ArgState = 'in_arg'
local cur_arg = ''
local cur_val = ''
local cur_quote = ''
local function peek(idx: integer): string
return x:sub(idx+1, idx+1)
end
local i = 1
while i <= #x do
local ch = x:sub(i, i)
-- dprintf('L(%d)(%s): cur_arg="%s" ch="%s"', i, state, cur_arg, ch)
if state == 'in_arg' then
if is_char(ch) then
if ch == '-' and peek(i) == '-' then
state = 'in_flag'
cur_arg = ''
i = i + 1
else
cur_arg = cur_arg .. ch
end
elseif ch:match('%s') then
pos_args[#pos_args+1] = cur_arg
state = 'in_ws'
elseif ch == '=' then
cur_val = ''
local next_ch = peek(i)
if next_ch == "'" or next_ch == '"' then
cur_quote = next_ch
i = i + 1
state = 'in_quote'
else
state = 'in_value'
end
end
elseif state == 'in_flag' then
if ch:match('%s') then
named_args[cur_arg] = true
state = 'in_ws'
else
cur_arg = cur_arg .. ch
end
elseif state == 'in_ws' then
if is_char(ch) then
if ch == '-' and peek(i) == '-' then
state = 'in_flag'
cur_arg = ''
i = i + 1
else
state = 'in_arg'
cur_arg = ch
end
end
elseif state == 'in_value' then
if is_char(ch) then
cur_val = cur_val .. ch
elseif ch:match('%s') then
named_args[cur_arg] = cur_val
cur_arg = ''
state = 'in_ws'
end
elseif state == 'in_quote' then
local next_ch = peek(i)
if ch == "\\" and next_ch == cur_quote then
cur_val = cur_val .. next_ch
i = i + 1
elseif ch == cur_quote then
named_args[cur_arg] = cur_val
state = 'in_ws'
if next_ch ~= '' and not next_ch:match('%s') then
error('malformed argument: '..next_ch)
end
else
cur_val = cur_val .. ch
end
end
i = i + 1
end
if #cur_arg > 0 then
if state == 'in_arg' then
pos_args[#pos_args+1] = cur_arg
elseif state == 'in_flag' then
named_args[cur_arg] = true
elseif state == 'in_value' then
named_args[cur_arg] = cur_val
end
end
return pos_args, named_args
end
return M

View file

@ -1,963 +0,0 @@
local warn: function(string, ...: any)
do
-- this is included in gen_help.lua so don't error if requires fail
local ok, ret = pcall(require, 'gitsigns.message')
if ok then
warn = ret.warn
end
end
--- @class Gitsigns.SchemaElem
--- @field type string|string[]
--- @field deep_extend boolean
--- @field default any
--- @field deprecated boolean|{new_field:string,message:string,hard:boolean}
--- @field default_help string
--- @field description string
--- @class Gitsigns.DiffOpts
--- @field algorithm string
--- @field internal boolean
--- @field indent_heuristic boolean
--- @field vertical boolean
--- @field linematch integer
--- @class Gitsign.SignConfig
--- @field show_count boolean
--- @field hl string
--- @field text string
--- @field numhl string
--- @field linehl string
--- @field keymaps table<string,string>
--- @alias Gitsigns.SignType
--- | 'add'
--- | 'change'
--- | 'delete'
--- | 'topdelete'
--- | 'changedelete'
--- | 'untracked'
--- @alias Gitsigns.CurrentLineBlameFmtOpts { relative_time: boolean }
--- @alias Gitsigns.CurrentLineBlameFmtFun fun(_: string, _: table<string,any>, _: Gitsigns.CurrentLineBlameFmtOpts): {[1]:string,[2]:string}[]
--- @class Gitsigns.CurrentLineBlameOpts
--- @field virt_text boolean
--- @field virt_text_pos 'eol'|'overlay'|'right_align'
--- @field delay integer
--- @field ignore_whitespace boolean
--- @field virt_text_priority integer
--- @class Gitsigns.Config
--- @field debug_mode boolean
--- @field diff_opts Gitsigns.DiffOpts
--- @field base string
--- @field signs table<Gitsigns.SignType,Gitsign.SignConfig>
--- @field _signs_staged table<Gitsigns.SignType,Gitsign.SignConfig>
--- @field _signs_staged_enable boolean
--- @field count_chars table<string|integer,string>
--- @field signcolumn boolean
--- @field numhl boolean
--- @field linehl boolean
--- @field show_deleted boolean
--- @field sign_priority integer
--- @field _on_attach_pre fun(bufnr: integer, callback: fun(_: table))
--- @field on_attach fun(bufnr: integer)
--- @field watch_gitdir { enable: boolean, interval: integer, follow_files: boolean }
--- @field max_file_length integer
--- @field update_debounce integer
--- @field status_formatter fun(_: table<string,any>): string
--- @field current_line_blame boolean
--- @field current_line_blame_formatter_opts { relative_time: boolean }
--- @field current_line_blame_formatter string|Gitsigns.CurrentLineBlameFmtFun
--- @field current_line_blame_formatter_nc string|Gitsigns.CurrentLineBlameFmtFun
--- @field current_line_blame_opts Gitsigns.CurrentLineBlameOpts
--- @field preview_config table<string,any>
--- @field attach_to_untracked boolean
--- @field yadm { enable: boolean }
--- @field worktrees {toplevel: string, gitdir: string}[]
--- @field word_diff boolean
--- -- Undocumented
--- @field _refresh_staged_on_update boolean
--- @field _blame_cache boolean
--- @field _threaded_diff boolean
--- @field _inline2 boolean
--- @field _extmark_signs boolean
--- @field _git_version string
--- @field _verbose boolean
--- @field _test_mode boolean
local record SchemaElem
type: string|{string}
deep_extend: boolean
default: any
record Deprecated
new_field: string
message: string
hard: boolean
end
deprecated: Deprecated|boolean
default_help: string
description: string
end
local record M
record Config
debug_mode: boolean
record DiffOpts
algorithm: string
internal: boolean
indent_heuristic: boolean
vertical: boolean
linematch: integer
end
diff_opts: DiffOpts
base: string
record SignConfig
show_count: boolean
hl: string
text: string
numhl: string
linehl: string
keymaps: {string:string}
end
enum SignType
'add'
'change'
'delete'
'topdelete'
'changedelete'
'untracked'
end
type SignsConfig = {SignType: SignConfig}
signs: SignsConfig
_signs_staged: SignsConfig
_signs_staged_enable: boolean
count_chars: {string|integer:string}
signcolumn: boolean
numhl: boolean
linehl: boolean
show_deleted: boolean
sign_priority: integer
keymaps: {string:any}
_on_attach_pre: function(bufnr: integer, callback: function(table))
on_attach: function(bufnr: integer)
record watch_gitdir
enable: boolean
interval: integer
follow_files: boolean
end
max_file_length: integer
update_debounce: integer
status_formatter: function({string:any}): string
current_line_blame: boolean
record current_line_blame_formatter_opts
relative_time: boolean
end
current_line_blame_formatter: string|function(string, {string:any}, current_line_blame_formatter_opts): {{string,string}}
current_line_blame_formatter_nc: string|function(string, {string:any}, current_line_blame_formatter_opts): {{string,string}}
record current_line_blame_opts
virt_text: boolean
enum VirtTextPos
'eol'
'overlay'
'right_align'
end
virt_text_pos: VirtTextPos
delay: integer
ignore_whitespace: boolean
virt_text_priority: integer
end
preview_config: {string:any}
attach_to_untracked: boolean
record yadm
enable: boolean
end
trouble: boolean
record Worktree
toplevel: string
gitdir: string
end
worktrees: {Worktree}
-- Undocumented
word_diff: boolean
_refresh_staged_on_update: boolean
_blame_cache: boolean
_threaded_diff: boolean
_inline2: boolean
_extmark_signs: boolean
_git_version: string
_verbose: boolean
_test_mode: boolean
end
schema: {string:SchemaElem}
config: Config
end
--- @type Gitsigns.Config
M.config = {}
--- @type table<string,Gitsigns.SchemaElem>
M.schema = {
signs = {
type = 'table',
deep_extend = true,
default = {
add = {hl = 'GitSignsAdd', text = '', numhl = 'GitSignsAddNr', linehl = 'GitSignsAddLn' },
change = {hl = 'GitSignsChange', text = '', numhl = 'GitSignsChangeNr', linehl = 'GitSignsChangeLn' },
delete = {hl = 'GitSignsDelete', text = '', numhl = 'GitSignsDeleteNr', linehl = 'GitSignsDeleteLn' },
topdelete = {hl = 'GitSignsTopdelete', text = '', numhl = 'GitSignsTopdeleteNr', linehl = 'GitSignsTopdeleteLn' },
changedelete = {hl = 'GitSignsChangedelete', text = '~', numhl = 'GitSignsChangedeleteNr', linehl = 'GitSignsChangedeleteLn' },
untracked = {hl = 'GitSignsUntracked', text = '', numhl = 'GitSignsUntrackedNr', linehl = 'GitSignsUntrackedLn' },
},
default_help = [[{
add = { text = '' },
change = { text = '' },
delete = { text = '' },
topdelete = { text = '' },
changedelete = { text = '~' },
untracked = { text = '' },
}]],
description = [[
Configuration for signs:
`text` specifies the character to use for the sign.
`show_count` to enable showing count of hunk, e.g. number of deleted
lines.
The highlights `GitSigns[kind][type]` is used for each kind of sign. E.g.
'add' signs uses the highlights:
`GitSignsAdd` (for normal text signs)
`GitSignsAddNr` (for signs when `config.numhl == true`)
`GitSignsAddLn `(for signs when `config.linehl == true`)
See |gitsigns-highlight-groups|.
]]
},
_signs_staged = {
type = 'table',
deep_extend = true,
default = {
add = {hl = 'GitSignsStagedAdd' , text = '', numhl='GitSignsStagedAddNr' , linehl='GitSignsStagedAddLn' },
change = {hl = 'GitSignsStagedChange' , text = '', numhl='GitSignsStagedChangeNr' , linehl='GitSignsStagedChangeLn' },
delete = {hl = 'GitSignsStagedDelete' , text = '', numhl='GitSignsStagedDeleteNr' , linehl='GitSignsStagedDeleteLn' },
topdelete = {hl = 'GitSignsStagedTopdelete' , text = '', numhl='GitSignsStagedTopdeleteNr' , linehl='GitSignsStagedTopdeleteLn' },
changedelete = {hl = 'GitSignsStagedChangedelete', text = '~', numhl='GitSignsStagedChangedeleteNr', linehl='GitSignsStagedChangedeleteLn' },
},
default_help = [[{
add = { text = '' },
change = { text = '' },
delete = { text = '' },
topdelete = { text = '' },
changedelete = { text = '~' },
}]],
description = [[
Configuration for signs of staged hunks.
See |gitsigns-config-signs|.
]]
},
_signs_staged_enable = {
type = 'boolean',
default = false,
description = [[
Show signs for staged hunks.
When enabled the signs defined in |git-config-signs_staged|` are used.
]]
},
keymaps = {
deprecated = {
message = "config.keymaps is now deprecated. Please define mappings in config.on_attach() instead."
},
type = 'table',
default = {},
description = [[
Keymaps to set up when attaching to a buffer.
Each key in the table defines the mode and key (whitespace delimited)
for the mapping and the value defines what the key maps to. The value
can be a table which can contain keys matching the options defined in
|map-arguments| which are: `expr`, `noremap`, `nowait`, `script`, `silent`
and `unique`. These options can also be used in the top level of the
table to define default options for all mappings.
Since this field is not extended (unlike |gitsigns-config-signs|),
mappings defined in this field can be disabled by setting the whole field
to `{}`, and |gitsigns-config-on_attach| can instead be used to define
mappings.
]]
},
worktrees = {
type = 'table',
default = nil,
description = [[
Detached working trees.
Array of tables with the keys `gitdir` and `toplevel`.
If normal attaching fails, then each entry in the table is attempted
with the work tree details set.
Example: >
worktrees = {
{
toplevel = vim.env.HOME,
gitdir = vim.env.HOME .. '/projects/dotfiles/.git'
}
}
]]
},
_on_attach_pre = {
type = 'function',
default = nil,
description = [[
Asynchronous hook called before attaching to a buffer. Mainly used to
configure detached worktrees.
This callback must call its callback argument. The callback argument can
accept an optional table argument with the keys: 'gitdir' and 'toplevel'.
Example: >
on_attach_pre = function(bufnr, callback)
...
callback {
gitdir = ...,
toplevel = ...
}
end
<
]]
},
on_attach = {
type = 'function',
default = nil,
description = [[
Callback called when attaching to a buffer. Mainly used to setup keymaps
when `config.keymaps` is empty. The buffer number is passed as the first
argument.
This callback can return `false` to prevent attaching to the buffer.
Example: >
on_attach = function(bufnr)
if vim.api.nvim_buf_get_name(bufnr):match(<PATTERN>) then
-- Don't attach to specific buffers whose name matches a pattern
return false
end
-- Setup keymaps
vim.api.nvim_buf_set_keymap(bufnr, 'n', 'hs', '<cmd>lua require"gitsigns".stage_hunk()<CR>', {})
... -- More keymaps
end
<
]]
},
watch_gitdir = {
type = 'table',
deep_extend = true,
default = {
enable = true,
interval = 1000,
follow_files = true
},
description = [[
When opening a file, a libuv watcher is placed on the respective
`.git` directory to detect when changes happen to use as a trigger to
update signs.
Fields: ~
`enable`:
Whether the watcher is enabled.
`interval`:
Interval the watcher waits between polls of the gitdir in milliseconds.
`follow_files`:
If a file is moved with `git mv`, switch the buffer to the new location.
]]
},
sign_priority = {
type = 'number',
default = 6,
description = [[
Priority to use for signs.
]]
},
signcolumn = {
type = 'boolean',
default = true,
description = [[
Enable/disable symbols in the sign column.
When enabled the highlights defined in `signs.*.hl` and symbols defined
in `signs.*.text` are used.
]]
},
numhl = {
type = 'boolean',
default = false,
description = [[
Enable/disable line number highlights.
When enabled the highlights defined in `signs.*.numhl` are used. If
the highlight group does not exist, then it is automatically defined
and linked to the corresponding highlight group in `signs.*.hl`.
]]
},
linehl = {
type = 'boolean',
default = false,
description = [[
Enable/disable line highlights.
When enabled the highlights defined in `signs.*.linehl` are used. If
the highlight group does not exist, then it is automatically defined
and linked to the corresponding highlight group in `signs.*.hl`.
]]
},
show_deleted = {
type = 'boolean',
default = false,
description = [[
Show the old version of hunks inline in the buffer (via virtual lines).
Note: Virtual lines currently use the highlight `GitSignsDeleteVirtLn`.
]]
},
diff_opts = {
type = 'table',
deep_extend = true,
default = function(): {string:any}
local r: M.Config.DiffOpts = {
algorithm = 'myers',
internal = false,
indent_heuristic = false,
vertical = true,
linematch = nil
}
for _, o in ipairs(vim.opt.diffopt:get()) do
if o == 'indent-heuristic' then
r.indent_heuristic = true
elseif o == 'internal' then
if vim.diff then
r.internal = true
end
elseif o == 'horizontal' then
r.vertical = false
elseif vim.startswith(o, 'algorithm:') then
r.algorithm = string.sub(o, ('algorithm:'):len() + 1)
elseif vim.startswith(o, 'linematch:') then
r.linematch = tonumber(string.sub(o, ('linematch:'):len() + 1)) as integer
end
end
return r
end,
default_help = "derived from 'diffopt'",
description = [[
Diff options.
Fields: ~
algorithm: string
Diff algorithm to use. Values:
"myers" the default algorithm
"minimal" spend extra time to generate the
smallest possible diff
"patience" patience diff algorithm
"histogram" histogram diff algorithm
internal: boolean
Use Neovim's built in xdiff library for running diffs.
indent_heuristic: boolean
Use the indent heuristic for the internal
diff library.
vertical: boolean
Start diff mode with vertical splits.
linematch: integer
Enable second-stage diff on hunks to align lines.
Requires `internal=true`.
]]
},
base = {
type = 'string',
default = nil,
default_help = 'index',
description = [[
The object/revision to diff against.
See |gitsigns-revision|.
]]
},
count_chars = {
type = 'table',
default = {
[1] = '1', -- '₁',
[2] = '2', -- '₂',
[3] = '3', -- '₃',
[4] = '4', -- '₄',
[5] = '5', -- '₅',
[6] = '6', -- '₆',
[7] = '7', -- '₇',
[8] = '8', -- '₈',
[9] = '9', -- '₉',
['+'] = '>', -- '₊',
},
description = [[
The count characters used when `signs.*.show_count` is enabled. The
`+` entry is used as a fallback. With the default, any count outside
of 1-9 uses the `>` character in the sign.
Possible use cases for this field:
to specify unicode characters for the counts instead of 1-9.
to define characters to be used for counts greater than 9.
]]
},
status_formatter = {
type = 'function',
default = function(status: {string:number}): string
local added, changed, removed = status.added, status.changed, status.removed
local status_txt = {}
if added and added > 0 then table.insert(status_txt, '+'..added ) end
if changed and changed > 0 then table.insert(status_txt, '~'..changed) end
if removed and removed > 0 then table.insert(status_txt, '-'..removed) end
return table.concat(status_txt, ' ')
end,
default_help = [[function(status)
local added, changed, removed = status.added, status.changed, status.removed
local status_txt = {}
if added and added > 0 then table.insert(status_txt, '+'..added ) end
if changed and changed > 0 then table.insert(status_txt, '~'..changed) end
if removed and removed > 0 then table.insert(status_txt, '-'..removed) end
return table.concat(status_txt, ' ')
end]],
description = [[
Function used to format `b:gitsigns_status`.
]]
},
max_file_length = {
type = 'number',
default = 40000,
description = [[
Max file length (in lines) to attach to.
]]
},
preview_config = {
type = 'table',
deep_extend = true,
default = {
border = 'single',
style = 'minimal',
relative = 'cursor',
row = 0,
col = 1
},
description = [[
Option overrides for the Gitsigns preview window. Table is passed directly
to `nvim_open_win`.
]]
},
attach_to_untracked = {
type = 'boolean',
default = true,
description = [[
Attach to untracked files.
]]
},
update_debounce = {
type = 'number',
default = 100,
description = [[
Debounce time for updates (in milliseconds).
]]
},
current_line_blame = {
type = 'boolean',
default = false,
description = [[
Adds an unobtrusive and customisable blame annotation at the end of
the current line.
The highlight group used for the text is `GitSignsCurrentLineBlame`.
]]
},
current_line_blame_opts = {
type = 'table',
deep_extend = true,
default = {
virt_text = true,
virt_text_pos = 'eol',
virt_text_priority = 100,
delay = 1000
},
description = [[
Options for the current line blame annotation.
Fields: ~
virt_text: boolean
Whether to show a virtual text blame annotation.
virt_text_pos: string
Blame annotation position. Available values:
`eol` Right after eol character.
`overlay` Display over the specified column, without
shifting the underlying text.
`right_align` Display right aligned in the window.
delay: integer
Sets the delay (in milliseconds) before blame virtual text is
displayed.
ignore_whitespace: boolean
Ignore whitespace when running blame.
virt_text_priority: integer
Priority of virtual text.
]]
},
current_line_blame_formatter_opts = {
type = 'table',
deep_extend = true,
deprecated = true,
default = {
relative_time = false
},
description = [[
Options for the current line blame annotation formatter.
Fields: ~
relative_time: boolean
]]
},
current_line_blame_formatter = {
type = {'string', 'function'},
default = ' <author>, <author_time> - <summary> ',
description = [[
String or function used to format the virtual text of
|gitsigns-config-current_line_blame|.
When a string, accepts the following format specifiers:
`<abbrev_sha>`
`<orig_lnum>`
`<final_lnum>`
`<author>`
`<author_mail>`
`<author_time>` or `<author_time:FORMAT>`
`<author_tz>`
`<committer>`
`<committer_mail>`
`<committer_time>` or `<committer_time:FORMAT>`
`<committer_tz>`
`<summary>`
`<previous>`
`<filename>`
For `<author_time:FORMAT>` and `<committer_time:FORMAT>`, `FORMAT` can
be any valid date format that is accepted by `os.date()` with the
addition of `%R` (defaults to `%Y-%m-%d`):
`%a` abbreviated weekday name (e.g., Wed)
`%A` full weekday name (e.g., Wednesday)
`%b` abbreviated month name (e.g., Sep)
`%B` full month name (e.g., September)
`%c` date and time (e.g., 09/16/98 23:48:10)
`%d` day of the month (16) [01-31]
`%H` hour, using a 24-hour clock (23) [00-23]
`%I` hour, using a 12-hour clock (11) [01-12]
`%M` minute (48) [00-59]
`%m` month (09) [01-12]
`%p` either "am" or "pm" (pm)
`%S` second (10) [00-61]
`%w` weekday (3) [0-6 = Sunday-Saturday]
`%x` date (e.g., 09/16/98)
`%X` time (e.g., 23:48:10)
`%Y` full year (1998)
`%y` two-digit year (98) [00-99]
`%%` the character `%´
`%R` relative (e.g., 4 months ago)
When a function:
Parameters: ~
{name} Git user name returned from `git config user.name` .
{blame_info} Table with the following keys:
`abbrev_sha`: string
`orig_lnum`: integer
`final_lnum`: integer
`author`: string
`author_mail`: string
`author_time`: integer
`author_tz`: string
`committer`: string
`committer_mail`: string
`committer_time`: integer
`committer_tz`: string
`summary`: string
`previous`: string
`filename`: string
Note that the keys map onto the output of:
`git blame --line-porcelain`
{opts} Passed directly from
|gitsigns-config-current_line_blame_formatter_opts|.
Return: ~
The result of this function is passed directly to the `opts.virt_text`
field of |nvim_buf_set_extmark| and thus must be a list of
[text, highlight] tuples.
]]
},
current_line_blame_formatter_nc = {
type = {'string', 'function'},
default = ' <author>',
description = [[
String or function used to format the virtual text of
|gitsigns-config-current_line_blame| for lines that aren't committed.
See |gitsigns-config-current_line_blame_formatter| for more information.
]]
},
trouble = {
type = 'boolean',
default = function(): boolean
local has_trouble = pcall(require, 'trouble')
return has_trouble
end,
default_help = "true if installed",
description = [[
When using setqflist() or setloclist(), open Trouble instead of the
quickfix/location list window.
]]
},
yadm = {
type = 'table',
default = { enable = false },
description = [[
yadm configuration.
]]
},
_git_version = {
type = 'string',
default = 'auto',
description = [[
Version of git available. Set to 'auto' to automatically detect.
]]
},
_verbose = {
type = 'boolean',
default = false,
description = [[
More verbose debug message. Requires debug_mode=true.
]]
},
_test_mode = {
type = 'boolean',
default = false,
},
word_diff = {
type = 'boolean',
default = false,
description = [[
Highlight intra-line word differences in the buffer.
Requires `config.diff_opts.internal = true` .
Uses the highlights:
For word diff in previews:
`GitSignsAddInline`
`GitSignsChangeInline`
`GitSignsDeleteInline`
For word diff in buffer:
`GitSignsAddLnInline`
`GitSignsChangeLnInline`
`GitSignsDeleteLnInline`
For word diff in virtual lines (e.g. show_deleted):
`GitSignsAddVirtLnInline`
`GitSignsChangeVirtLnInline`
`GitSignsDeleteVirtLnInline`
]]
},
_refresh_staged_on_update = {
type = 'boolean',
default = false,
description = [[
Always refresh the staged file on each update. Disabling this will cause
the staged file to only be refreshed when an update to the index is
detected.
]]
},
_blame_cache = {
type = 'boolean',
default = true,
description = [[
Cache blame results for current_line_blame
]]
},
_threaded_diff = {
type = 'boolean',
default = false,
description = [[
Run diffs on a separate thread
]]
},
_inline2 = {
type = 'boolean',
default = false,
description = [[
Enable enhanced version of preview_hunk_inline()
]]
},
_extmark_signs = {
type = 'boolean',
default = false,
description = [[
Use extmarks for placing signs.
]]
},
debug_mode = {
type = 'boolean',
default = false,
description = [[
Enables debug logging and makes the following functions
available: `dump_cache`, `debug_messages`, `clear_debug`.
]]
},
}
warn = function(s: string, ...: any)
vim.notify(s:format(...), vim.log.levels.WARN, {title = 'gitsigns'})
end
--- @param config Gitsigns.Config
local function validate_config(config: {string:any})
for k, v in pairs(config) do
local kschema = M.schema[k]
if kschema == nil then
warn("gitsigns: Ignoring invalid configuration field '%s'", k)
elseif kschema.type then
if type(kschema.type) == 'string' then
vim.validate {
[k] = { v, kschema.type } as {any};
}
end
end
end
end
local function resolve_default(v: SchemaElem): any
if type(v.default) == 'function' and v.type ~= 'function' then
return (v.default as function)()
else
return v.default
end
end
local function handle_deprecated(cfg: {string:any})
for k, v in pairs(M.schema) do
local dep = v.deprecated
if dep and cfg[k] ~= nil then
if dep is SchemaElem.Deprecated then
if dep.new_field then
local opts_key, field = dep.new_field:match('(.*)%.(.*)')
if opts_key and field then
-- Field moved to an options table
local opts = (cfg[opts_key] or {}) as {string:any}
opts[field] = cfg[k]
cfg[opts_key] = opts
else
-- Field renamed
cfg[dep.new_field] = cfg[k]
end
end
if dep.hard then
if dep.message then
warn(dep.message)
elseif dep.new_field then
warn('%s is now deprecated, please use %s', k, dep.new_field)
else
warn('%s is now deprecated; ignoring', k)
end
end
end
end
end
end
--- @param user_config Gitsigns.Config
function M.build(user_config: {string:any})
user_config = user_config or {}
handle_deprecated(user_config)
validate_config(user_config)
local config = M.config as {string:any}
for k, v in pairs(M.schema) do
if user_config[k] ~= nil then
if v.deep_extend then
local d = resolve_default(v)
config[k] = vim.tbl_deep_extend('force', d as table, user_config[k] as table)
else
config[k] = user_config[k]
end
else
config[k] = resolve_default(v)
end
end
end
return M

View file

@ -1,214 +0,0 @@
local a = require('gitsigns.async')
local wrap = a.wrap
local void = a.void
local scheduler = a.scheduler
local cache = require('gitsigns.cache').cache
local config = require('gitsigns.config').config
local BlameInfo = require('gitsigns.git').BlameInfo
local util = require('gitsigns.util')
local uv = require('gitsigns.uv')
local api = vim.api
local current_buf = api.nvim_get_current_buf
local namespace = api.nvim_create_namespace('gitsigns_blame')
local timer = uv.new_timer(true)
local record M
setup: function()
end
local wait_timer = wrap(vim.loop.timer_start, 4)
local function set_extmark(bufnr: integer, row: integer, opts: {string:any})
opts = opts or {}
opts.id = 1
api.nvim_buf_set_extmark(bufnr, namespace, row-1, 0, opts)
end
local function get_extmark(bufnr: integer): integer
local pos = api.nvim_buf_get_extmark_by_id(bufnr, namespace, 1, {}) as {integer, integer}
if pos[1] then
return pos[1] + 1
end
return
end
local function reset(bufnr: integer)
bufnr = bufnr or current_buf()
if not api.nvim_buf_is_valid(bufnr) then
return
end
api.nvim_buf_del_extmark(bufnr, namespace, 1)
vim.b[bufnr].gitsigns_blame_line_dict = nil
end
-- TODO: expose as config
local max_cache_size = 1000
local record BlameCache
record Elem
tick: integer
cache: {integer:BlameInfo}
size: integer
end
contents: {integer:Elem}
end
BlameCache.contents = {}
function BlameCache:add(bufnr: integer, lnum: integer, x: BlameInfo)
if not config._blame_cache then return end
local scache = self.contents[bufnr]
if scache.size <= max_cache_size then
scache.cache[lnum] = x
scache.size = scache.size + 1
end
end
function BlameCache:get(bufnr: integer, lnum: integer): BlameInfo
if not config._blame_cache then return end
-- init and invalidate
local tick = vim.b[bufnr].changedtick
if not self.contents[bufnr] or self.contents[bufnr].tick ~= tick then
self.contents[bufnr] = {tick = tick, cache = {}, size = 0}
end
return self.contents[bufnr].cache[lnum]
end
local function expand_blame_format(fmt: string, name: string, info: BlameInfo): string
if info.author == name then
info.author = 'You'
end
return util.expand_format(fmt, info, config.current_line_blame_formatter_opts.relative_time)
end
local function flatten_virt_text(virt_text: {{string, string}}): string
local res = {}
for _, part in ipairs(virt_text) do
res[#res+1] = part[1]
end
return table.concat(res)
end
-- Update function, must be called in async context
local update = void(function()
local bufnr = current_buf()
local lnum = api.nvim_win_get_cursor(0)[1]
local old_lnum = get_extmark(bufnr)
if old_lnum and lnum == old_lnum and BlameCache:get(bufnr, lnum) then
-- Don't update if on the same line and we already have results
return
end
if api.nvim_get_mode().mode == 'i' then
reset(bufnr)
return
end
-- Set an empty extmark to save the line number.
-- This will also clear virt_text.
-- Only do this if there was already an extmark to avoid clearing the intro
-- text.
if get_extmark(bufnr) then
reset(bufnr)
set_extmark(bufnr, lnum)
end
-- Can't show extmarks on folded lines so skip
if vim.fn.foldclosed(lnum) ~= -1 then
return
end
local opts = config.current_line_blame_opts
-- Note because the same timer is re-used, this call has a debouncing effect.
wait_timer(timer, opts.delay, 0)
scheduler()
local bcache = cache[bufnr]
if not bcache or not bcache.git_obj.object_name then
return
end
local result = BlameCache:get(bufnr, lnum)
if not result then
local buftext = util.buf_lines(bufnr)
result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace)
BlameCache:add(bufnr, lnum, result)
scheduler()
end
local lnum1 = api.nvim_win_get_cursor(0)[1]
if bufnr == current_buf() and lnum ~= lnum1 then
-- Cursor has moved during events; abort
return
end
if not api.nvim_buf_is_loaded(bufnr) then
-- Buffer is no longer loaded; abort
return
end
vim.b[bufnr].gitsigns_blame_line_dict = result
if result then
local virt_text: {{string, string}}
local clb_formatter = result.author == 'Not Committed Yet' and
config.current_line_blame_formatter_nc or
config.current_line_blame_formatter
if clb_formatter is string then
virt_text = {{
expand_blame_format(clb_formatter, bcache.git_obj.repo.username, result),
'GitSignsCurrentLineBlame'
}}
else -- function
virt_text = clb_formatter(
bcache.git_obj.repo.username,
result,
config.current_line_blame_formatter_opts
)
end
vim.b[bufnr].gitsigns_blame_line = flatten_virt_text(virt_text)
if opts.virt_text then
set_extmark(bufnr, lnum, {
virt_text = virt_text,
virt_text_pos = opts.virt_text_pos,
priority = opts.virt_text_priority,
hl_mode = 'combine',
})
end
end
end)
M.setup = function()
local group = api.nvim_create_augroup('gitsigns_blame', {})
for k, _ in pairs(cache as {integer:any}) do
reset(k)
end
if config.current_line_blame then
api.nvim_create_autocmd({'FocusGained', 'BufEnter', 'CursorMoved', 'CursorMovedI'}, {
group = group, callback = function() update() end
})
api.nvim_create_autocmd({'InsertEnter', 'FocusLost', 'BufLeave'}, {
group = group, callback = function() reset() end
})
-- Call via vim.schedule to avoid the debounce timer killing the async
-- coroutine
vim.schedule(update)
end
end
return M

View file

@ -1,86 +0,0 @@
local uv = require('gitsigns.uv')
local record M
throttle_by_id : function<F>(fn: F, schedule: boolean): F
debounce_trailing : function<F>(ms: number, fn: F): F
end
--- Debounces a function on the trailing edge.
---
--- @generic F: function
--- @param ms number Timeout in ms
--- @param fn F Function to debounce
--- @return F Debounced function.
function M.debounce_trailing(ms: number, fn: function): function
local timer = uv.new_timer(true)
return function(...)
local argv = {...}
timer:start(ms, 0, function()
timer:stop()
fn(unpack(argv))
end)
end
end
--- Throttles a function on the leading edge.
---
--- @generic F: function
--- @param ms number Timeout in ms
--- @param fn F Function to throttle
--- @return F throttled function.
function M.throttle_leading(ms: number, fn: function): function
local timer = uv.new_timer(true)
local running = false
return function(...)
if not running then
timer:start(ms, 0, function()
running = false
timer:stop()
end)
running = true
fn(...)
end
end
end
--- Throttles a function using the first argument as an ID
---
--- If function is already running then the function will be scheduled to run
--- again once the running call has finished.
---
--- fn#1 _/‾\__/‾\_/‾\_____________________________
--- throttled#1 _/‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\/‾‾‾‾‾‾‾‾‾‾\____________
--
--- fn#2 ______/‾\___________/‾\___________________
--- throttled#2 ______/‾‾‾‾‾‾‾‾‾‾\__/‾‾‾‾‾‾‾‾‾‾\__________
---
---
--- @generic F: function
--- @param fn F Function to throttle
--- @param schedule boolean
--- @return F throttled function.
function M.throttle_by_id(fn: function, schedule: boolean): function
local scheduled: {any:boolean} = {} --- @type table<any,boolean>
local running: {any:boolean} = {} --- @type table<any,boolean>
return function(id: any, ...)
if scheduled[id] then
-- If fn is already scheduled, then drop
return
end
if not running[id] or schedule then
scheduled[id] = true
end
if running[id] then
return
end
while scheduled[id] do
scheduled[id] = nil
running[id] = true
fn(id, ...)
running[id] = nil
end
end
end
return M

View file

@ -1,50 +0,0 @@
local log = require'gitsigns.debug.log'
local M = {}
--- @param raw_item any
--- @param path string[]
--- @return any
local function process(raw_item: any, path: {string}): any
if path[#path] == vim.inspect.METATABLE then
return nil
elseif raw_item is function then
return nil
elseif raw_item is table then
local key = path[#path]
if key == 'compare_text' or key == 'compare_text_head' then
local item = raw_item as {string}
return { '...', length=#item, head=item[1] }
elseif not vim.tbl_isempty(raw_item) and key == 'staged_diffs' then
return { '...', length=#vim.tbl_keys(raw_item) }
end
end
return raw_item
end
--- @return any
function M.dump_cache(): any
-- TODO(lewis6991): hack: use package.loaded to avoid circular deps
local cache = (require('gitsigns.cache') as table).cache
local text = vim.inspect(cache, { process = process })
vim.api.nvim_echo({{text}}, false, {})
return cache
end
--- @param noecho boolean
--- @return string[]
function M.debug_messages(noecho: boolean): {string}
if noecho then
return log.messages
else
for _, m in ipairs(log.messages) do
vim.api.nvim_echo({{m}}, false, {})
end
end
end
function M.clear_debug()
log.messages = {}
end
return M

View file

@ -1,109 +0,0 @@
local M = {
debug_mode = false,
verbose = false,
messages: {string} = {},
}
local function getvarvalue(name: string, lvl: integer): any
lvl = lvl + 1
local value: any
local found: boolean
-- try local variables
local i = 1
while true do
local n, v = debug.getlocal(lvl as function, i) as (string, any)
if not n then break end
if n == name then
value = v
found = true
end
i = i + 1
end
if found then return value end
-- try upvalues
local func = debug.getinfo(lvl).func as function
i = 1
while true do
local n, v = debug.getupvalue(func, i) as (string, any)
if not n then break end
if n == name then return v end
i = i + 1
end
-- not found; get global
return getfenv(func)[name]
end
local function get_context(lvl: integer): table
lvl = lvl + 1
local ret: table = {}
ret.name = getvarvalue('__FUNC__', lvl) as string
if not ret.name then
local name0 = debug.getinfo(lvl, 'n').name or ''
ret.name = name0:gsub('(.*)%d+$', '%1')
end
ret.bufnr = getvarvalue('bufnr', lvl)
or getvarvalue('_bufnr', lvl)
or getvarvalue('cbuf', lvl)
or getvarvalue('buf', lvl)
return ret
end
-- If called in a callback then make sure the callback defines a __FUNC__
-- variable which can be used to identify the name of the function.
local function cprint(obj: any, lvl: integer)
lvl = lvl + 1
local msg = obj is string and obj or vim.inspect(obj)
local ctx = get_context(lvl)
local msg2: string
if ctx.bufnr then
msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg)
else
msg2 = string.format('%s: %s', ctx.name, msg)
end
table.insert(M.messages, msg2)
end
function M.dprint(obj: any)
if not M.debug_mode then return end
cprint(obj, 2)
end
function M.dprintf(obj: string, ...:any)
if not M.debug_mode then return end
cprint(obj:format(...), 2)
end
function M.vprint(obj: any)
if not (M.debug_mode and M.verbose) then return end
cprint(obj, 2)
end
function M.vprintf(obj: string, ...:any)
if not (M.debug_mode and M.verbose) then return end
cprint(obj:format(...), 2)
end
local function eprint(msg: string, level: integer)
local info = debug.getinfo(level+2, 'Sl')
if info then
msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg)
end
M.messages[#M.messages+1] = msg
if M.debug_mode then
error(msg)
end
end
function M.eprint(msg: string)
eprint(msg, 1)
end
function M.eprintf(fmt: string, ...:any)
eprint(fmt:format(...), 1)
end
return M

View file

@ -1,18 +0,0 @@
local config = require('gitsigns.config').config
local Hunk = require('gitsigns.hunks').Hunk
return function(a: {string}, b: {string}, linematch: boolean): {Hunk}
local diff_opts = config.diff_opts
local f: function({string}, {string}, string, boolean, integer): {Hunk}
if diff_opts.internal then
f = require('gitsigns.diff_int').run_diff
else
f = require('gitsigns.diff_ext').run_diff
end
local linematch0: integer
if linematch ~= false then
linematch0 = diff_opts.linematch
end
return f(a, b, diff_opts.algorithm, diff_opts.indent_heuristic, linematch0)
end

View file

@ -1,80 +0,0 @@
local git_diff = require('gitsigns.git').diff
local gs_hunks = require("gitsigns.hunks")
local Hunk = gs_hunks.Hunk
local util = require('gitsigns.util')
local scheduler = require('gitsigns.async').scheduler
local record M
-- Async function
run_diff: function({string}, {string}, string, boolean): {Hunk}
end
local function write_to_file(path: string, text: {string})
local f, err = io.open(path, 'wb')
if f == nil then
error(err)
end
for _, l in ipairs(text) do
f:write(l)
f:write('\n')
end
f:close()
end
M.run_diff = function(
text_cmp: {string},
text_buf: {string},
diff_algo: string,
indent_heuristic: boolean
): {Hunk}
local results: {Hunk} = {}
-- tmpname must not be called in a callback
if vim.in_fast_event() then
scheduler()
end
local file_buf = util.tmpname()
local file_cmp = util.tmpname()
write_to_file(file_buf, text_buf)
write_to_file(file_cmp, text_cmp)
-- Taken from gitgutter, diff.vim:
--
-- If a file has CRLF line endings and git's core.autocrlf is true, the file
-- in git's object store will have LF line endings. Writing it out via
-- git-show will produce a file with LF line endings.
--
-- If this last file is one of the files passed to git-diff, git-diff will
-- convert its line endings to CRLF before diffing -- which is what we want
-- but also by default outputs a warning on stderr.
--
-- warning: LF will be replace by CRLF in <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)
for _, line in ipairs(out) do
if vim.startswith(line, '@@') then
results[#results+1] = gs_hunks.parse_diff_line(line)
elseif #results > 0 then
local r = results[#results]
if line:sub(1, 1) == '-' then
r.removed.lines[#r.removed.lines+1] = line:sub(2)
elseif line:sub(1, 1) == '+' then
r.added.lines[#r.added.lines+1] = line:sub(2)
end
end
end
os.remove(file_buf)
os.remove(file_cmp)
return results
end
return M

View file

@ -1,149 +0,0 @@
local create_hunk = require("gitsigns.hunks").create_hunk
local Hunk = require('gitsigns.hunks').Hunk
local config = require('gitsigns.config').config
local async = require('gitsigns.async')
local record M
run_diff: function({string}, {string}, string, boolean, integer): {Hunk}
run_word_diff: function({string}, {string}): {Region}, {Region}
end
local type DiffFun = function({string}, {string}, string, boolean, integer): {DiffResult}
local type DiffResult = {integer, integer, integer, integer}
local run_diff_xdl = function(
fa: {string}, fb: {string},
algorithm: string, indent_heuristic: boolean,
linematch: integer
): {DiffResult}
local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n')..'\n'
local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n')..'\n'
return vim.diff(a, b, {
result_type = 'indices',
algorithm = algorithm,
indent_heuristic = indent_heuristic,
linematch = linematch
})
end
local run_diff_xdl_async = async.wrap(function(
fa: {string}, fb: {string},
algorithm: string, indent_heuristic: boolean,
linematch: integer,
callback: function({DiffResult})
)
local a = vim.tbl_isempty(fa) and '' or table.concat(fa, '\n')..'\n'
local b = vim.tbl_isempty(fb) and '' or table.concat(fb, '\n')..'\n'
vim.loop.new_work(function(
a0: string, b0: string,
algorithm0: string, indent_heuristic0: boolean,
linematch0: integer
): string
return vim.mpack.encode(vim.diff(a0, b0, {
result_type = 'indices',
algorithm = algorithm0,
indent_heuristic = indent_heuristic0,
linematch = linematch0
}))
end, function(r: string)
callback(vim.mpack.decode(r) as {DiffResult})
end):queue(a, b, algorithm, indent_heuristic, linematch)
end, 6)
M.run_diff = async.void(function(
fa: {string}, fb: {string},
diff_algo: string, indent_heuristic: boolean,
linematch: integer
): {Hunk}
local run_diff0: DiffFun
if config._threaded_diff and vim.is_thread then
run_diff0 = run_diff_xdl_async
else
run_diff0 = run_diff_xdl
end
local results = run_diff0(fa, fb, diff_algo, indent_heuristic, linematch)
local hunks: {Hunk} = {}
for _, r in ipairs(results) do
local rs, rc, as, ac = unpack(r)
local hunk = create_hunk(rs, rc, as, ac)
if rc > 0 then
for i = rs, rs+rc-1 do
hunk.removed.lines[#hunk.removed.lines+1] = fa[i] or ''
end
end
if ac > 0 then
for i = as, as+ac-1 do
hunk.added.lines[#hunk.added.lines+1] = fb[i] or ''
end
end
hunks[#hunks+1] = hunk
end
return hunks
end)
local type Region = {integer, string, integer, integer}
local gaps_between_regions = 5
local function denoise_hunks(hunks: {Hunk}): {Hunk}
-- Denoise the hunks
local ret = {hunks[1]}
for j = 2, #hunks do
local h, n = ret[#ret], hunks[j]
if not h or not n then break end
if n.added.start - h.added.start - h.added.count < gaps_between_regions then
h.added.count = n.added.start + n.added.count - h.added.start
h.removed.count = n.removed.start + n.removed.count - h.removed.start
if h.added.count > 0 or h.removed.count > 0 then
h.type = 'change'
end
else
ret[#ret+1] = n
end
end
return ret
end
function M.run_word_diff(removed: {string}, added: {string}): {Region}, {Region}
local adds: {Region} = {}
local rems: {Region} = {}
if #removed ~= #added then
return rems, adds
end
for i = 1, #removed do
-- pair lines by position
local a, b = vim.split(removed[i], ''), vim.split(added[i], '')
local hunks: {Hunk} = {}
for _, r in ipairs(run_diff_xdl(a, b)) do
local rs, rc, as, ac = unpack(r)
-- Balance of the unknown offset done in hunk_func
if rc == 0 then rs = rs + 1 end
if ac == 0 then as = as + 1 end
hunks[#hunks+1] = create_hunk(rs, rc, as, ac)
end
hunks = denoise_hunks(hunks)
for _, h in ipairs(hunks) do
adds[#adds+1] = {i, h.type, h.added.start , h.added.start + h.added.count}
rems[#rems+1] = {i, h.type, h.removed.start, h.removed.start + h.removed.count}
end
end
return rems, adds
end
return M

View file

@ -1,201 +0,0 @@
local api = vim.api
local void = require('gitsigns.async').void
local scheduler = require('gitsigns.async').scheduler
local awrap = require('gitsigns.async').wrap
local gs_cache = require('gitsigns.cache')
local cache = gs_cache.cache
local CacheEntry = gs_cache.CacheEntry
local util = require('gitsigns.util')
local manager = require('gitsigns.manager')
local message = require('gitsigns.message')
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
local input = awrap(vim.ui.input, 2)
local record M
record DiffthisOpts
vertical: boolean
split: string
end
diffthis: function(base: string, opts: DiffthisOpts)
show: function(base: string)
update: function(bufnr: integer)
end
local bufread = void(function(bufnr: integer, dbufnr: integer, base: string, bcache: CacheEntry)
local comp_rev = bcache:get_compare_rev(util.calc_base(base))
local text: {string}
if util.calc_base(base) == util.calc_base(bcache.base) then
text = bcache.compare_text
else
local err: string
text, err = bcache.git_obj:get_show_text(comp_rev)
if err then
error(err, 2)
end
scheduler()
if vim.bo[bufnr].fileformat == 'dos' then
text = util.strip_cr(text)
end
end
local modifiable = vim.bo[dbufnr].modifiable
vim.bo[dbufnr].modifiable = true
util.set_lines(dbufnr, 0, -1, text)
vim.bo[dbufnr].modifiable = modifiable
vim.bo[dbufnr].modified = false
vim.bo[dbufnr].filetype = vim.bo[bufnr].filetype
vim.bo[dbufnr].bufhidden = 'wipe'
end)
local bufwrite = void(function(bufnr: integer, dbufnr: integer, base: string, bcache: CacheEntry)
local buftext = util.buf_lines(dbufnr)
bcache.git_obj:stage_lines(buftext)
scheduler()
vim.bo[dbufnr].modified = false
-- If diff buffer base matches the bcache base then also update the
-- signs.
if util.calc_base(base) == util.calc_base(bcache.base) then
bcache.compare_text = buftext
manager.update(bufnr, bcache)
end
end)
local function run(base: string, diffthis: boolean, opts: M.DiffthisOpts)
local bufnr = vim.api.nvim_get_current_buf()
local bcache = cache[bufnr]
if not bcache then
return
end
opts = opts or {}
local comp_rev = bcache:get_compare_rev(util.calc_base(base))
local bufname = bcache:get_rev_bufname(comp_rev)
local dbuf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_name(dbuf, bufname)
local ok, err = pcall(bufread as function, bufnr, dbuf, base, bcache)
if not ok then
message.error(err as string)
scheduler()
vim.cmd'bdelete'
if diffthis then
vim.cmd'diffoff'
end
return
end
if comp_rev == ':0' then
vim.bo[dbuf].buftype = 'acwrite'
api.nvim_create_autocmd('BufReadCmd', {
group = 'gitsigns',
buffer = dbuf,
callback = function()
bufread(bufnr, dbuf, base, bcache)
if diffthis then
vim.cmd'diffthis'
end
end
})
api.nvim_create_autocmd('BufWriteCmd', {
group = 'gitsigns',
buffer = dbuf,
callback = function()
bufwrite(bufnr, dbuf, base, bcache)
end
})
else
vim.bo[dbuf].buftype = 'nowrite'
vim.bo[dbuf].modifiable = false
end
if diffthis then
vim.cmd(table.concat({
'keepalt', opts.split or 'aboveleft',
opts.vertical and 'vertical' or '',
'diffsplit', bufname
}, ' '))
else
vim.cmd('edit '..bufname)
end
end
M.diffthis = void(function(base: string, opts: M.DiffthisOpts)
if vim.wo.diff then
return
end
local bufnr = vim.api.nvim_get_current_buf()
local bcache = cache[bufnr]
if not bcache then
return
end
local cwin = api.nvim_get_current_win()
if not base and bcache.git_obj.has_conflicts then
run(':2', true, opts)
api.nvim_set_current_win(cwin)
opts.split = 'belowright'
run(':3', true, opts)
else
run(base, true, opts)
end
api.nvim_set_current_win(cwin)
end)
M.show = void(function(base: string)
run(base, false)
end)
local function should_reload(bufnr: integer): boolean
if not vim.bo[bufnr].modified then
return true
end
local response: string
while not vim.tbl_contains({'O', 'L'}, response) do
response = input{
prompt = 'Warning: The git index has changed and the buffer was changed as well. [O]K, (L)oad File:'
}
end
return response == 'L'
end
-- This function needs to be throttled as there is a call to vim.ui.input
M.update = throttle_by_id(void(function(bufnr: integer)
if not vim.wo.diff then
return
end
local bcache = cache[bufnr]
-- Note this will be the bufname for the currently set base
-- which are the only ones we want to update
local bufname = bcache:get_rev_bufname()
for _, w in ipairs(api.nvim_list_wins()) do
if api.nvim_win_is_valid(w) then
local b = api.nvim_win_get_buf(w)
local bname = api.nvim_buf_get_name(b)
if bname == bufname or vim.startswith(bname, 'fugitive://') then
if should_reload(b) then
api.nvim_buf_call(b, function(): nil
vim.cmd('doautocmd BufReadCmd')
vim.cmd('diffthis')
end)
end
end
end
end
end))
return M

View file

@ -1,725 +0,0 @@
local async = require('gitsigns.async')
local scheduler = require('gitsigns.async').scheduler
local log = require("gitsigns.debug.log")
local util = require('gitsigns.util')
local subprocess = require('gitsigns.subprocess')
local gs_config = require('gitsigns.config')
local config = gs_config.config
local gs_hunks = require("gitsigns.hunks")
local Hunk = gs_hunks.Hunk
local uv = vim.loop
local startswith = vim.startswith
local dprint = require('gitsigns.debug.log').dprint
local eprint = require('gitsigns.debug.log').eprint
local err = require('gitsigns.message').error
local record GJobSpec
command: string
args: {string}
cwd: string
writer: {string}
-- local extensions
suppress_stderr: boolean
end
local record M
record BlameInfo
-- Info in header
sha: string
abbrev_sha: string
orig_lnum: integer
final_lnum: integer
-- Porcelain fields
author: string
author_mail: string
author_time: integer
author_tz: string
committer: string
committer_mail: string
committer_time: integer
committer_tz: string
summary: string
previous: string
previous_filename: string
previous_sha: string
filename: string
end
record Version
major: integer
minor: integer
patch: integer
end
version: Version
record RepoInfo
gitdir: string
toplevel: string
detached: boolean
abbrev_head: string
end
get_repo_info: function(path: string, cmd: string, gitdir: string, toplevel: string): RepoInfo
diff: function(file_cmp: string, file_buf: string, indent_heuristic: boolean, diff_algo: string): {string}, string
record Repo
toplevel : string
gitdir : string
detached : boolean
abbrev_head: string
username : string
command : function(Repo, {string}, GJobSpec): {string}, string
files_changed : function(Repo): {string}
get_show_text : function(Repo, string, string): {string}, string
update_abbrev_head : function(Repo)
new : function(dir: string, gitdir: string, toplevel: string): Repo
end
record FileProps
relpath : string
orig_relpath : string -- Use for tracking moved files
object_name : string
mode_bits : string
has_conflicts : boolean
i_crlf : boolean -- Object has crlf
w_crlf : boolean -- Working copy has crlf
end
record Obj
repo : Repo
file : string
relpath : string
orig_relpath : string -- Use for tracking moved files
object_name : string
mode_bits : string
has_conflicts : boolean
i_crlf : boolean -- Object has crlf
w_crlf : boolean -- Working copy has crlf
encoding : string
command : function(Obj, {string}, GJobSpec): {string}, string
update_file_info : function(Obj, boolean, silent: boolean): boolean
unstage_file : function(Obj, string, string)
run_blame : function(Obj, {string}, number, boolean): BlameInfo
file_info : function(Obj, string, silent: boolean): FileProps
get_show_text : function(Obj, string): {string}, string
ensure_file_in_index : function(Obj)
stage_hunks : function(Obj, {Hunk}, boolean)
stage_lines : function(Obj, {string})
has_moved : function(Obj): string
new : function(path: string, enc: string, gitdir: string, toplevel: string): Obj
end
end
local in_git_dir = function(file: string): boolean
for _, p in ipairs(vim.split(file, util.path_sep)) do
if p == '.git' then
return true
end
end
return false
end
local Obj = M.Obj
local Repo = M.Repo
local function parse_version(version: string): M.Version
assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: '..version)
local ret: M.Version = {}
local parts = vim.split(version, '%.')
ret.major = tonumber(parts[1]) as integer
ret.minor = tonumber(parts[2]) as integer
if parts[3] == 'GIT' then
ret.patch = 0
else
ret.patch = tonumber(parts[3]) as integer
end
return ret
end
-- Usage: check_version{2,3}
local function check_version(version: {number,number,number}): boolean
if not M.version then
return false
end
if M.version.major < version[1] then
return false
end
if version[2] and M.version.minor < version[2] then
return false
end
if version[3] and M.version.patch < version[3] then
return false
end
return true
end
--- @async
function M._set_version(version: string)
if version ~= 'auto' then
M.version = parse_version(version)
return
end
local _, _, stdout, stderr = async.wait(2, subprocess.run_job, {
command = 'git', args = { '--version' }
})
local line = vim.split(stdout or '', '\n', true)[1]
if not line then
err("Unable to detect git version as 'git --version' failed to return anything")
eprint(stderr)
return
end
assert(type(line) == 'string', 'Unexpected output: '..line)
assert(startswith(line, 'git version'), 'Unexpected output: '..line)
local parts = vim.split(line, '%s+')
M.version = parse_version(parts[3])
end
--- @async
local git_command = async.create(function(args: {string}, spec: GJobSpec): {string}, string
if not M.version then
M._set_version(config._git_version)
end
spec = spec or {}
spec.command = spec.command or 'git'
spec.args = spec.command == 'git' and {
'--no-pager',
'--literal-pathspecs',
'-c', 'gc.auto=0', -- Disable auto-packing which emits messages to stderr
unpack(args)
} or args
if not spec.cwd and not uv.cwd() then
spec.cwd = vim.env.HOME
end
local _, _, stdout, stderr = async.wait(2, subprocess.run_job, spec as subprocess.JobSpec)
if not spec.suppress_stderr then
if stderr then
local cmd_str = table.concat({spec.command, unpack(args)}, ' ')
log.eprintf("Received stderr when running command\n'%s':\n%s", cmd_str, stderr)
end
end
local stdout_lines = vim.split(stdout or '', '\n', true)
-- If stdout ends with a newline, then remove the final empty string after
-- the split
if stdout_lines[#stdout_lines] == '' then
stdout_lines[#stdout_lines] = nil
end
if log.verbose then
log.vprintf('%d lines:', #stdout_lines)
for i = 1, math.min(10, #stdout_lines) do
log.vprintf('\t%s', stdout_lines[i])
end
end
return stdout_lines, stderr
end, 2)
--- @async
function M.diff(file_cmp: string, file_buf: string, indent_heuristic: boolean, diff_algo: string): {string}, string
return git_command{
'-c', 'core.safecrlf=false',
'diff',
'--color=never',
'--'..(indent_heuristic and '' or 'no-')..'indent-heuristic',
'--diff-algorithm='..diff_algo,
'--patch-with-raw',
'--unified=0',
file_cmp,
file_buf,
}
end
--- @async
local function process_abbrev_head(gitdir: string, head_str: string, path: string, cmd: string): string
if not gitdir then
return head_str
end
if head_str == 'HEAD' then
local short_sha = git_command({'rev-parse', '--short', 'HEAD'}, {
command = cmd or 'git',
suppress_stderr = true,
cwd = path,
})[1] or ''
if log.debug_mode and short_sha ~= '' then
short_sha = 'HEAD'
end
if util.path_exists(gitdir..'/rebase-merge')
or util.path_exists(gitdir..'/rebase-apply') then
return short_sha..'(rebasing)'
end
return short_sha
end
return head_str
end
local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1
--- @async
local cygpath_convert: function(path: string): string
if has_cygpath then
cygpath_convert = function(path: string): string
return git_command({ '-aw', path }, { command = 'cygpath' })[1]
end
end
local function normalize_path(path: string): string
if path and has_cygpath and not uv.fs_stat(path) then
-- If on windows and path isn't recognizable as a file, try passing it
-- through cygpath
path = cygpath_convert(path)
end
return path
end
--- @async
function M.get_repo_info(path: string, cmd: string, gitdir: string, toplevel: string): M.RepoInfo
-- Does git rev-parse have --absolute-git-dir, added in 2.13:
-- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/
local has_abs_gd = check_version{2,13}
local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir'
-- Wait for internal scheduler to settle before running command
-- https://github.com/lewis6991/gitsigns.nvim/pull/215
scheduler()
local args = {}
if gitdir then
vim.list_extend(args, {'--git-dir', gitdir})
end
if toplevel then
vim.list_extend(args, {'--work-tree', toplevel})
end
vim.list_extend(args, {
'rev-parse', '--show-toplevel', git_dir_opt, '--abbrev-ref', 'HEAD',
})
local results = git_command(args, {
command = cmd or 'git',
suppress_stderr = true,
cwd = toplevel or path,
})
local ret: M.RepoInfo = {
toplevel = normalize_path(results[1]),
gitdir = normalize_path(results[2]),
}
ret.abbrev_head = process_abbrev_head(ret.gitdir, results[3], path, cmd)
if ret.gitdir and not has_abs_gd then
ret.gitdir = uv.fs_realpath(ret.gitdir)
end
ret.detached = ret.toplevel and ret.gitdir ~= ret.toplevel..'/.git'
return ret
end
--------------------------------------------------------------------------------
-- Git repo object methods
--------------------------------------------------------------------------------
--- Run git command the with the objects gitdir and toplevel
--- @async
function Repo:command(args: {string}, spec: GJobSpec): {string}, string
spec = spec or {}
spec.cwd = self.toplevel
local args1 = {
'--git-dir', self.gitdir,
}
if self.detached then
vim.list_extend(args1, {'--work-tree', self.toplevel})
end
vim.list_extend(args1, args)
return git_command(args1, spec)
end
--- @async
function Repo:files_changed(): {string}
local results = self:command({ 'status', '--porcelain', '--ignore-submodules' })
local ret: {string} = {}
for _, line in ipairs(results) do
if line:sub(1, 2):match('^.M') then
ret[#ret+1] = line:sub(4, -1)
end
end
return ret
end
local function make_bom(...: integer): string
local r = {}
for i, a in ipairs{...} do
r[i] = string.char(a)
end
return table.concat(r)
end
local BOM_TABLE: {string:string} = {
['utf-8'] = make_bom(0xef, 0xbb, 0xbf),
['utf-16le'] = make_bom(0xff, 0xfe),
['utf-16'] = make_bom(0xfe, 0xff),
['utf-16be'] = make_bom(0xfe, 0xff),
['utf-32le'] = make_bom(0xff, 0xfe, 0x00, 0x00),
['utf-32'] = make_bom(0xff, 0xfe, 0x00, 0x00),
['utf-32be'] = make_bom(0x00, 0x00, 0xfe, 0xff),
['utf-7'] = make_bom(0x2b, 0x2f, 0x76),
['utf-1'] = make_bom(0xf7, 0x54, 0x4c),
}
local function strip_bom(x: string, encoding: string): string
local bom = BOM_TABLE[encoding]
if bom and vim.startswith(x, bom) then
return x:sub(bom:len() + 1)
end
return x
end
local function iconv_supported(encoding: string): boolean
-- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924
if vim.startswith(encoding, 'utf-16') then
return false
elseif vim.startswith(encoding, 'utf-32') then
return false
end
return true
end
--- Get version of file in the index, return array lines
--- @async
function Repo:get_show_text(object: string, encoding: string): {string}, string
local stdout, stderr = self:command({'show', object}, {suppress_stderr = true})
if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then
stdout[1] = strip_bom(stdout[1], encoding)
if vim.iconv then
for i, l in ipairs(stdout) do
stdout[i] = vim.iconv(l, encoding, 'utf-8')
end
else
scheduler()
for i, l in ipairs(stdout) do
-- vimscript will interpret strings containing NUL as blob type
if vim.fn.type(l) == vim.v.t_string then
stdout[i] = vim.fn.iconv(l, encoding, 'utf-8')
end
end
end
end
return stdout, stderr
end
--- @async
function Repo:update_abbrev_head()
self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head
end
--- @async
function Repo.new(dir: string, gitdir: string, toplevel: string): Repo
local self: Repo = setmetatable({}, {__index = Repo})
self.username = git_command({'config', 'user.name'})[1]
local info = M.get_repo_info(dir, nil, gitdir, toplevel)
for k, v in pairs(info as table) do
(self as table)[k] = v
end
-- Try yadm
if config.yadm.enable and not self.gitdir then
if vim.startswith(dir, os.getenv('HOME'))
and #git_command({'ls-files', dir}, {command = 'yadm'}) ~= 0 then
M.get_repo_info(dir, 'yadm', gitdir, toplevel)
local yadm_info = M.get_repo_info(dir, 'yadm', gitdir, toplevel)
for k, v in pairs(yadm_info as {string:any}) do
(self as table)[k] = v
end
end
end
return self
end
--------------------------------------------------------------------------------
-- Git object methods
--------------------------------------------------------------------------------
--- Run git command the with the objects gitdir and toplevel
--- @async
function Obj:command(args: {string}, spec: GJobSpec): {string}, string
return self.repo:command(args, spec)
end
--- @async
function Obj:update_file_info(update_relpath: boolean, silent: boolean): boolean
local old_object_name = self.object_name
local props = self:file_info(self.file, silent)
if update_relpath then
self.relpath = props.relpath
end
self.object_name = props.object_name
self.mode_bits = props.mode_bits
self.has_conflicts = props.has_conflicts
self.i_crlf = props.i_crlf
self.w_crlf = props.w_crlf
return old_object_name ~= self.object_name
end
--- @async
function Obj:file_info(file: string, silent: boolean): M.FileProps
local results, stderr = self:command({
'-c', 'core.quotepath=off',
'ls-files',
'--stage',
'--others',
'--exclude-standard',
'--eol',
file or self.file
}, {suppress_stderr = true})
if stderr and not silent then
-- Suppress_stderr for the cases when we run:
-- git ls-files --others exists/nonexist
if not stderr:match('^warning: could not open directory .*: No such file or directory') then
log.eprint(stderr)
end
end
local result: M.FileProps = {}
for _, line in ipairs(results) do
local parts = vim.split(line, '\t')
if #parts > 2 then -- tracked file
local eol = vim.split(parts[2], '%s+')
result.i_crlf = eol[1] == 'i/crlf'
result.w_crlf = eol[2] == 'w/crlf'
result.relpath = parts[3]
local attrs = vim.split(parts[1], '%s+')
local stage = tonumber(attrs[3])
if stage <= 1 then
result.mode_bits = attrs[1]
result.object_name = attrs[2]
else
result.has_conflicts = true
end
else -- untracked file
result.relpath = parts[2]
end
end
return result
end
--- @async
function Obj:get_show_text(revision: string): {string}, string
if not self.relpath then
return {}
end
local stdout, stderr = self.repo:get_show_text(revision..':'..self.relpath, self.encoding)
if not self.i_crlf and self.w_crlf then
-- Add cr
for i = 1, #stdout do
stdout[i] = stdout[i]..'\r'
end
end
return stdout, stderr
end
--- @async
Obj.unstage_file = function(self: Obj)
self:command{'reset', self.file }
end
--- @async
function Obj:run_blame(lines: {string}, lnum: number, ignore_whitespace: boolean): M.BlameInfo
local not_committed = {
author = 'Not Committed Yet',
['author_mail'] = '<not.committed.yet>',
committer = 'Not Committed Yet',
['committer_mail'] = '<not.committed.yet>',
}
if not self.object_name or self.repo.abbrev_head == '' then
-- As we support attaching to untracked files we need to return something if
-- the file isn't isn't tracked in git.
-- If abbrev_head is empty, then assume the repo has no commits
return not_committed
end
local args = {
'blame',
'--contents', '-',
'-L', lnum..',+1',
'--line-porcelain',
self.file
}
if ignore_whitespace then
args[#args+1] = '-w'
end
local ignore_file = self.repo.toplevel ..'/.git-blame-ignore-revs'
if uv.fs_stat(ignore_file) then
vim.list_extend(args, {'--ignore-revs-file', ignore_file})
end
local results = self:command(args, { writer = lines })
if #results == 0 then
return
end
local header = vim.split(table.remove(results, 1), ' ')
local ret: {string:any} = {}
ret.sha = header[1]
ret.orig_lnum = tonumber(header[2]) as integer
ret.final_lnum = tonumber(header[3]) as integer
ret.abbrev_sha = string.sub(ret.sha as string, 1, 8)
for _, l in ipairs(results) do
if not startswith(l, '\t') then
local cols = vim.split(l, ' ')
local key = table.remove(cols, 1):gsub('-', '_')
ret[key] = table.concat(cols, ' ')
if key == 'previous' then
ret.previous_sha = cols[1]
ret.previous_filename = cols[2]
end
end
end
-- New in git 2.41:
-- The output given by "git blame" that attributes a line to contents
-- taken from the file specified by the "--contents" option shows it
-- differently from a line attributed to the working tree file.
if ret.author_mail == '<external.file>' then
ret = vim.tbl_extend('force', ret, not_committed as table) as {string:any}
end
return ret as M.BlameInfo
end
--- @async
local function ensure_file_in_index(obj: Obj)
if obj.object_name and not obj.has_conflicts then
return
end
if not obj.object_name then
-- If there is no object_name then it is not yet in the index so add it
obj:command{'add', '--intent-to-add', obj.file}
else
-- Update the index with the common ancestor (stage 1) which is what bcache
-- stores
local info = string.format('%s,%s,%s', obj.mode_bits, obj.object_name, obj.relpath)
obj:command{'update-index', '--add', '--cacheinfo', info}
end
obj:update_file_info()
end
-- Stage 'lines' as the entire contents of the file
--- @async
--- @param lines
function Obj:stage_lines(lines: {string})
local stdout = self:command({
'hash-object', '-w', '--path', self.relpath, '--stdin'
}, { writer = lines })
local new_object = stdout[1]
self:command{
'update-index', '--cacheinfo', string.format('%s,%s,%s', self.mode_bits, new_object, self.relpath)
}
end
--- @async
Obj.stage_hunks = function(self: Obj, hunks: {Hunk}, invert: boolean)
ensure_file_in_index(self)
local patch = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert)
if not self.i_crlf and self.w_crlf then
-- Remove cr
for i = 1, #patch do
patch[i] = patch[i]:gsub('\r$', '')
end
end
self:command({
'apply', '--whitespace=nowarn', '--cached', '--unidiff-zero', '-'
}, {
writer = patch
})
end
--- @async
function Obj:has_moved(): string
local out = self:command{'diff', '--name-status', '-C', '--cached'}
local orig_relpath = self.orig_relpath or self.relpath
for _, l in ipairs(out) do
local parts = vim.split(l, '%s+')
if #parts == 3 then
local orig, new = parts[2], parts[3]
if orig_relpath == orig then
self.orig_relpath = orig_relpath
self.relpath = new
self.file = self.repo.toplevel..'/'..new
return new
end
end
end
end
--- @async
function Obj.new(file: string, encoding: string, gitdir: string, toplevel: string): Obj
if in_git_dir(file) then
dprint('In git dir')
return nil
end
local self: Obj = setmetatable({}, {__index = Obj})
self.file = file
self.encoding = encoding
self.repo = Repo.new(util.dirname(file), gitdir, toplevel)
if not self.repo.gitdir then
dprint('Not in git repo')
return nil
end
-- When passing gitdir and toplevel, suppress stderr when resolving the file
local silent = gitdir ~= nil and toplevel ~= nil
self:update_file_info(true, silent)
return self
end
return M

View file

@ -1,223 +0,0 @@
local record Hldef
{string}
desc: string
hidden: boolean
fg_factor: number
bg_factor: number
end
local record M
setup_highlights: function()
hls: {{string:Hldef}}
end
-- Use array of dict so we can iterate deterministically
-- Export for docgen
M.hls = {
{GitSignsAdd = { 'GitGutterAdd', 'SignifySignAdd', 'DiffAddedGutter', 'diffAdded', 'DiffAdd',
desc = "Used for the text of 'add' signs."
}},
{GitSignsChange = { 'GitGutterChange', 'SignifySignChange', 'DiffModifiedGutter', 'diffChanged', 'DiffChange',
desc = "Used for the text of 'change' signs."
}},
{GitSignsDelete = { 'GitGutterDelete', 'SignifySignDelete', 'DiffRemovedGutter' , 'diffRemoved', 'DiffDelete',
desc = "Used for the text of 'delete' signs."
}},
{GitSignsChangedelete = { 'GitSignsChange',
desc = "Used for the text of 'changedelete' signs."
}},
{GitSignsTopdelete = { 'GitSignsDelete',
desc = "Used for the text of 'topdelete' signs."
}},
{GitSignsUntracked = { 'GitSignsAdd',
desc = "Used for the text of 'untracked' signs."
}},
{GitSignsAddNr = {'GitGutterAddLineNr', 'GitSignsAdd',
desc = "Used for number column (when `config.numhl == true`) of 'add' signs."
}},
{GitSignsChangeNr = {'GitGutterChangeLineNr', 'GitSignsChange',
desc = "Used for number column (when `config.numhl == true`) of 'change' signs."
}},
{GitSignsDeleteNr = {'GitGutterDeleteLineNr', 'GitSignsDelete',
desc = "Used for number column (when `config.numhl == true`) of 'delete' signs."
}},
{GitSignsChangedeleteNr = {'GitSignsChangeNr',
desc = "Used for number column (when `config.numhl == true`) of 'changedelete' signs."
}},
{GitSignsTopdeleteNr = {'GitSignsDeleteNr',
desc = "Used for number column (when `config.numhl == true`) of 'topdelete' signs."
}},
{GitSignsUntrackedNr = {'GitSignsAddNr',
desc = "Used for number column (when `config.numhl == true`) of 'untracked' signs."
}},
{GitSignsAddLn = {'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd',
desc = "Used for buffer line (when `config.linehl == true`) of 'add' signs."
}},
{GitSignsChangeLn = {'GitGutterChangeLine', 'SignifyLineChange', 'DiffChange',
desc = "Used for buffer line (when `config.linehl == true`) of 'change' signs."
}},
{GitSignsChangedeleteLn = {'GitSignsChangeLn',
desc = "Used for buffer line (when `config.linehl == true`) of 'changedelete' signs."
}},
{GitSignsUntrackedLn = {'GitSignsAddLn',
desc = "Used for buffer line (when `config.linehl == true`) of 'untracked' signs."
}},
-- Don't set GitSignsDeleteLn by default
-- {GitSignsDeleteLn = {}},
{GitSignsStagedAdd = {'GitSignsAdd' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedChange = {'GitSignsChange' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedDelete = {'GitSignsDelete' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedChangedelete = {'GitSignsChangedelete' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedTopdelete = {'GitSignsTopdelete' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedAddNr = {'GitSignsAddNr' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedChangeNr = {'GitSignsChangeNr' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedDeleteNr = {'GitSignsDeleteNr' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedChangedeleteNr = {'GitSignsChangedeleteNr', fg_factor = 0.5, hidden = true, }},
{GitSignsStagedTopdeleteNr = {'GitSignsTopdeleteNr' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedAddLn = {'GitSignsAddLn' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedChangeLn = {'GitSignsChangeLn' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedDeleteLn = {'GitSignsDeleteLn' , fg_factor = 0.5, hidden = true, }},
{GitSignsStagedChangedeleteLn = {'GitSignsChangedeleteLn', fg_factor = 0.5, hidden = true, }},
{GitSignsStagedTopdeleteLn = {'GitSignsTopdeleteLn' , fg_factor = 0.5, hidden = true, }},
{GitSignsAddPreview = {'GitGutterAddLine', 'SignifyLineAdd', 'DiffAdd',
desc = "Used for added lines in previews."
}},
{GitSignsDeletePreview = {'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete',
desc = "Used for deleted lines in previews."
}},
{GitSignsCurrentLineBlame = {'NonText',
desc = "Used for current line blame."
}},
{GitSignsAddInline = {'TermCursor',
desc = "Used for added word diff regions in inline previews."
}},
{GitSignsDeleteInline = {'TermCursor',
desc = "Used for deleted word diff regions in inline previews."
}},
{GitSignsChangeInline = {'TermCursor',
desc = "Used for changed word diff regions in inline previews."
}},
{GitSignsAddLnInline = {'GitSignsAddInline',
desc = "Used for added word diff regions when `config.word_diff == true`."
}},
{GitSignsChangeLnInline = {'GitSignsChangeInline',
desc = "Used for changed word diff regions when `config.word_diff == true`."
}},
{GitSignsDeleteLnInline = {'GitSignsDeleteInline',
desc = "Used for deleted word diff regions when `config.word_diff == true`."
}},
-- Currently unused
-- {GitSignsAddLnVirtLn = {'GitSignsAddLn'}},
-- {GitSignsChangeVirtLn = {'GitSignsChangeLn'}},
-- {GitSignsAddLnVirtLnInLine = {'GitSignsAddLnInline', }},
-- {GitSignsChangeVirtLnInLine = {'GitSignsChangeLnInline', }},
{GitSignsDeleteVirtLn = {'GitGutterDeleteLine', 'SignifyLineDelete', 'DiffDelete',
desc = "Used for deleted lines shown by inline `preview_hunk_inline()` or `show_deleted()`."
}},
{GitSignsDeleteVirtLnInLine = {'GitSignsDeleteLnInline',
desc = "Used for word diff regions in lines shown by inline `preview_hunk_inline()` or `show_deleted()`."
}},
{GitSignsVirtLnum = {'GitSignsDeleteVirtLn',
desc = 'Used for line numbers in inline hunks previews.'
}},
}
local function is_hl_set(hl_name: string): boolean
-- TODO: this only works with `set termguicolors`
local exists, hl = pcall(vim.api.nvim_get_hl_by_name, hl_name, true)
local color = hl.foreground or hl.background or hl.reverse
return exists and color ~= nil
end
local function cmul(x: integer, factor: number): integer
if not x or factor == 1 then
return x
end
local r = math.floor(x / 2^16)
local x1 = x - (r * 2^16)
local g = math.floor(x1 / 2^8)
local b = math.floor(x1 - (g * 2^8))
return math.floor(math.floor(r*factor) * 2^16 + math.floor(g*factor) * 2^8 + math.floor(b*factor))
end
local function dprintf(fmt: string, ...:any)
require('gitsigns.debug.log').dprintf(fmt, ...)
end
local function derive(hl: string, hldef: Hldef)
for _, d in ipairs(hldef) do
if is_hl_set(d) then
dprintf('Deriving %s from %s', hl, d)
if hldef.fg_factor or hldef.bg_factor then
hldef.fg_factor = hldef.fg_factor or 1
hldef.bg_factor = hldef.bg_factor or 1
local dh = vim.api.nvim_get_hl_by_name(d, true)
vim.api.nvim_set_hl(0, hl, {
default = true,
fg = cmul(dh.foreground, hldef.fg_factor),
bg = cmul(dh.background, hldef.bg_factor),
})
else
vim.api.nvim_set_hl(0, hl, {default = true, link = d })
end
return
end
end
if hldef[1] and not hldef.bg_factor and not hldef.fg_factor then
-- No fallback found which is set. Just link to the first fallback
-- if there are no modifiers
dprintf('Deriving %s from %s', hl, hldef[1])
vim.api.nvim_set_hl(0, hl, {default = true, link = hldef[1] })
else
dprintf('Could not derive %s', hl)
end
end
-- Setup a GitSign* highlight by deriving it from other potentially present
-- highlights.
M.setup_highlights = function()
for _, hlg in ipairs(M.hls) do
for hl, hldef in pairs(hlg) do
if is_hl_set(hl) then
-- Already defined
dprintf('Highlight %s is already defined', hl)
else
derive(hl, hldef)
end
end
end
end
return M

View file

@ -1,472 +0,0 @@
local Sign = require('gitsigns.signs').Sign
local StatusObj = require('gitsigns.status').StatusObj
local util = require('gitsigns.util')
local min, max = math.min, math.max
--- @alias Gitsigns.Hunk.Type
--- | "add"
--- | "change"
--- | "delete"
--- @class Gitsigns.Hunk.Node
--- @field start integer
--- @field count integer
--- @field lines string[]
--- @class Gitsigns.Hunk.Hunk
--- @field type Gitsigns.Hunk.Type
--- @field head string
--- @field added Gitsigns.Hunk.Node
--- @field removed Gitsigns.Hunk.Node
--- @field vend integer
--- @class Gitsigns.Hunk.Hunk_Public
--- @field type Gitsigns.Hunk.Type
--- @field head string
--- @field lines string[]
--- @field added Gitsigns.Hunk.Node
--- @field removed Gitsigns.Hunk.Node
local record M
enum Type
"add"
"change"
"delete"
end
record Node
start: integer
count: integer
lines: {string}
end
-- For internal use
record Hunk
type: Type
head: string
added: Node
removed: Node
vend: integer
end
record Hunk_Public
type: Type
head: string
lines: {string}
added: Node
removed: Node
end
end
local Hunk = M.Hunk
--- @param old_start integer
--- @param old_count integer
--- @param new_start integer
--- @param new_count integer
--- @return Gitsigns.Hunk.Hunk
function M.create_hunk(old_start: integer, old_count: integer, new_start: integer, new_count: integer): Hunk
return {
removed = { start = old_start, count = old_count, lines = {} },
added = { start = new_start, count = new_count, lines = {} },
head = ('@@ -%d%s +%d%s @@'):format(
old_start, old_count > 0 and ',' .. old_count or '',
new_start, new_count > 0 and ',' .. new_count or ''
),
vend = new_start + math.max(new_count - 1, 0),
type = new_count == 0 and 'delete' or
old_count == 0 and 'add' or
'change'
}
end
--- @param hunks Gitsigns.Hunk.Hunk[]
--- @param top integer
--- @param bot integer
--- @return Gitsigns.Hunk.Hunk
function M.create_partial_hunk(hunks: {Hunk}, top: integer, bot: integer): Hunk
local pretop, precount = top, bot - top + 1
for _, h in ipairs(hunks) do
local added_in_hunk = h.added.count - h.removed.count
local added_in_range = 0
if h.added.start >= top and h.vend <= bot then
-- Range contains hunk
added_in_range = added_in_hunk
else
local added_above_bot = max(0, bot + 1 - (h.added.start + h.removed.count))
local added_above_top = max(0, top - (h.added.start + h.removed.count))
if h.added.start >= top and h.added.start <= bot then
-- Range top intersects hunk
added_in_range = added_above_bot
elseif h.vend >= top and h.vend <= bot then
-- Range bottom intersects hunk
added_in_range = added_in_hunk - added_above_top
pretop = pretop - added_above_top
elseif h.added.start <= top and h.vend >= bot then
-- Range within hunk
added_in_range = added_above_bot - added_above_top
pretop = pretop - added_above_top
end
if top > h.vend then
pretop = pretop - added_in_hunk
end
end
precount = precount - added_in_range
end
if precount == 0 then
pretop = pretop - 1
end
return M.create_hunk(pretop, precount, top, bot - top + 1)
end
--- @param hunk Gitsigns.Hunk.Hunk
--- @param fileformat string
--- @return string[]
function M.patch_lines(hunk: Hunk, fileformat: string): {string}
local lines: {string} = {} --- @type string[]
for _, l in ipairs(hunk.removed.lines) do
lines[#lines+1] = '-'..l
end
for _, l in ipairs(hunk.added.lines) do
lines[#lines+1] = '+'..l
end
if fileformat == 'dos' then
lines = util.strip_cr(lines)
end
return lines
end
--- @param line string
--- @return Gitsigns.Hunk.Hunk
function M.parse_diff_line(line: string): Hunk
local diffkey = vim.trim(vim.split(line, '@@', true)[2])
-- diffKey: "-xx,n +yy"
-- pre: {xx, n}, now: {yy}
local pre, now = unpack(vim.tbl_map(function(s: string): {string}
return vim.split(string.sub(s, 2), ',')
end, vim.split(diffkey, ' ')) as {{string}})
local hunk = M.create_hunk(
tonumber(pre[1]) as integer, (tonumber(pre[2]) or 1) as integer,
tonumber(now[1]) as integer, (tonumber(now[2]) or 1) as integer
)
hunk.head = line
return hunk
end
--- @param hunk Gitsigns.Hunk.Hunk
--- @return integer
local function change_end(hunk: Hunk): integer
if hunk.added.count == 0 then
-- delete
return hunk.added.start
elseif hunk.removed.count == 0 then
-- add
return hunk.added.start + hunk.added.count - 1
else
-- change
return hunk.added.start + min(hunk.added.count, hunk.removed.count) - 1
end
end
--- Calculate signs needed to be applied from a hunk for a specified line range.
--- @param hunk Gitsigns.Hunk.Hunk
--- @param min_lnum integer
--- @param max_lnum integer
--- @param untracked boolean
--- @return Gitsigns.Sign[]
function M.calc_signs(hunk: Hunk, min_lnum: integer, max_lnum: integer, untracked: boolean): {Sign}
assert(not untracked or hunk.type == 'add')
min_lnum = min_lnum or 1
max_lnum = max_lnum or math.huge as integer
local start, added, removed = hunk.added.start, hunk.added.count, hunk.removed.count
if hunk.type == 'delete' and start == 0 then
if min_lnum <= 1 then
-- topdelete signs get placed one row lower
return {{ type = 'topdelete', count = removed, lnum = 1 }}
else
return {}
end
end
local signs: {Sign} = {}
local cend = change_end(hunk)
for lnum = max(start, min_lnum), min(cend, max_lnum) do
local changedelete = hunk.type == 'change' and removed > added and lnum == cend
signs[#signs+1] = {
type = changedelete and 'changedelete' or
untracked and 'untracked' or hunk.type,
count = lnum == start and (hunk.type == 'add' and added or removed),
lnum = lnum
}
end
if hunk.type == "change" and added > removed and
hunk.vend >= min_lnum and cend <= max_lnum then
for lnum = max(cend, min_lnum), min(hunk.vend, max_lnum) do
signs[#signs+1] = {
type = 'add',
count = lnum == hunk.vend and (added - removed),
lnum = lnum
}
end
end
return signs
end
--- @param relpath string
--- @param hunks Gitsigns.Hunk.Hunk[]
--- @param mode_bits string
--- @param invert boolean
--- @return string[]
function M.create_patch(relpath: string, hunks: {Hunk}, mode_bits: string, invert: boolean): {string}
invert = invert or false
local results = {
string.format('diff --git a/%s b/%s', relpath, relpath),
'index 000000..000000 '..mode_bits,
'--- a/'..relpath,
'+++ b/'..relpath,
}
local offset = 0
for _, process_hunk in ipairs(hunks) do
local start, pre_count, now_count =
process_hunk.removed.start, process_hunk.removed.count, process_hunk.added.count
if process_hunk.type == 'add' then
start = start + 1
end
local pre_lines = process_hunk.removed.lines
local now_lines = process_hunk.added.lines
if invert then
pre_count, now_count = now_count, pre_count
pre_lines, now_lines = now_lines, pre_lines
end
table.insert(results, string.format('@@ -%s,%s +%s,%s @@', start, pre_count, start + offset, now_count))
for _, l in ipairs(pre_lines) do
results[#results+1] = '-'..l
end
for _, l in ipairs(now_lines) do
results[#results+1] = '+'..l
end
process_hunk.removed.start = start + offset
offset = offset + (now_count - pre_count)
end
return results
end
--- @param hunks Gitsigns.Hunk.Hunk[]
--- @return Gitsigns.StatusObj
function M.get_summary(hunks: {Hunk}): StatusObj
local status = { added = 0, changed = 0, removed = 0 }
for _, hunk in ipairs(hunks or {}) do
if hunk.type == 'add' then
status.added = status.added + hunk.added.count
elseif hunk.type == 'delete' then
status.removed = status.removed + hunk.removed.count
elseif hunk.type == 'change' then
local add, remove = hunk.added.count, hunk.removed.count
local delta = min(add, remove)
status.changed = status.changed + delta
status.added = status.added + add - delta
status.removed = status.removed + remove - delta
end
end
return status
end
--- @param lnum integer
--- @param hunks Gitsigns.Hunk.Hunk[]
--- @return Gitsigns.Hunk.Hunk?, integer?
function M.find_hunk(lnum: integer, hunks: {Hunk}): Hunk, integer
for i, hunk in ipairs(hunks or {}) do
if lnum == 1 and hunk.added.start == 0 and hunk.vend == 0 then
return hunk, i
end
if hunk.added.start <= lnum and hunk.vend >= lnum then
return hunk, i
end
end
end
--- @param lnum integer
--- @param hunks Gitsigns.Hunk.Hunk[]
--- @param forwards boolean
--- @param wrap boolean
--- @return Gitsigns.Hunk.Hunk, integer
function M.find_nearest_hunk(lnum: integer, hunks: {Hunk}, forwards: boolean, wrap: boolean): Hunk, integer
local ret: Hunk --- @type Gitsigns.Hunk.Hunk
local index: integer --- @type integer
local distance: integer = math.huge as integer
if forwards then
for i = 1, #hunks do
local hunk = hunks[i]
local dist = hunk.added.start - lnum
if dist > 0 and dist < distance then
distance = dist
ret = hunk
index = i
end
end
else
for i = #hunks, 1, -1 do
local hunk = hunks[i]
local dist = lnum - hunk.vend
if dist > 0 and dist < distance then
distance = dist
ret = hunk
index = i
end
end
end
if not ret and wrap then
index = forwards and 1 or #hunks
ret = hunks[index as integer]
end
return ret, index
end
--- @param a Gitsigns.Hunk.Hunk[]
--- @param b Gitsigns.Hunk.Hunk[]
--- @return boolean
function M.compare_heads(a: {Hunk}, b: {Hunk}): boolean
if (a == nil) ~= (b == nil) then
return true
elseif a and #a ~= #b then
return true
end
for i, ah in ipairs(a or {}) do
if b[i].head ~= ah.head then
return true
end
end
return false
end
--- @param a Gitsigns.Hunk.Hunk
--- @param b Gitsigns.Hunk.Hunk
--- @return boolean
local function compare_new(a: Hunk, b: Hunk): boolean
if a.added.start ~= b.added.start then
return false
end
if a.added.count ~= b.added.count then
return false
end
for i = 1, a.added.count do
if a.added.lines[i] ~= b.added.lines[i] then
return false
end
end
return true
end
--- Return hunks in a using b's hunks as a filter. Only compare the 'new' section
--- of the hunk.
---
--- Eg. Given:
---
--- a = {
--- 1 = '@@ -24 +25,1 @@',
--- 2 = '@@ -32 +34,1 @@',
--- 3 = '@@ -37 +40,1 @@'
--- }
---
--- b = {
--- 1 = '@@ -26 +25,1 @@'
--- }
---
--- Since a[1] and b[1] introduce the same changes to the buffer (both have
--- +25,1), we exclude this hunk in the output so we return:
---
--- {
--- 1 = '@@ -32 +34,1 @@',
--- 2 = '@@ -37 +40,1 @@'
--- }
---
--- @param a Gitsigns.Hunk.Hunk[]
--- @param b Gitsigns.Hunk.Hunk[]
--- @return Gitsigns.Hunk.Hunk[]?
function M.filter_common(a: {Hunk}, b: {Hunk}): {Hunk}
if not a and not b then
return
end
a, b = a or {}, b or {}
local max_iter = math.max(#a, #b)
local a_i = 1
local b_i = 1
--- @type Gitsigns.Hunk.Hunk[]
local ret: {Hunk} = {}
for _ = 1, max_iter do
local a_h, b_h = a[a_i], b[b_i]
if not a_h then
-- Reached the end of a
break
end
if not b_h then
-- Reached the end of b, add remainder of a
for i = a_i, #a do
ret[#ret+1] = a[i]
end
break
end
if a_h.added.start > b_h.added.start then
-- a pointer is ahead of b; increment b pointer
b_i = b_i + 1
elseif a_h.added.start < b_h.added.start then
-- b pointer is ahead of a; add a_h to ret and increment a pointer
ret[#ret+1] = a_h
a_i = a_i + 1
else -- a_h.start == b_h.start
-- a_h and b_h start on the same line, if hunks have the same changes then
-- skip (filtered) otherwise add a_h to ret. Increment both hunk
-- pointers
-- TODO(lewis6991): Be smarter; if bh intercepts then break down ah.
if not compare_new(a_h, b_h) then
ret[#ret+1] = a_h
end
a_i = a_i + 1
b_i = b_i + 1
end
end
return ret
end
return M

View file

@ -1,582 +0,0 @@
local void = require('gitsigns.async').void
local awrap = require('gitsigns.async').wrap
local scheduler = require('gitsigns.async').scheduler
local gs_cache = require('gitsigns.cache')
local CacheEntry = gs_cache.CacheEntry
local cache = gs_cache.cache
local Signs = require('gitsigns.signs')
local Status = require("gitsigns.status")
local debounce_trailing = require('gitsigns.debounce').debounce_trailing
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
local log = require('gitsigns.debug.log')
local dprint = log.dprint
local dprintf = log.dprintf
local eprint = log.eprint
local subprocess = require('gitsigns.subprocess')
local util = require('gitsigns.util')
local run_diff = require('gitsigns.diff')
local uv = require('gitsigns.uv')
local gs_hunks = require("gitsigns.hunks")
local Hunk = gs_hunks.Hunk
local config = require('gitsigns.config').config
local api = vim.api
local signs_normal: Signs
local signs_staged: Signs
local record M
update : function(bufnr: integer, CacheEntry)
update_debounced : function(bufnr: integer, CacheEntry)
on_lines : function(buf: integer, first: integer, last_orig: integer, last_new: integer): boolean
watch_gitdir : function(bufnr: integer, gitdir: string): vim.loop.FSPollObj
detach : function(bufnr: integer, keep_signs: boolean)
reset_signs : function()
setup : function()
end
local scheduler_if_buf_valid = awrap(function(buf: integer, cb: function)
vim.schedule(function()
if vim.api.nvim_buf_is_valid(buf) then
cb()
end
end)
end, 2)
local function apply_win_signs0(bufnr: integer, signs: Signs, hunks: {Hunk}, top: integer, bot: integer, clear: boolean, untracked: boolean)
if clear then
signs:remove(bufnr) -- Remove all signs
end
for i, hunk in ipairs(hunks or {}) do
-- To stop the sign column width changing too much, if there are signs to be
-- added but none of them are visible in the window, then make sure to add at
-- least one sign. Only do this on the first call after an update when we all
-- the signs have been cleared.
if clear and i == 1 then
signs:add(bufnr, gs_hunks.calc_signs(hunk, hunk.added.start, hunk.added.start, untracked))
end
if top <= hunk.vend and bot >= hunk.added.start then
signs:add(bufnr, gs_hunks.calc_signs(hunk, top, bot, untracked))
end
if hunk.added.start > bot then
break
end
end
end
local function apply_win_signs(bufnr: integer, top: integer, bot: integer, clear: boolean, untracked: boolean)
local bcache = cache[bufnr]
if not bcache then
return
end
apply_win_signs0(bufnr, signs_normal, bcache.hunks, top, bot, clear, untracked)
if signs_staged then
apply_win_signs0(bufnr, signs_staged, bcache.hunks_staged, top, bot, clear, false)
end
end
M.on_lines = function(buf: integer, first: integer, last_orig: integer, last_new: integer): boolean
local bcache = cache[buf]
if not bcache then
dprint('Cache for buffer was nil. Detaching')
return true
end
signs_normal:on_lines(buf, first, last_orig, last_new)
if signs_staged then
signs_staged:on_lines(buf, first, last_orig, last_new)
end
-- Signs in changed regions get invalidated so we need to force a redraw if
-- any signs get removed.
if bcache.hunks and signs_normal:contains(buf, first, last_new) then
-- Force a sign redraw on the next update (fixes #521)
bcache.force_next_update = true
end
if signs_staged then
if bcache.hunks_staged and signs_staged:contains(buf, first, last_new) then
-- Force a sign redraw on the next update (fixes #521)
bcache.force_next_update = true
end
end
M.update_debounced(buf, cache[buf])
end
local ns = api.nvim_create_namespace('gitsigns')
local function apply_word_diff(bufnr: integer, row: integer)
-- Don't run on folded lines
if vim.fn.foldclosed(row+1) ~= -1 then
return
end
local bcache = cache[bufnr]
if not bcache or not bcache.hunks then
return
end
local line = api.nvim_buf_get_lines(bufnr, row, row+1, false)[1]
if not line then
-- Invalid line
return
end
local lnum = row + 1
local hunk = gs_hunks.find_hunk(lnum, bcache.hunks)
if not hunk then
-- No hunk at line
return
end
if hunk.added.count ~= hunk.removed.count then
-- Only word diff if added count == removed
return
end
local pos = lnum - hunk.added.start + 1
local added_line = hunk.added.lines[pos]
local removed_line = hunk.removed.lines[pos]
local _, added_regions = require('gitsigns.diff_int').run_word_diff({removed_line}, {added_line})
local cols = #line
for _, region in ipairs(added_regions) do
local rtype, scol, ecol = region[2], region[3] - 1, region[4] - 1
if ecol == scol then
-- Make sure region is at least 1 column wide so deletes can be shown
ecol = scol + 1
end
local hl_group = rtype == 'add' and 'GitSignsAddLnInline'
or rtype == 'change' and 'GitSignsChangeLnInline'
or 'GitSignsDeleteLnInline'
local opts: {string:any} = {
ephemeral = true,
priority = 1000
}
if ecol > cols and ecol == scol + 1 then
-- delete on last column, use virtual text instead
opts.virt_text = {{' ', hl_group}}
opts.virt_text_pos = 'overlay'
else
opts.end_col = ecol
opts.hl_group = hl_group
end
api.nvim_buf_set_extmark(bufnr, ns, row, scol, opts)
api.nvim__buf_redraw_range(bufnr, row, row+1)
end
end
local ns_rm = api.nvim_create_namespace('gitsigns_removed')
local VIRT_LINE_LEN = 300
local function clear_deleted(bufnr: integer)
local marks = api.nvim_buf_get_extmarks(bufnr, ns_rm, 0, -1, {})
for _, mark in ipairs(marks as {{integer, integer, integer}}) do
api.nvim_buf_del_extmark(bufnr, ns_rm, mark[1])
end
end
function M.show_deleted(bufnr: integer, nsd: integer, hunk: Hunk)
local virt_lines = {}
for i, line in ipairs(hunk.removed.lines) do
local vline = {}
local last_ecol = 1
if config.word_diff then
local regions = require('gitsigns.diff_int').run_word_diff(
{hunk.removed.lines[i]}, {hunk.added.lines[i]})
for _, region in ipairs(regions) do
local rline, scol, ecol = region[1], region[3], region[4]
if rline > 1 then
break
end
vline[#vline+1] = { line:sub(last_ecol, scol-1), 'GitSignsDeleteVirtLn'}
vline[#vline+1] = { line:sub(scol, ecol-1), 'GitSignsDeleteVirtLnInline'}
last_ecol = ecol
end
end
if #line > 0 then
vline[#vline+1] = { line:sub(last_ecol, -1), 'GitSignsDeleteVirtLn'}
end
-- Add extra padding so the entire line is highlighted
local padding = string.rep(' ', VIRT_LINE_LEN-#line)
vline[#vline+1] = { padding, 'GitSignsDeleteVirtLn'}
virt_lines[i] = vline
end
local topdelete = hunk.added.start == 0 and hunk.type == 'delete'
local row = topdelete and 0 or hunk.added.start - 1
api.nvim_buf_set_extmark(bufnr, nsd, row, -1, {
virt_lines = virt_lines,
-- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166
virt_lines_above = hunk.type ~= 'delete' or topdelete,
})
end
function M.show_deleted_in_float(bufnr: integer, nsd: integer, hunk: Hunk): integer
local virt_lines = {}
for i = 1, hunk.removed.count do
virt_lines[i] = { { '', 'Normal' } }
end
local topdelete = hunk.added.start == 0 and hunk.type == 'delete'
local virt_lines_above = hunk.type ~= 'delete' or topdelete
local row = topdelete and 0 or hunk.added.start - 1
api.nvim_buf_set_extmark(bufnr, nsd, row, -1, {
virt_lines = virt_lines,
-- TODO(lewis6991): Note virt_lines_above doesn't work on row 0 neovim/neovim#16166
virt_lines_above = virt_lines_above
})
local bcache = cache[bufnr]
local pbufnr = api.nvim_create_buf(false, true)
api.nvim_buf_set_lines(pbufnr, 0, -1, false, bcache.compare_text)
local cwin = api.nvim_get_current_win()
local width = api.nvim_win_get_width(0)
local opts: vim.api.WinConfig = {
relative = 'win',
win = cwin,
width = width,
height = hunk.removed.count,
anchor = 'SW',
bufpos = { hunk.added.start, 0 }
}
local bufpos_offset = virt_lines_above and not topdelete and 1 or 0
opts.bufpos[1] = opts.bufpos[1] - bufpos_offset
local winid = api.nvim_open_win(pbufnr, false, opts)
-- Align buffer text by accounting for differences in the statuscolumn
local textoff = vim.fn.getwininfo(cwin)[1].textoff
local ptextoff = vim.fn.getwininfo(winid)[1].textoff
local col_offset = textoff - ptextoff
if col_offset ~= 0 then
opts.width = opts.width - col_offset
opts.bufpos[2] = opts.bufpos[2] + col_offset
api.nvim_win_set_config(winid, opts)
end
vim.bo[pbufnr].filetype = vim.bo[bufnr].filetype
vim.bo[pbufnr].bufhidden = 'wipe'
vim.wo[winid].scrolloff = 0
vim.wo[winid].relativenumber = false
api.nvim_win_call(winid, function(): nil
-- Expand folds
vim.cmd('normal '..'zR')
-- Navigate to hunk
vim.cmd('normal '..tostring(hunk.removed.start)..'gg')
vim.cmd('normal '..vim.api.nvim_replace_termcodes('z<CR>', true, false, true))
end)
-- Apply highlights
for i = hunk.removed.start, hunk.removed.start + hunk.removed.count do
api.nvim_buf_set_extmark(pbufnr, nsd, i - 1, 0, {
hl_group = 'GitSignsDeleteVirtLn',
hl_eol = true,
end_row = i,
priority = 1000
})
end
local removed_regions =
require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines)
for _, region in ipairs(removed_regions) do
local start_row = (hunk.removed.start - 1) + (region[1] - 1)
local start_col = region[3] - 1
local end_col = region[4] - 1
api.nvim_buf_set_extmark(pbufnr, nsd, start_row, start_col, {
hl_group = 'GitSignsDeleteVirtLnInline',
end_col = end_col,
end_row = start_row,
priority = 1001
})
end
return winid
end
function M.show_added(bufnr: integer, nsw: integer, hunk: Hunk)
local start_row = hunk.added.start - 1
for offset = 0, hunk.added.count - 1 do
local row = start_row + offset
api.nvim_buf_set_extmark(bufnr, nsw, row, 0, {
end_row = row + 1,
hl_group = 'GitSignsAddPreview',
hl_eol = true,
priority = 1000
})
end
local _, added_regions = require('gitsigns.diff_int').run_word_diff(hunk.removed.lines, hunk.added.lines)
for _, region in ipairs(added_regions) do
local offset, rtype, scol, ecol = region[1] - 1, region[2], region[3] - 1, region[4] - 1
api.nvim_buf_set_extmark(bufnr, nsw, start_row + offset, scol, {
end_col = ecol,
hl_group = rtype == 'add' and 'GitSignsAddInline'
or rtype == 'change' and 'GitSignsChangeInline'
or 'GitSignsDeleteInline',
priority = 1001
})
end
end
local function update_show_deleted(bufnr: integer)
local bcache = cache[bufnr]
clear_deleted(bufnr)
if config.show_deleted then
for _, hunk in ipairs(bcache.hunks or {}) do
M.show_deleted(bufnr, ns_rm, hunk)
end
end
end
local update_cnt = 0
-- Ensure updates cannot be interleaved.
-- Since updates are asynchronous we need to make sure an update isn't performed
-- whilst another one is in progress. If this happens then schedule another
-- update after the current one has completed.
M.update = throttle_by_id(function(bufnr: integer, bcache: CacheEntry)
local __FUNC__ = 'update'
bcache = bcache or cache[bufnr]
if not bcache then
eprint('Cache for buffer '..bufnr..' was nil')
return
end
local old_hunks, old_hunks_staged = bcache.hunks, bcache.hunks_staged
bcache.hunks, bcache.hunks_staged = nil, nil
scheduler_if_buf_valid(bufnr)
local buftext = util.buf_lines(bufnr)
local git_obj = bcache.git_obj
if not bcache.compare_text or config._refresh_staged_on_update then
bcache.compare_text = git_obj:get_show_text(bcache:get_compare_rev())
end
bcache.hunks = run_diff(bcache.compare_text, buftext)
if config._signs_staged_enable then
if not bcache.compare_text_head or config._refresh_staged_on_update then
bcache.compare_text_head = git_obj:get_show_text(bcache:get_staged_compare_rev())
end
local hunks_head = run_diff(bcache.compare_text_head, buftext)
bcache.hunks_staged = gs_hunks.filter_common(hunks_head, bcache.hunks)
end
scheduler_if_buf_valid(bufnr)
-- Note the decoration provider may have invalidated bcache.hunks at this
-- point
if bcache.force_next_update or gs_hunks.compare_heads(bcache.hunks, old_hunks) or
gs_hunks.compare_heads(bcache.hunks_staged, old_hunks_staged) then
-- Apply signs to the window. Other signs will be added by the decoration
-- provider as they are drawn.
apply_win_signs(bufnr, vim.fn.line('w0'), vim.fn.line('w$'), true, git_obj.object_name == nil)
update_show_deleted(bufnr)
bcache.force_next_update = false
api.nvim_exec_autocmds('User', {
pattern = 'GitSignsUpdate',
modeline = false,
})
end
local summary = gs_hunks.get_summary(bcache.hunks)
summary.head = git_obj.repo.abbrev_head
Status:update(bufnr, summary)
update_cnt = update_cnt + 1
dprintf('updates: %s, jobs: %s', update_cnt, subprocess.job_cnt)
end, true)
M.detach = function(bufnr: integer, keep_signs: boolean)
if not keep_signs then
-- Remove all signs
signs_normal:remove(bufnr)
if signs_staged then
signs_staged:remove(bufnr)
end
end
end
local function handle_moved(bufnr: integer, bcache: CacheEntry, old_relpath: string)
local git_obj = bcache.git_obj
local do_update = false
local new_name = git_obj:has_moved()
if new_name then
dprintf('File moved to %s', new_name)
git_obj.relpath = new_name
if not git_obj.orig_relpath then
git_obj.orig_relpath = old_relpath
end
do_update = true
elseif git_obj.orig_relpath then
local orig_file = git_obj.repo.toplevel..util.path_sep..git_obj.orig_relpath
if git_obj:file_info(orig_file).relpath then
dprintf('Moved file reset')
git_obj.relpath = git_obj.orig_relpath
git_obj.orig_relpath = nil
do_update = true
end
else
-- File removed from index, do nothing
end
if do_update then
git_obj.file = git_obj.repo.toplevel..util.path_sep..git_obj.relpath
bcache.file = git_obj.file
git_obj:update_file_info()
scheduler()
local bufexists = vim.fn.bufexists(bcache.file) == 1
local old_name = api.nvim_buf_get_name(bufnr)
if not bufexists then
util.buf_rename(bufnr, bcache.file)
end
local msg = bufexists and 'Cannot rename' or 'Renamed'
dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file)
end
end
function M.watch_gitdir(bufnr: integer, gitdir: string): vim.loop.FSPollObj
if not config.watch_gitdir.enable then
return
end
dprintf('Watching git dir')
local w = uv.new_fs_poll(true)
w:start(gitdir, config.watch_gitdir.interval, void(function(err: string)
local __FUNC__ = 'watcher_cb'
if err then
dprintf('Git dir update error: %s', err)
return
end
dprint('Git dir update')
local bcache = cache[bufnr]
if not bcache then
-- Very occasionally an external git operation may cause the buffer to
-- detach and update the git dir simultaneously. When this happens this
-- handler will trigger but there will be no cache.
dprint('Has detached, aborting')
return
end
local git_obj = bcache.git_obj
git_obj.repo:update_abbrev_head()
scheduler()
Status:update(bufnr, { head = git_obj.repo.abbrev_head})
local was_tracked = git_obj.object_name ~= nil
local old_relpath = git_obj.relpath
git_obj:update_file_info()
if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then
-- File was tracked but is no longer tracked. Must of been removed or
-- moved. Check if it was moved and switch to it.
handle_moved(bufnr, bcache, old_relpath)
end
bcache:invalidate()
M.update(bufnr, bcache)
end))
return w
end
function M.reset_signs()
-- Remove all signs
if signs_normal then
signs_normal:reset()
end
if signs_staged then
signs_staged:reset()
end
end
local function on_win(_, _, bufnr: integer, topline: integer, botline_guess: integer): boolean
local bcache = cache[bufnr]
if not bcache or not bcache.hunks then
return false
end
local botline = math.min(botline_guess, api.nvim_buf_line_count(bufnr))
local untracked = bcache.git_obj.object_name == nil
apply_win_signs(bufnr, topline+1, botline+1, false, untracked)
if not (config.word_diff and config.diff_opts.internal) then
return false
end
end
local function on_line(_, _, bufnr: integer, row: integer)
apply_word_diff(bufnr, row)
end
function M.setup()
-- Calling this before any await calls will stop nvim's intro messages being
-- displayed
api.nvim_set_decoration_provider(ns, {
on_win = on_win,
on_line = on_line,
})
signs_normal = Signs.new(config.signs)
if config._signs_staged_enable then
signs_staged = Signs.new(config._signs_staged, 'staged')
end
M.update_debounced = debounce_trailing(config.update_debounce, void(M.update)) as function(integer, CacheEntry)
end
return M

View file

@ -1,87 +0,0 @@
-- Originated from:
-- https://github.com/norcalli/neovim-plugin/blob/master/lua/neovim-plugin/apply_mappings.lua
local validate = vim.validate
local api = vim.api
local valid_modes: {string:string} = {
n = 'n'; v = 'v'; x = 'x'; i = 'i'; o = 'o'; t = 't'; c = 'c'; s = 's';
-- :map! and :map
['!'] = '!'; [' '] = '';
}
local valid_options: {string:string} = {
buffer = 'boolean',
expr = 'boolean',
noremap = 'boolean',
nowait = 'boolean',
script = 'boolean',
silent = 'boolean',
unique = 'boolean',
}
local function validate_option_keywords(options: table)
for option_name, expected_type in pairs(valid_options) do
local value = options[option_name]
if value then
validate {
[option_name] = { value, expected_type };
}
end
end
end
local function apply_mappings(mappings: {string:any}, bufnr: integer)
validate {
mappings = { mappings, 'table' };
}
local default_options = {}
for key, val in pairs(mappings) do
-- Skip any inline default keywords.
if valid_options[key] then
default_options[key] = val
end
end
for key, opts in pairs(mappings) do
repeat
-- Skip any inline default keywords.
if valid_options[key] then
break
end
local rhs: string
local options: {string:any}
if opts is string then
rhs = opts
options = {}
elseif opts is table then
rhs = opts[1] as string
local boptions = {}
for k in pairs(valid_options) do
boptions[k] = opts[k]
end
options = boptions
else
error(("Invalid type for option rhs: %q = %s"):format(type(opts), vim.inspect(opts)))
end
options = vim.tbl_extend('keep', default_options, options) as {string:any}
validate_option_keywords(options)
local mode, mapping = key:match("^(.)[ ]*(.+)$")
if not mode or not valid_modes[mode] then
error("Invalid mode specified for keymapping. mode="..mode)
end
-- In case users haven't updated their config.
options.buffer = nil
api.nvim_buf_set_keymap(bufnr, mode, mapping, rhs, options)
until true
end
end
return apply_mappings

View file

@ -1,16 +0,0 @@
local type MsgFun = function(string, ...: any)
local record M
warn : MsgFun
error : MsgFun
end
M.warn = vim.schedule_wrap(function(s: string, ...:any)
vim.notify(s:format(...), vim.log.levels.WARN, {title = 'gitsigns'})
end) as MsgFun
M.error = vim.schedule_wrap(function(s: string, ...:any)
vim.notify(s:format(...), vim.log.levels.ERROR, {title = 'gitsigns'})
end) as MsgFun
return M

View file

@ -1,245 +0,0 @@
local record popup
type LinesSpec = {{{string,string|{HlMark}}}}
record HlMark
hl_group : string
start_row : integer
end_row : integer
start_col : integer
end_col : integer
end
end
local HlMark = popup.HlMark
local api = vim.api
local function bufnr_calc_width(bufnr: integer, lines: {string}): integer
return api.nvim_buf_call(bufnr, function(): integer
local width = 0
for _, l in ipairs(lines) do
if vim.fn.type(l) == vim.v.t_string then
local len = vim.fn.strdisplaywidth(l)
if len > width then
width = len
end
end
end
return width + 1 -- Add 1 for some miinor padding
end)
end
-- Expand height until all lines are visible to account for wrapped lines.
local function expand_height(winid: integer, nlines: integer)
local newheight = 0
for _ = 0, 50 do
local winheight = api.nvim_win_get_height(winid)
if newheight > winheight then
-- Window must be max height
break
end
local wd = api.nvim_win_call(winid, function(): integer
return vim.fn.line('w$')
end)
if wd >= nlines then
break
end
newheight = winheight+nlines-wd
api.nvim_win_set_height(winid, newheight)
end
end
local function offset_hlmarks(hlmarks: {HlMark}, row_offset: integer)
for _, h in ipairs(hlmarks) do
if h.start_row then
h.start_row = h.start_row + row_offset
end
if h.end_row then
h.end_row = h.end_row + row_offset
end
end
end
local function process_linesspec(fmt: popup.LinesSpec): {string}, {HlMark}
local lines: {string} = {}
local hls: {HlMark} = {}
local row = 0
for _, section in ipairs(fmt) do
local sec = {}
local pos = 0
for _, part in ipairs(section) do
local text = part[1]
local hl = part[2]
sec[#sec+1] = text
local srow = row
local scol = pos
local ts = vim.split(text, '\n')
if #ts > 1 then
pos = 0
row = row + #ts - 1
else
pos = pos + #text
end
if hl is string then
hls[#hls+1] = {
hl_group = hl,
start_row = srow,
end_row = row,
start_col = scol,
end_col = pos,
}
else -- hl is {HlMark}
offset_hlmarks(hl, srow)
vim.list_extend(hls, hl)
end
end
for _, l in ipairs(vim.split(table.concat(sec, ''), '\n')) do
lines[#lines+1] = l
end
row = row + 1
end
return lines, hls
end
local function close_all_but(id: string)
for _, winid in ipairs(api.nvim_list_wins()) do
if vim.w[winid].gitsigns_preview ~= nil and
vim.w[winid].gitsigns_preview ~= id then
pcall(api.nvim_win_close, winid, true)
end
end
end
function popup.close(id: string)
for _, winid in ipairs(api.nvim_list_wins()) do
if vim.w[winid].gitsigns_preview == id then
pcall(api.nvim_win_close, winid, true)
end
end
end
function popup.create0(lines: {string}, opts: {string:any}, id: string): integer, integer
-- Close any popups not matching id
close_all_but(id)
local ts = vim.bo.tabstop
local bufnr = api.nvim_create_buf(false, true)
assert(bufnr, "Failed to create buffer")
-- In case nvim was opened with '-M'
vim.bo[bufnr].modifiable = true
api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
vim.bo[bufnr].modifiable = false
-- Set tabstop before calculating the buffer width so that the correct width
-- is calculated
vim.bo[bufnr].tabstop = ts
local opts1 = vim.deepcopy(opts or {})
opts1.height = opts1.height or #lines -- Guess, adjust later
opts1.width = opts1.width or bufnr_calc_width(bufnr, lines)
local winid = api.nvim_open_win(bufnr, false, opts1)
vim.w[winid].gitsigns_preview = id or true
if not opts.height then
expand_height(winid, #lines)
end
if opts1.style == 'minimal' then
-- If 'signcolumn' = auto:1-2, then a empty signcolumn will appear and cause
-- line wrapping.
vim.wo[winid].signcolumn = 'no'
end
-- Close the popup when navigating to any window which is not the preview
-- itself.
local group = 'gitsigns_popup'
local group_id = api.nvim_create_augroup(group, {})
local old_cursor = api.nvim_win_get_cursor(0)
api.nvim_create_autocmd({'CursorMoved', 'CursorMovedI'}, {
group = group_id,
callback = function()
local cursor = api.nvim_win_get_cursor(0)
-- Did the cursor REALLY change (neovim/neovim#12923)
if (old_cursor[1] ~= cursor[1] or old_cursor[2] ~= cursor[2])
and api.nvim_get_current_win() ~= winid then
-- Clear the augroup
api.nvim_create_augroup(group, {})
pcall(api.nvim_win_close, winid, true)
return
end
old_cursor = cursor
end
})
api.nvim_create_autocmd('WinClosed', {
pattern = tostring(winid),
group = group_id,
callback = function()
-- Clear the augroup
api.nvim_create_augroup(group, {})
end
})
-- update window position to follow the cursor when scrolling
api.nvim_create_autocmd('WinScrolled', {
buffer = api.nvim_get_current_buf(),
group = group_id,
callback = function()
if api.nvim_win_is_valid(winid) then
api.nvim_win_set_config(winid, opts1)
end
end
})
return winid, bufnr
end
local ns = api.nvim_create_namespace('gitsigns_popup')
function popup.create(lines_spec: popup.LinesSpec, opts: {string:any}, id: string): integer, integer
local lines, highlights = process_linesspec(lines_spec)
local winid, bufnr = popup.create0(lines, opts, id)
for _, hl in ipairs(highlights) do
local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, ns, hl.start_row, hl.start_col or 0, {
hl_group = hl.hl_group,
end_row = hl.end_row,
end_col = hl.end_col,
hl_eol = true,
})
if not ok then
error(vim.inspect(hl)..'\n'..err)
end
end
return winid, bufnr
end
function popup.is_open(id: string): integer
for _, winid in ipairs(api.nvim_list_wins()) do
if vim.w[winid].gitsigns_preview == id then
return winid
end
end
return nil
end
function popup.focus_open(id: string): integer
local winid = popup.is_open(id)
if winid then
api.nvim_set_current_win(winid)
end
return winid
end
return popup

View file

@ -1,28 +0,0 @@
local api = vim.api
local record M
mk_repeatable: function<F>(fn: F): F
repeat_action: function()
end
function M.mk_repeatable(fn: function): function
return function(...: any)
local args = {...}
local nargs = select('#', ...)
vim.go.operatorfunc = "v:lua.require'gitsigns.repeat'.repeat_action"
M.repeat_action = function()
fn(unpack(args, 1, nargs))
if vim.fn.exists('*repeat#set') == 1 then
local action = api.nvim_replace_termcodes(
string.format('<cmd>call %s()<cr>', vim.go.operatorfunc),
true, true, true)
vim.fn['repeat#set'](action, -1)
end
end
vim.cmd'normal! g@l'
end
end
return M

View file

@ -1,43 +0,0 @@
local config = require('gitsigns.config').config
local SignsConfig = require('gitsigns.config').Config.SignsConfig
local dprint = require('gitsigns.debug.log').dprint
local B = require('gitsigns.signs.base')
-- local function capitalise_word(x: string): string
-- return x:sub(1, 1):upper()..x:sub(2)
-- end
function B.new(cfg: SignsConfig, name: string): B
local __FUNC__ = 'signs.init'
local C: B
if config._extmark_signs then
dprint('Using extmark signs')
C = require('gitsigns.signs.extmarks')
else
dprint('Using vimfn signs')
C = require('gitsigns.signs.vimfn')
end
local hls = (name == 'staged' and config._signs_staged or config.signs) as {B.SignType:B.HlDef}
-- Add when config.signs.*.[hl,numhl,linehl] are removed
-- for _, t in ipairs {
-- 'add',
-- 'change',
-- 'delete',
-- 'topdelete',
-- 'changedelete',
-- 'untracked',
-- } do
-- local hl = string.format('GitSigns%s%s', name, capitalise_word(t))
-- obj.hls[t] = {
-- hl = hl,
-- numhl = hl..'Nr',
-- linehl = hl..'Ln',
-- }
-- end
return C._new(cfg, hls, name)
end
return B

View file

@ -1,46 +0,0 @@
local SignsConfig = require('gitsigns.config').Config.SignsConfig
local record M
enum SignType
"add"
"delete"
"change"
"topdelete"
"changedelete"
"untracked"
end
record Sign
type: SignType
count: integer
lnum: integer
end
record HlDef
hl: string
numhl: string
linehl: string
end
hls: {SignType:HlDef}
name: string
group: string
config: SignsConfig
-- Used by signs/extmarks.tl
ns: integer
-- Used by signs/vimfn.tl
placed: {integer:{integer:Sign}}
new : function(cfg: SignsConfig, name: string): M
_new : function(cfg: SignsConfig, hls: {SignType:HlDef}, name: string): M
remove : function(M, bufnr: integer, start_lnum: integer, end_lnum: integer)
add : function(M, bufnr: integer, signs: {M.Sign})
contains : function(M, bufnr: integer, start: integer, last: integer): boolean
on_lines : function(M, bufnr: integer, first: integer, last_orig: integer, last_new: integer)
reset : function(M)
end
return M

View file

@ -1,89 +0,0 @@
local api = vim.api
local SignsConfig = require('gitsigns.config').Config.SignsConfig
local config = require('gitsigns.config').config
local B = require('gitsigns.signs.base')
local M: B = {}
local group_base = 'gitsigns_extmark_signs_'
function M._new(cfg: SignsConfig, hls: {M.SignType:M.HlDef}, name: string): B
local self = setmetatable({} as B, {__index = M})
self.config = cfg
self.hls = hls
self.group = group_base..(name or '')
self.ns = api.nvim_create_namespace(self.group)
return self
end
function M:on_lines(buf: integer, _: integer, last_orig: integer, last_new: integer)
-- Remove extmarks on line deletions to mimic
-- the behaviour of vim signs.
if last_orig > last_new then
self:remove(buf, last_new+1, last_orig)
end
end
function M:remove(bufnr: integer, start_lnum: integer, end_lnum: integer)
if start_lnum then
api.nvim_buf_clear_namespace(bufnr, self.ns, start_lnum-1, end_lnum or start_lnum)
else
api.nvim_buf_clear_namespace(bufnr, self.ns, 0, -1)
end
end
function M:add(bufnr: integer, signs: {M.Sign})
if not config.signcolumn and not config.numhl and not config.linehl then
-- Don't place signs if it won't show anything
return
end
for _, s in ipairs(signs) do
if not self:contains(bufnr, s.lnum) then
local cs = self.config[s.type]
local text = cs.text
if config.signcolumn and cs.show_count and s.count then
local count = s.count
local cc = config.count_chars
local count_char = cc[count] or cc['+'] or ''
text = cs.text..count_char
end
local hls = self.hls[s.type]
local ok, err = pcall(api.nvim_buf_set_extmark, bufnr, self.ns, s.lnum-1, -1, {
id = s.lnum,
sign_text = config.signcolumn and text or '',
priority = config.sign_priority,
sign_hl_group = hls.hl,
number_hl_group = config.numhl and hls.numhl or nil,
line_hl_group = config.linehl and hls.linehl or nil,
})
if not ok and config.debug_mode then
vim.schedule(function()
error(table.concat({
string.format('Error placing extmark on line %d', s.lnum),
err
}, '\n'))
end)
end
end
end
end
function M:contains(bufnr: integer, start: integer, last: integer): boolean
local marks = api.nvim_buf_get_extmarks(
bufnr, self.ns, {start-1, 0}, {last or start, 0}, {limit=1})
return #marks > 0
end
function M:reset()
for _, buf in ipairs(api.nvim_list_bufs()) do
self:remove(buf)
end
end
return M

View file

@ -1,163 +0,0 @@
local fn = vim.fn
local SignsConfig = require('gitsigns.config').Config.SignsConfig
local config = require('gitsigns.config').config
local emptytable = require('gitsigns.util').emptytable
local B = require('gitsigns.signs.base')
local M: B = {}
-- The internal representation of signs in Neovim is a linked list which is slow
-- to index. To improve efficiency we add an abstraction layer to the signs API
-- which keeps track of which signs have already been placed in the buffer.
--
-- This allows us to:
-- - efficiently query placed signs.
-- - skip adding a sign if it has already been placed.
local function capitalise_word(x: string): string
return x:sub(1, 1):upper()..x:sub(2)
end
local sign_define_cache: {string:table} = {}
local sign_name_cache: {string:string} = {}
local function get_sign_name(name: string, stype: string): string
local key = name..stype
if not sign_name_cache[key] then
sign_name_cache[key] = string.format(
'%s%s%s', 'GitSigns', capitalise_word(key), capitalise_word(stype))
end
return sign_name_cache[key]
end
local function sign_get(name: string): table
if not sign_define_cache[name] then
local s = fn.sign_getdefined(name)
if not vim.tbl_isempty(s) then
sign_define_cache[name] = s
end
end
return sign_define_cache[name]
end
local function define_sign(name: string, opts: {string:any}, redefine: boolean)
if redefine then
sign_define_cache[name] = nil
fn.sign_undefine(name)
fn.sign_define(name, opts)
elseif not sign_get(name) then
fn.sign_define(name, opts)
end
end
local function define_signs(obj: B, redefine: boolean)
-- Define signs
for stype, cs in pairs(obj.config) do
local hls = obj.hls[stype]
define_sign(get_sign_name(obj.name, stype), {
texthl = hls.hl,
text = config.signcolumn and cs.text or nil,
numhl = config.numhl and hls.numhl or nil,
linehl = config.linehl and hls.linehl or nil
}, redefine)
end
end
local group_base = 'gitsigns_vimfn_signs_'
function M._new(cfg: SignsConfig, hls: {M.SignType:M.HlDef}, name: string): B
local self = setmetatable({} as B, {__index = M})
self.name = name or ''
self.group = group_base..(name or '')
self.config = cfg
self.hls = hls
self.placed = emptytable()
define_signs(self, false)
return self
end
function M:on_lines(_: integer, _: integer, _: integer, _: integer)
end
function M:remove(bufnr: integer, start_lnum: integer, end_lnum: integer)
end_lnum = end_lnum or start_lnum
if start_lnum then
for lnum = start_lnum, end_lnum do
self.placed[bufnr][lnum] = nil
fn.sign_unplace(self.group, {buffer = bufnr, id = lnum})
end
else
self.placed[bufnr] = nil
fn.sign_unplace(self.group, {buffer = bufnr})
end
end
function M:add(bufnr: integer, signs: {M.Sign})
if not config.signcolumn and not config.numhl and not config.linehl then
-- Don't place signs if it won't show anything
return
end
local to_place = {}
for _, s in ipairs(signs) do
local sign_name = get_sign_name(self.name, s.type)
local cs = self.config[s.type]
if config.signcolumn and cs.show_count and s.count then
local count = s.count
local cc = config.count_chars
local count_suffix = cc[count] and tostring(count) or (cc['+'] and 'Plus') or ''
local count_char = cc[count] or cc['+'] or ''
local hls = self.hls[s.type]
sign_name = sign_name..count_suffix
define_sign(sign_name, {
texthl = hls.hl,
text = config.signcolumn and cs.text..count_char or '',
numhl = config.numhl and hls.numhl or nil,
linehl = config.linehl and hls.linehl or nil
})
end
if not self.placed[bufnr][s.lnum] then
local sign = {
id = s.lnum,
group = self.group,
name = sign_name,
buffer = bufnr,
lnum = s.lnum,
priority = config.sign_priority
}
self.placed[bufnr][s.lnum] = s
to_place[#to_place+1] = sign
end
end
if #to_place > 0 then
fn.sign_placelist(to_place)
end
end
function M:contains(bufnr: integer, start: integer, last: integer): boolean
for i = start+1, last+1 do
if self.placed[bufnr][i] then
return true
end
end
return false
end
function M:reset()
self.placed = emptytable()
fn.sign_unplace(self.group)
define_signs(self, true)
end
return M

View file

@ -1,46 +0,0 @@
local api = vim.api
local record StatusObj
added : integer
removed : integer
changed : integer
head : string
root : string
gitdir : string
end
local Status = {
StatusObj = StatusObj,
}
function Status:update(bufnr: integer, status: StatusObj)
if not api.nvim_buf_is_loaded(bufnr) then
return
end
local bstatus = vim.b[bufnr].gitsigns_status_dict
if bstatus then
status = vim.tbl_extend('force', bstatus as table, status as table) as StatusObj
end
vim.b[bufnr].gitsigns_head = status.head or ''
vim.b[bufnr].gitsigns_status_dict = status
local config = require('gitsigns.config').config
vim.b[bufnr].gitsigns_status = config.status_formatter(status)
end
function Status:clear(bufnr: integer)
if not api.nvim_buf_is_loaded(bufnr) then
return
end
vim.b[bufnr].gitsigns_head = nil
vim.b[bufnr].gitsigns_status_dict = nil
vim.b[bufnr].gitsigns_status = nil
end
function Status:clear_diff(bufnr: integer)
self:update(bufnr, { added = 0, removed = 0, changed = 0 })
end
return Status

View file

@ -1,119 +0,0 @@
local log = require("gitsigns.debug.log")
local guv = require("gitsigns.uv")
local uv = vim.loop
local record M
job_cnt: integer
record JobSpec
command: string
args: {string}
cwd: string
writer: {string} | string
end
end
M.job_cnt = 0
--- @param ... uv_pipe_t
local function try_close(...: uv.Pipe)
for i = 1, select('#', ...) do
local pipe = select(i, ...)
if pipe and not pipe:is_closing() then
pipe:close()
end
end
end
--- @param pipe uv_pipe_t
--- @param x string[]|string
local function handle_writer(pipe: uv.Pipe, x: {string} | string)
if x is {string} then
for i, v in ipairs(x) do
pipe:write(v)
if i ~= #(x as {string}) then
pipe:write("\n")
else
pipe:write("\n", function()
try_close(pipe)
end)
end
end
elseif x then
-- write is string
pipe:write(x, function()
try_close(pipe)
end)
end
end
--- @param pipe uv_pipe_t
--- @param output string[]
local function handle_reader(pipe: uv.Pipe, output: {string})
pipe:read_start(function(err: string, data: string)
if err then
log.eprint(err)
end
if data then
output[#output+1] = data
else
try_close(pipe)
end
end)
end
--- @param obj table
--- @param callback fun(_: integer, _: integer, _: string?, _: string?)
function M.run_job(obj: M.JobSpec, callback: function(integer, integer, string, string))
local __FUNC__ = 'run_job'
if log.debug_mode then
local cmd: string = obj.command..' '..table.concat(obj.args, ' ')
log.dprint(cmd)
end
local stdout_data: {string} = {}
local stderr_data: {string} = {}
local stdout = guv.new_pipe(false)
local stderr = guv.new_pipe(false)
local stdin: uv.Pipe
if obj.writer then
stdin = guv.new_pipe(false)
end
--- @type uv_process_t?, integer|string
local handle, _pid: uv.Process, integer
handle, _pid = vim.loop.spawn(obj.command, {
args = obj.args,
stdio = { stdin, stdout, stderr },
cwd = obj.cwd
},
function(code: integer, signal: integer)
if handle then
handle:close()
end
stdout:read_stop()
stderr:read_stop()
try_close(stdin, stdout, stderr)
local stdout_result = #stdout_data > 0 and table.concat(stdout_data) or nil
local stderr_result = #stderr_data > 0 and table.concat(stderr_data) or nil
callback(code, signal, stdout_result, stderr_result)
end
)
if not handle then
try_close(stdin, stdout, stderr)
error(debug.traceback("Failed to spawn process: " .. vim.inspect(obj)))
end
handle_reader(stdout, stdout_data)
handle_reader(stderr, stderr_data)
handle_writer(stdin, obj.writer)
M.job_cnt = M.job_cnt + 1
end
return M

View file

@ -1,35 +0,0 @@
local record M
_tests: {string:function}
end
local function eq(act: any, exp: any)
assert(act == exp, string.format('%s != %s', act, exp))
end
M._tests = {}
M._tests.expand_format = function()
local util = require'gitsigns.util'
assert('hello % world % 2021' == util.expand_format('<var1> % <var2> % <var_time:%Y>' , {
var1 = 'hello', var2 = 'world', var_time = 1616838297 }))
end
M._tests.test_args = function()
local parse_args = require'gitsigns.cli.argparse'.parse_args
local pos_args, named_args = parse_args('hello there key=value, key1="a b c"')
eq(pos_args[1], 'hello')
eq(pos_args[2], 'there')
eq(named_args.key, 'value,')
eq(named_args.key1, 'a b c')
pos_args, named_args = parse_args('base=HEAD~1 posarg')
eq(named_args.base, 'HEAD~1')
eq(pos_args[1], 'posarg')
end
return M

View file

@ -1,223 +0,0 @@
local record M
type FmtInfo = {string:string|integer|{string}}
path_sep: string
end
function M.path_exists(path: string): boolean
return vim.loop.fs_stat(path) and true or false
end
local jit_os: string --- @type string
if jit then
jit_os = jit.os:lower()
end
local is_unix: boolean = false
if jit_os then
is_unix = jit_os == 'linux' or jit_os == 'osx' or jit_os == 'bsd'
else
local binfmt = package.cpath:match("%p[\\|/]?%p(%a+)")
is_unix = binfmt ~= "dll"
end
--- @param file string
--- @return string
function M.dirname(file: string): string
return file:match(string.format('^(.+)%s[^%s]+', M.path_sep, M.path_sep))
end
--- @param file string
--- @return string[]
function M.file_lines(file: string): {string}
local text: {string} = {} --- @type string[]
for line in io.lines(file) do
text[#text+1] = line
end
return text
end
M.path_sep = package.config:sub(1, 1)
--- @param bufnr integer
--- @return string[]
function M.buf_lines(bufnr: integer): {string}
-- nvim_buf_get_lines strips carriage returns if fileformat==dos
local buftext: {string} = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
if vim.bo[bufnr].fileformat == 'dos' then
for i = 1, #buftext do
buftext[i] = buftext[i]..'\r'
end
end
return buftext
end
--- @param buf integer
local function delete_alt(buf: integer)
local alt = vim.api.nvim_buf_call(buf, function(): integer
return vim.fn.bufnr('#')
end) as integer
if alt ~= buf and alt ~= -1 then
pcall(vim.api.nvim_buf_delete, alt, {force=true})
end
end
--- @param bufnr integer
--- @param name string
function M.buf_rename(bufnr: integer, name: string)
vim.api.nvim_buf_set_name(bufnr, name)
delete_alt(bufnr)
end
--- @param bufnr integer
--- @param start_row integer
--- @param end_row integer
--- @param lines string[]
function M.set_lines(bufnr: integer, start_row: integer, end_row: integer, lines: {string})
if vim.bo[bufnr].fileformat == 'dos' then
for i = 1, #lines do
lines[i] = lines[i]:gsub('\r$', '')
end
end
vim.api.nvim_buf_set_lines(bufnr, start_row, end_row, false, lines)
end
--- @return string
function M.tmpname(): string
if is_unix then
return os.tmpname()
end
return vim.fn.tempname()
end
--- @param timestamp number
--- @return string
function M.get_relative_time(timestamp: number): string
local current_timestamp = os.time()
local elapsed = current_timestamp - timestamp
if elapsed == 0 then
return 'a while ago'
end
local minute_seconds = 60
local hour_seconds = minute_seconds * 60
local day_seconds = hour_seconds * 24
local month_seconds = day_seconds * 30
local year_seconds = month_seconds * 12
local to_relative_string = function(time: number, divisor: number, time_word: string): string
local num = math.floor(time / divisor)
if num > 1 then
time_word = time_word .. 's'
end
return num .. ' ' .. time_word .. ' ago'
end
if elapsed < minute_seconds then
return to_relative_string(elapsed, 1, 'second')
elseif elapsed < hour_seconds then
return to_relative_string(elapsed, minute_seconds, 'minute')
elseif elapsed < day_seconds then
return to_relative_string(elapsed, hour_seconds, 'hour')
elseif elapsed < month_seconds then
return to_relative_string(elapsed, day_seconds, 'day')
elseif elapsed < year_seconds then
return to_relative_string(elapsed, month_seconds, 'month')
else
return to_relative_string(elapsed, year_seconds, 'year')
end
end
--- @generic T
--- @param x T[]
--- @return T[]
function M.copy_array<T>(x: {T}): {T}
local r = {}
for i, e in ipairs(x) do
r[i] = e
end
return r
end
--- Strip '\r' from the EOL of each line only if all lines end with '\r'
--- @param xs0 string[]
--- @return string[]
function M.strip_cr(xs0: {string}): {string}
for i = 1, #xs0 do
if xs0[i]:sub(-1) ~= '\r' then
-- don't strip, return early
return xs0
end
end
-- all lines end with '\r', need to strip
local xs = vim.deepcopy(xs0)
for i = 1, #xs do
xs[i] = xs[i]:sub(1, -2)
end
return xs
end
function M.calc_base(base: string): string
if base and base:sub(1, 1):match('[~\\^]') then
base = 'HEAD'..base
end
return base
end
function M.emptytable<T>(): T
return setmetatable({} as T, {
__index = function(t: table, k: any): any
t[k] = {}
return t[k]
end
})
end
local function expand_date(fmt: string, time: integer): string
if fmt == '%R' then
return M.get_relative_time(time)
end
return os.date(fmt, time)
end
---@param fmt string
---@param info table
---@param reltime boolean Use relative time as the default date format
---@return string
function M.expand_format(fmt: string, info: M.FmtInfo, reltime: boolean): string
local ret = {} --- @type string[]
for _ = 1, 20 do -- loop protection
-- Capture <name> or <name:format>
local scol, ecol, match, key, time_fmt = fmt:find('(<([^:>]+):?([^>]*)>)')
if not match then
break
end
ret[#ret+1], fmt = fmt:sub(1, scol-1), fmt:sub(ecol+1)
local v = info[key]
if v then
if v is {string} then
v = table.concat(v, '\n')
end
if vim.endswith(key, '_time') then
if time_fmt == '' then
time_fmt = reltime and '%R' or '%Y-%m-%d'
end
v = expand_date(time_fmt, v as integer)
end
match = tostring(v)
end
ret[#ret+1] = match
end
ret[#ret+1] = fmt
return table.concat(ret, '')
end
return M

View file

@ -1,80 +0,0 @@
local uv = vim.loop
local record M
handles: {integer:{uv.Handle, boolean, string}}
end
--- @type table<integer,{[1]: uv_handle_t, [2]: boolean, [3]: string}>
local handles: {integer:{uv.Handle, boolean, string}} = {}
M.handles = handles
function M.print_handles()
local none = true
for _, e in pairs(handles) do
local handle, longlived, tr = e[1], e[2], e[3]
if handle and not longlived and not handle:is_closing() then
print('')
print(tr)
none = false
end
end
if none then
print('No active handles')
end
end
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
for _, e in pairs(handles) do
local handle = e[1]
if handle and not handle:is_closing() then
handle:close()
end
end
end
})
--- @param longlived boolean
--- @return uv_timer_t?
function M.new_timer(longlived: boolean): uv.Timer
local r = uv.new_timer()
if r then
table.insert(handles, {r as uv.Handle, longlived, debug.traceback()})
end
return r
end
--- @param longlived boolean
--- @return uv_fs_poll_t?
function M.new_fs_poll(longlived: boolean): uv.FSPollObj
local r = uv.new_fs_poll()
if r then
table.insert(handles, {r as uv.Handle, longlived, debug.traceback()})
end
return r
end
--- @param ipc boolean
--- @return uv_pipe_t?
function M.new_pipe(ipc: boolean): uv.Pipe
local r = uv.new_pipe(ipc)
if r then
table.insert(handles, {r as uv.Handle, false, debug.traceback()})
end
return r
end
--- @param cmd string
--- @param opts uv.aliases.spawn_options
--- @param on_exit fun(_: integer, _, integer): uv_process_t, integer
--- @return uv_process_t?, string|integer
function M.spawn(cmd: string, opts: uv.SpawnOpts, on_exit: function(integer, integer)): uv.Process, integer
local handle, pid = uv.spawn(cmd, opts, on_exit)
if handle then
table.insert(handles, {handle as uv.Handle, false, cmd..' '..vim.inspect(opts)})
end
return handle, pid
end
return M

View file

@ -1,10 +0,0 @@
return {
gen_target = '5.1',
gen_compat = 'off',
global_env_def = 'types',
include_dir = {
'types', 'teal',
},
source_dir = 'teal',
build_dir = "lua",
}

View file

@ -1,25 +0,0 @@
local record Cdefs
xdl_diff: (function(...:any): number)
end
-- C callback
local record CCB
free: function(CCB)
set: function(CCB, function)
end
local record ffi
cdef: function(string)
load: function(string): any
new: function(string, ...:any): any
string: function(any, number): string
gc: function<T>(T, function): T
C: Cdefs
cast: function(string, any): CCB
errno: function(): integer
copy: function
metatype: function(string, any)
end
return ffi

View file

@ -1,5 +0,0 @@
local record Trouble
open: function(mode: string)
end
return Trouble

View file

@ -1,22 +0,0 @@
global _: any
global _TEST: boolean
global loadstring: function(string): (function(): any)
global unpack: function<T>({T}, number, number): T...
global getfenv: function(function): table
global record jit
arch: string
os: string
version: string
version_num: number
off: function()
off: function(function)
on: function()
status: function(): boolean
end
global vim = require('vim')

View file

@ -1,237 +0,0 @@
local api = require 'vim.api'
local fn = require 'vim.fn'
local uv = require 'vim.uv'
local record M
api: api
fn: fn
call: function(string, ...:any)
cmd: function(string): any
deepcopy: function<T>(T): T
defer_fn: function(function, integer): uv.Timer
type DiffResult = {integer, integer, integer, integer}
-- Assume result_type == 'indices'
diff: function(string|{string}, string|{string}, table): {DiffResult}
env: {string:string}
iconv: function(string, string, string): string
is_callable: function(any): boolean
record go
operatorfunc: string
end
record o
diffopt: string
eventignore: string
shortmess: string
splitright: boolean
updatetime: number
wrapscan: boolean
end
record WinOption
{WinOption}
diff: boolean
signcolumn: string
scrolloff: integer
relativenumber: boolean
end
wo: WinOption
record BufOption
{BufOption}
fileformat: string
fileencoding: string
filetype: string
modifiable: boolean
modified: boolean
tabstop: integer
enum BufHidden
'' 'hide' 'unload' 'delete' 'wipe'
end
bufhidden: BufHidden
enum BufType
'' 'acwrite' 'help' 'nofile' 'nowrite' 'quickfix' 'terminal' 'prompt'
end
buftype: BufType
end
bo: BufOption
record BufVar
{BufVar}
changedtick: integer
gitsigns_head: string
gitsigns_status_dict: {string:any}
gitsigns_status: string
gitsigns_blame_line_dict: {string:any}
gitsigns_blame_line: string
end
b: BufVar
record WinVar
{WinVar}
gitsigns_preview: string|boolean
end
w: WinVar
record g
gitsigns_head: string
loaded_fugitive: integer
end
record v
vim_did_enter: integer
t_string: integer
end
record opt
record Opt<T>
get: function<T>(Opt<T>): T
end
diffopt: Opt<{string}>
foldopen: Opt<{string}>
shortmess: Opt<{string:boolean}>
wrapscan: Opt<boolean>
end
record lsp
record util
close_preview_autocmd: function ({string}, number)
end
end
loop: uv
in_fast_event: function(): boolean
list_extend: function<T>({T}, {T}, integer, integer): {T}
list_slice: function<T>({T}, integer, integer): {T}
record keymap
record Options
buffer: boolean|integer
expr: boolean
end
set: function(string|{string}, string, string|function, Options)
end
record log
record levels
WARN: integer
ERROR: integer
INFO: integer
DEBUG: integer
end
end
notify: function(string, integer, table)
pretty_print: function(any)
split: function(string, string): {string}
split: function(string, string, boolean): {string}
gsplit: function(string, string, boolean): function(): string
pesc: function(string): string
record Regex
userdata
match_str: function(Regex, string): integer, integer
end
regex: function(string): Regex
startswith: function(string, string): boolean
endswith: function(string, string): boolean
schedule_wrap: function(function()): function()
schedule_wrap: function(function(...:any): any...): function(...:any): any...
schedule: function(function)
validate: function({string:{any}})
trim: function(string): string
enum ExtendBehavior
'error'
'keep'
'force'
end
tbl_add_reverse_lookup: function<K,I>({K:I}): {I:K}
tbl_contains: function(table, any): boolean
tbl_count: function(table): integer
tbl_deep_extend: function(ExtendBehavior, table, table, ...: table): table
tbl_extend: function(ExtendBehavior, table, table, ...: table): table
tbl_filter: function<T>((function(any): boolean), {T}): {T}
tbl_isempty: function(table): boolean
tbl_islist: function(table): boolean
tbl_keys: function<K,V>({K:V}): {K}
tbl_map: function(function, table): table
record InspectOptions
depth: number
newline: string
indent: string
process: function
end
record inspect
METATABLE: any
KEY: any
metamethod __call: function(inspect, any, InspectOptions): string
metamethod __call: function(inspect, any): string
end
wait: function(number, function, number, boolean)
record ui
input: function({string:any}, function(string))
record SelectOpts<T>
prompt: string
format_item: function(T): string
kind: string
end
select: function<T>({T}, SelectOpts<T>, on_choice: function(T, idx: integer))
end
record VersionDetails
api_compatible: integer
api_level: integer
api_prerelease: boolean
major: integer
minor: integer
patch: integer
end
version: function(): VersionDetails
record mpack
encode: function(any): string
decode: function(string): any
end
is_thread: function(): boolean
record fs
find: function(string|{string}, table): {string}
end
end
return M

View file

@ -1,288 +0,0 @@
local record M
record UserCmdParams
args: string
bang: boolean
line1: integer
line2: integer
range: integer
count: number
reg: string
mods: string
record Mods
browse : boolean
confirm : boolean
emsg_silent : boolean
hide : boolean
keepalt : boolean
keepjumps : boolean
keepmarks : boolean
keeppatterns : boolean
lockmarks : boolean
noautocmd : boolean
noswapfile : boolean
sandbox : boolean
silent : boolean
tab : integer
verbose : integer
vertical : boolean
enum Split ''
'aboveleft'
'belowright'
'topleft'
'botright'
end
split : Split
end
smods: Mods
end
record UserCmdOpts
nargs: string|integer
range: boolean|string|integer
count: boolean|integer
addr: string
bang: boolean
bar: boolean
register: boolean
force: boolean
complete: string|function(arglead: string, line: string): {string}
end
nvim_create_user_command : function(string, function(UserCmdParams), UserCmdOpts)
nvim_buf_attach : function(integer, boolean, {string:any}): boolean
nvim_buf_call : function<T>(integer, function(): T): T
nvim_buf_clear_namespace : function(integer, number, number, number)
nvim_buf_del_extmark : function(integer, number, number): boolean
nvim_buf_delete : function(integer, {string:boolean})
nvim_buf_get_extmark_by_id : function(integer, number, number, table): {number}
record GetExtmarOpts
limit: integer
details: boolean
end
nvim_buf_get_extmarks: function(
buf: integer,
ns: integer,
start: integer | {integer, integer},
eend: integer | {integer, integer},
GetExtmarOpts
): {{integer, integer, integer}}
nvim_buf_get_lines : function(integer, number, number, boolean): {string}
nvim_buf_get_name : function(integer): string
nvim_buf_is_loaded : function(integer): boolean
nvim_buf_is_valid : function(integer): boolean
nvim_buf_line_count : function(integer): integer
nvim_buf_set_extmark : function(integer, integer, integer, integer, {string:any}): integer
nvim_buf_set_keymap : function(integer, string, string, string, {string:any})
nvim_buf_set_lines : function(integer, number, number, boolean, {string})
nvim_buf_set_name : function(integer, string)
nvim_create_buf : function(boolean, boolean): integer
nvim_create_namespace : function(string): integer
record AugroupOpts
clear: boolean
end
nvim__buf_redraw_range : function(integer, number, number)
nvim_create_augroup: function(string, AugroupOpts): integer
record AutoCmdOpts
record CallbackData
id: integer
group: integer
event: string
match: string
buf: integer
file: string
data: any
end
callback: function(CallbackData)
command: string
group: integer|string
pattern: string|{string}
once: boolean
nested: boolean
desc: string
buffer: integer
end
nvim_create_autocmd: function(string|{string}, AutoCmdOpts): integer
nvim_del_current_line: function()
nvim_del_keymap: function(string, string)
nvim_del_var: function(string)
nvim_echo: function({{string}}, boolean, {string:any})
nvim_err_write: function(string)
nvim_err_writeln: function(string)
nvim_eval: function(string): any
nvim_exec: function(string, boolean): string
record ExecAutoCmdOpts
group: string|integer
pattern: string|{string}
buffer: integer
modeline: boolean
data: any
end
nvim_exec_autocmds: function(string|{string}, ExecAutoCmdOpts): any
nvim_exec_lua: function(string, any): any
nvim_feedkeys: function(string, string, boolean)
nvim_get_api_info: function(): any
nvim_get_chan_info: function(number): {string:any}
nvim_get_color_by_name: function(string): number
nvim_get_color_map: function(): {string:any}
nvim_get_commands: function({string:any}): {string:any}
nvim_get_context: function({string:any}): {string:any}
nvim_get_current_buf: function(): integer
nvim_get_current_line: function(): string
nvim_get_current_tabpage: function(): any
nvim_get_current_win: function(): integer
nvim_get_hl_by_id: function(number, boolean): {string:any}
record HlAttrs
foreground: integer
background: integer
reverse: boolean
end
nvim_get_hl_by_name: function(string, boolean): HlAttrs
nvim_get_hl_id_by_name: function(string): number
nvim_get_keymap: function(string): {{string:any}}
nvim_get_mode: function(): {string:any}
nvim_get_namespaces: function(): {string:any}
nvim_get_option: function(string): any
nvim_get_proc: function(number): any
nvim_get_proc_children: function(number): any
nvim_get_runtime_file: function(string, boolean): {string}
nvim_get_var: function(string): any
nvim_get_vvar: function(string): any
nvim_input: function(string): number
nvim_input_mouse: function(string, string, string, number, number, number)
nvim_list_bufs: function(): {integer}
nvim_list_chans: function(): any
nvim_list_runtime_paths: function(): {string}
nvim_list_tabpages: function(): {any}
nvim_list_uis: function(): any
nvim_list_wins: function(): {integer}
nvim_load_context: function({string:any}): any
nvim_open_win: function(number, boolean, {string:any}): integer
nvim_out_write: function(string)
nvim_parse_expression: function(string, string, boolean): {string:any}
nvim_paste: function(string, boolean, number): boolean
nvim_put: function({string}, string, boolean, boolean)
nvim_replace_termcodes: function(string, boolean, boolean, boolean): string
nvim_select_popupmenu_item: function(number, boolean, boolean, {string:any})
nvim_set_client_info: function(string, {string:any}, string, {string:any}, {string:any})
nvim_set_current_buf: function(number)
nvim_set_current_dir: function(string)
nvim_set_current_line: function(string)
nvim_set_current_tabpage: function(any)
nvim_set_current_win: function(number)
nvim_set_decoration_provider: function(number, {string:function})
nvim_set_hl: function(integer, string, {string:any})
nvim_set_keymap: function(string, string, string, {string:any})
nvim_set_option: function(string, any)
nvim_set_var: function(string, any)
nvim_set_vvar: function(string, any)
nvim_strwidth: function(string): number
nvim_subscribe: function(string)
nvim_tabpage_del_var: function(any, string)
nvim_tabpage_get_number: function(any): number
nvim_tabpage_get_var: function(any, string): any
nvim_tabpage_get_win: function(any): number
nvim_tabpage_is_valid: function(any): boolean
nvim_tabpage_list_wins: function(any): {number}
nvim_tabpage_set_var: function(any, string, any)
nvim_ui_attach: function(number, number, {string:any})
nvim_ui_detach: function()
nvim_ui_pum_set_bounds: function(number, number, number, number)
nvim_ui_pum_set_height: function(number)
nvim_ui_set_option: function(string, any)
nvim_ui_try_resize: function(number, number)
nvim_ui_try_resize_grid: function(number, number, number)
nvim_unsubscribe: function(string)
nvim_win_call: function<T>(number, (function(): T)): T
nvim_win_close: function(number, boolean)
nvim_win_del_var: function(number, string)
nvim_win_get_buf: function(integer): integer
nvim_win_get_config: function(number): {string:any}
nvim_win_get_cursor: function(number): {integer}
nvim_win_get_height: function(integer): integer
nvim_win_get_number: function(number): number
nvim_win_get_option: function(number, string): any
nvim_win_get_position: function(number): {number}
nvim_win_get_tabpage: function(number): any
nvim_win_get_var: function(number, string): any
nvim_win_get_width: function(integer): integer
nvim_win_is_valid: function(number): boolean
nvim_win_set_buf: function(number, number)
record WinConfig
bufpos: {integer, integer}
virt_lines_above: boolean
width: integer
height: integer
relative: string
row: integer
col: integer
win: integer
enum Anchor
'NW'
'NE'
'SW'
'SE'
end
anchor: Anchor
end
nvim_win_set_config: function(number, WinConfig)
nvim_win_set_cursor: function(number, {number})
nvim_win_set_height: function(number, number)
nvim_win_set_option: function(number, string, any)
nvim_win_set_var: function(number, string, any)
nvim_win_set_width: function(number, number)
nvim__buf_redraw_range: function(number, number, number)
nvim_create_autocmd : function(string|{string}, AutoCmdOpts): integer
nvim_echo : function({{string}}, boolean, {string:any})
nvim_get_color_by_name : function(string): number
nvim_get_current_buf : function(): integer
nvim_get_current_line : function(): string
nvim_get_current_tabpage : function(): any
nvim_get_current_win : function(): integer
nvim_get_hl_by_name : function(string, boolean): {string:any}
nvim_get_mode : function(): {string:any}
nvim_list_bufs : function(): {integer}
nvim_list_wins : function(): {integer}
nvim_open_win : function(integer, boolean, {string:any}): integer
nvim_replace_termcodes : function(string, boolean, boolean, boolean): string
nvim_set_current_buf : function(integer)
nvim_set_current_line : function(string)
nvim_set_current_win : function(integer)
nvim_set_decoration_provider : function(integer, {string:function})
nvim_set_hl : function(integer, string, {string:any})
nvim_set_keymap : function(string, string, string, {string:any})
nvim_strwidth : function(string): number
nvim_win_call : function<T>(integer, (function(): T)): T
nvim_win_close : function(integer, boolean)
nvim_win_get_buf : function(integer): integer
nvim_win_get_config : function(integer): {string:any}
nvim_win_get_cursor : function(integer): {integer}
nvim_win_get_height : function(integer): integer
nvim_win_get_width : function(integer): integer
nvim_win_is_valid : function(integer): boolean
nvim_win_set_buf : function(integer, number)
nvim_win_set_config : function(integer, {string:any})
nvim_win_set_cursor : function(integer, {number})
nvim_win_set_height : function(integer, number)
nvim_win_set_width : function(integer, number)
end
return M

View file

@ -1,87 +0,0 @@
local record M
bufexists: function(string): integer
bufnr: function(string): integer
iconv: function(string, string, string): string
line: function(string): integer
join: function({any}, string): string
getpos: function(string): {integer}
executable: function(string): integer
exists: function(string): integer
expand: function(string): string
foldclosed: function(integer): integer
foldclosedend: function(integer): integer
getcwd: function(): string
input: function(string, string): string
['repeat#set']: function(string, integer)
record QFItem
bufnr: integer
filename: string
lnum: integer
nr: integer
text: string
type: string
end
record QFWhat
context: any
efm: string
id: integer
idx: integer
items: {QFItem}
lines: {string}
nr: integer
quickfixtextfunc: string
title: string
end
setqflist: function(list: {QFItem}, action: string, what: QFWhat)
setloclist: function(nr: integer, list: {QFItem}, action: string, what: QFWhat)
sign_unplace: function(string, {string:any})
sign_place: function(number, string, string, string | number, {string:any})
record SignPlaceItem
buffer: integer
group: string
id: integer
lnum: integer
name: string
priority: integer
end
sign_placelist: function({SignPlaceItem})
sign_getdefined: function(string): table
record SignPlacedInfo
bufnr: integer
record SignPlacedSigns
id: integer
name: string
group: string
lnum: integer
priority: integer
end
signs: {SignPlacedSigns}
end
sign_getplaced: function(integer, table): {SignPlacedInfo}
sign_define: function(string, table): number
sign_undefine: function(string): number
strdisplaywidth: function(string, integer): integer
stridx: function(haystack: string, needle: string, start: integer): integer
string: function(any): string
systemlist: function({string}): {string}
tempname: function(): string
type: function(any): integer
FugitiveReal: function(...: any): string
FugitiveParse: function(...: any): {string, string}
record WinInfo
textoff: integer
end
getwininfo: function(integer): {WinInfo}
end
return M

View file

@ -1,109 +0,0 @@
local record M
cwd: function(): string
record Timer
userdata
start: function(Timer, number, number, function): number
stop: function(Timer): number
close: function(Timer): number
is_closing: function(Timer): boolean
again: function(Timer): number
set_repeat: function(Timer, number): number
get_repeat: function(Timer): number
get_due_in: function(Timer): number
end
hrtime: function(): number
new_timer: function(): Timer
timer_start: function(Timer, integer, integer, function()): integer | string
new_fs_event: function()
record FSPollObj
userdata
is_closing: function(FSPollObj): boolean | string
close: function(FSPollObj)
start: function(FSPollObj, string, integer, function)
stop: function(FSPollObj)
getpath: function(FSPollObj): string
end
new_fs_poll: function(): FSPollObj
record FsStatRet
dev : number
mode : number
nlink : number
uid : number
gid : number
rdev : number
ino : number
size : number
blksize : number
blocks : number
flags : number
gen : number
record Time
sec : number
nsec : number
end
atime: Time
mtime: Time
ctime: Time
birthtime: Time
type : string
end
fs_stat: function(string, function): FsStatRet
fs_realpath: function(string): string
new_tcp: function()
sleep: function(integer)
record Handle
userdata
close: function(Handle)
is_closing: function(Handle): boolean | string
end
record Pipe
userdata
close: function(Pipe)
is_closing: function(Pipe): boolean | string
read_start: function(Pipe, err: any, data: string)
read_stop: function(Pipe)
write: function(Pipe, string, function())
open: function(any)
end
record Process
userdata
close: function(Process)
end
record SpawnOpts
stdio: {Pipe, Pipe, Pipe}
args: {string}
cwd: string
env: {string}
end
spawn: function(string, SpawnOpts, function(integer, integer)): Process, integer
read_start: function(Pipe, function)
new_pipe: function(boolean): Pipe
shutdown: function(any, function)
close: function(any, function)
record WorkCtx
queue: function(WorkCtx, ...:any)
end
new_work: function(function, function): WorkCtx
end
return M