diff --git a/Makefile b/Makefile index 429d2f9..a31d0dd 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ export PJ_ROOT=$(PWD) +FILTER=.* + BUSTED_ARGS = \ --lpath=$(PJ_ROOT)/lua/?.lua \ - --lpath=$(PJ_ROOT)/plenary.nvim/lua/?.lua + --lpath=$(PJ_ROOT)/plenary.nvim/lua/?.lua \ + --filter=$(FILTER) TEST_FILE = $(PJ_ROOT)/test/gitsigns_spec.lua diff --git a/README.md b/README.md index ca49801..9d265fd 100644 --- a/README.md +++ b/README.md @@ -73,13 +73,14 @@ the default settings: ```lua require('gitsigns').setup { signs = { - add = {hl = 'DiffAdd' , text = '│', numhl='GitSignsAddNr'}, - change = {hl = 'DiffChange', text = '│', numhl='GitSignsChangeNr'}, - delete = {hl = 'DiffDelete', text = '_', numhl='GitSignsDeleteNr'}, - topdelete = {hl = 'DiffDelete', text = '‾', numhl='GitSignsDeleteNr'}, - changedelete = {hl = 'DiffChange', text = '~', numhl='GitSignsChangeNr'}, + 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 = 'GitSignsDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn}, + changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn}, }, numhl = false, + linehl = false, keymaps = { -- Default keymap options noremap = true, @@ -102,7 +103,9 @@ require('gitsigns').setup { interval = 1000 }, sign_priority = 6, + update_debounce = 200, status_formatter = nil, -- Use default + use_decoration_api = false } ``` @@ -122,24 +125,6 @@ set statusline+=%{get(b:,'gitsigns_status','')} For the current branch use the variable `b:gitsigns_head`. -## FAQ - -#### The default signs set the background and not the foreground. How do I get my signs to look like the GIF? -By default Gitsigns uses the highlight groups `DiffAdd`, `DiffChange` and `DiffDelete` for signs as these are the most appropriate highlights that are builtin to Neovim. In many colorschemes, these default highlights set the background but not the foreground. To make the signs looks more like the GIF then you need to set the highlights appropriately. Many colorschemes have specific highlights for Gitgutter (`GitGutterAdd`, `GitGutterChange` and `GitGutterDelete`), so you may use these highlights instead if you have them. - -The following configuration will make Gitsigns look like GitGutters defaults: -```lua -require('gitsigns').setup { - signs = { - add = {hl = 'GitGutterAdd' , text = '+'}, - change = {hl = 'GitGutterChange', text = '~'}, - delete = {hl = 'GitGutterDelete', text = '_'}, - topdelete = {hl = 'GitGutterDelete', text = '‾'}, - changedelete = {hl = 'GitGutterChange', text = '~'}, - } -} -``` - ## TODO - [x] Add action for undoing a stage of a hunk diff --git a/doc/gitsigns.txt b/doc/gitsigns.txt index faf6493..07c7252 100644 --- a/doc/gitsigns.txt +++ b/doc/gitsigns.txt @@ -155,11 +155,11 @@ signs *gitsigns-config-signs* Type: `table`, Default: > { - add = {hl = 'DiffAdd' , text = '│', numhl='GitSignsAddNr' , linehl='GitSignsAddLr' }, - change = {hl = 'DiffChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLr' }, - delete = {hl = 'DiffDelete', text = '_', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLr' }, - topdelete = {hl = 'DiffDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLr' }, - changedelete = {hl = 'DiffChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLr' }, + 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 = 'GitSignsDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, } < Configuration for signs: @@ -172,6 +172,18 @@ signs *gitsigns-config-signs* • `show_count` to enable showing count of hunk, e.g. number of deleted lines. + Note if `hl`, `numhl` or `linehl` use a `GitSigns*` highlight and it is + not defined, it will be automatically derived by searching for other + defined highlights in the following order: + • `GitGutter*` + • `Signify*` + • `Diff*` + + For example if `signs.add.hl = GitSignsAdd` and `GitSignsAdd` is not + defined but `GitGutterAdd` is defined, then `GitSignsAdd` will be linked + to `GitGutterAdd`. + + keymaps *gitsigns-config-keymaps* Type: `table`, Default: > diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index b2c314f..f542cc3 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -10,6 +10,7 @@ local throttle_leading = gs_debounce.throttle_leading local debounce_trailing = gs_debounce.debounce_trailing local gs_popup = require('gitsigns/popup') +local gs_hl = require('gitsigns/highlight') local sign_define = require('gitsigns/signs').sign_define local process_config = require('gitsigns/config').process @@ -555,14 +556,12 @@ local function setup(cfg) for t, sign_name in pairs(sign_map) do local cs = config.signs[t] - local HlTy = {} + gs_hl.setup_highlight(cs.hl) - for _, hl in ipairs({ 'numhl', 'linehl' }) do - if config[hl] then - local hl_exists, _ = pcall(api.nvim_get_hl_by_name, cs[hl], false) - if not hl_exists then - vim.cmd(('highlight link %s %s'):format(cs[hl], cs.hl)) - end + local HlTy = {} + for _, hlty in ipairs({ 'numhl', 'linehl' }) do + if config[hlty] then + gs_hl.setup_other_highlight(cs[hlty], cs.hl) end end diff --git a/lua/gitsigns/config.lua b/lua/gitsigns/config.lua index f4e2e1c..3887118 100644 --- a/lua/gitsigns/config.lua +++ b/lua/gitsigns/config.lua @@ -12,11 +12,11 @@ local schema = { type = 'table', deep_extend = true, default = [[{ - add = {hl = 'DiffAdd' , text = '│', numhl='GitSignsAddNr' , linehl='GitSignsAddLr' }, - change = {hl = 'DiffChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLr' }, - delete = {hl = 'DiffDelete', text = '_', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLr' }, - topdelete = {hl = 'DiffDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLr' }, - changedelete = {hl = 'DiffChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLr' }, + 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 = 'GitSignsDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, }]], description = [[ Configuration for signs: @@ -28,6 +28,18 @@ local schema = { (see |gitsigns-config.linehl|). • `show_count` to enable showing count of hunk, e.g. number of deleted lines. + + Note if `hl`, `numhl` or `linehl` use a `GitSigns*` highlight and it is + not defined, it will be automatically derived by searching for other + defined highlights in the following order: + • `GitGutter*` + • `Signify*` + • `Diff*` + + For example if `signs.add.hl = GitSignsAdd` and `GitSignsAdd` is not + defined but `GitGutterAdd` is defined, then `GitSignsAdd` will be linked + to `GitGutterAdd`. + ]], }, diff --git a/lua/gitsigns/highlight.lua b/lua/gitsigns/highlight.lua new file mode 100644 index 0000000..51a9472 --- /dev/null +++ b/lua/gitsigns/highlight.lua @@ -0,0 +1,91 @@ + +local api = vim.api + +local dprint = require("gitsigns/debug").dprint + +local M = {} + +local GitSignHl = {} + + + + + +local hls = { + GitSignsAdd = { 'GitGutterAdd', 'SignifySignAdd', 'DiffAdd' }, + GitSignsChange = { 'GitGutterChange', 'SignifySignChange', 'DiffChange' }, + GitSignsDelete = { 'GitGutterDelete', 'SignifySignDelete', 'DiffDelete' }, +} + +local function hl_link(to, from, reverse) + local to_exists, _ = pcall(api.nvim_get_hl_by_name, to, false) + if to_exists then + return + end + + if not reverse then + vim.cmd(('highlight link %s %s'):format(to, from)) + return + end + + local exists, hl = pcall(api.nvim_get_hl_by_name, from, true) + if exists then + local bg = hl.background and ('guibg=#%06x'):format(hl.background) or '' + local fg = hl.foreground and ('guifg=#%06x'):format(hl.foreground) or '' + vim.cmd(table.concat({ 'highlight', to, fg, bg, 'gui=reverse' }, ' ')) + end +end + +local stdHl = { + 'DiffAdd', + 'DiffChange', + 'DiffDelete', +} + +local function isStdHl(hl) + return vim.tbl_contains(stdHl, hl) +end + +local function isGitSignHl(hl) + return hls[hl] ~= nil +end + + + +function M.setup_highlight(hl_name0) + if not isGitSignHl(hl_name0) then + return + end + + local hl_name = hl_name0 + + local exists, hl = pcall(api.nvim_get_hl_by_name, hl_name, true) + if exists and (hl.foreground or hl.background) then + + return + end + + for _, d in ipairs(hls[hl_name]) do + local _, dhl = pcall(api.nvim_get_hl_by_name, d, true) + local color = dhl.foreground or dhl.background + if color then + dprint(('Deriving %s from %s'):format(hl_name, d)) + if isStdHl(d) then + hl_link(hl_name, d, true) + else + hl_link(hl_name, d) + end + return + end + end +end + +function M.setup_other_highlight(hl, from_hl) + local hl_pfx, hl_sfx = hl:sub(1, -3), hl:sub(-2, -1) + if isGitSignHl(hl_pfx) and (hl_sfx == 'Ln' or hl_sfx == 'Nr') then + dprint(('Deriving %s from %s'):format(hl, from_hl)) + hl_link(hl, from_hl, hl_sfx == 'Ln') + end +end + +return M diff --git a/teal/gitsigns.tl b/teal/gitsigns.tl index 671bec6..bca198d 100644 --- a/teal/gitsigns.tl +++ b/teal/gitsigns.tl @@ -10,6 +10,7 @@ local throttle_leading = gs_debounce.throttle_leading local debounce_trailing = gs_debounce.debounce_trailing local gs_popup = require('gitsigns/popup') +local gs_hl = require('gitsigns/highlight') local sign_define = require('gitsigns/signs').sign_define local process_config = require('gitsigns/config').process @@ -555,14 +556,12 @@ local function setup(cfg: Config) for t, sign_name in pairs(sign_map) do local cs = config.signs[t] - local enum HlTy 'numhl' 'linehl' end + gs_hl.setup_highlight(cs.hl) - for _, hl in ipairs({'numhl', 'linehl'} as {HlTy}) do - if config[hl] then - local hl_exists, _ = pcall(api.nvim_get_hl_by_name, cs[hl], false) - if not hl_exists then - vim.cmd(('highlight link %s %s'):format(cs[hl], cs.hl)) - end + local enum HlTy 'numhl' 'linehl' end + for _, hlty in ipairs({'numhl', 'linehl'} as {HlTy}) do + if config[hlty] then + gs_hl.setup_other_highlight(cs[hlty], cs.hl) end end diff --git a/teal/gitsigns/config.tl b/teal/gitsigns/config.tl index 23be640..2824865 100644 --- a/teal/gitsigns/config.tl +++ b/teal/gitsigns/config.tl @@ -12,11 +12,11 @@ local schema: {string:SchemaElem} = { type = 'table', deep_extend = true, default = [[{ - add = {hl = 'DiffAdd' , text = '│', numhl='GitSignsAddNr' , linehl='GitSignsAddLr' }, - change = {hl = 'DiffChange', text = '│', numhl='GitSignsChangeNr', linehl='GitSignsChangeLr' }, - delete = {hl = 'DiffDelete', text = '_', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLr' }, - topdelete = {hl = 'DiffDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLr' }, - changedelete = {hl = 'DiffChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLr' }, + 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 = 'GitSignsDelete', text = '‾', numhl='GitSignsDeleteNr', linehl='GitSignsDeleteLn' }, + changedelete = {hl = 'GitSignsChange', text = '~', numhl='GitSignsChangeNr', linehl='GitSignsChangeLn' }, }]], description = [[ Configuration for signs: @@ -28,6 +28,18 @@ local schema: {string:SchemaElem} = { (see |gitsigns-config.linehl|). • `show_count` to enable showing count of hunk, e.g. number of deleted lines. + + Note if `hl`, `numhl` or `linehl` use a `GitSigns*` highlight and it is + not defined, it will be automatically derived by searching for other + defined highlights in the following order: + • `GitGutter*` + • `Signify*` + • `Diff*` + + For example if `signs.add.hl = GitSignsAdd` and `GitSignsAdd` is not + defined but `GitGutterAdd` is defined, then `GitSignsAdd` will be linked + to `GitGutterAdd`. + ]] }, diff --git a/teal/gitsigns/highlight.tl b/teal/gitsigns/highlight.tl new file mode 100644 index 0000000..d5ac714 --- /dev/null +++ b/teal/gitsigns/highlight.tl @@ -0,0 +1,91 @@ + +local api = vim.api + +local dprint = require("gitsigns/debug").dprint + +local M = {} + +local enum GitSignHl + 'GitSignsAdd' + 'GitSignsChange' + 'GitSignsDelete' +end + +local hls: {GitSignHl:{string}} = { + GitSignsAdd = { 'GitGutterAdd' , 'SignifySignAdd' , 'DiffAdd' }, + GitSignsChange = { 'GitGutterChange', 'SignifySignChange', 'DiffChange' }, + GitSignsDelete = { 'GitGutterDelete', 'SignifySignDelete', 'DiffDelete' }, +} + +local function hl_link(to: string, from: string, reverse: boolean) + local to_exists, _ = pcall(api.nvim_get_hl_by_name, to, false) + if to_exists then + return + end + + if not reverse then + vim.cmd(('highlight link %s %s'):format(to, from)) + return + end + + local exists, hl = pcall(api.nvim_get_hl_by_name, from, true) + if exists then + local bg = hl.background and ('guibg=#%06x'):format(hl.background) or '' + local fg = hl.foreground and ('guifg=#%06x'):format(hl.foreground) or '' + vim.cmd(table.concat({'highlight', to, fg, bg, 'gui=reverse'}, ' ')) + end +end + +local stdHl = { + 'DiffAdd', + 'DiffChange', + 'DiffDelete' +} + +local function isStdHl(hl: string): boolean + return vim.tbl_contains(stdHl, hl) +end + +local function isGitSignHl(hl: string): boolean + return hls[hl as GitSignHl] ~= nil +end + +-- Setup a GitSign* highlight by deriving it from other potentially present +-- highlights. +function M.setup_highlight(hl_name0: string) + if not isGitSignHl(hl_name0) then + return + end + + local hl_name = hl_name0 as GitSignHl + + local exists, hl = pcall(api.nvim_get_hl_by_name, hl_name, true) + if exists and (hl.foreground or hl.background) then + -- Alread defined + return + end + + for _, d in ipairs(hls[hl_name]) do + local _, dhl = pcall(api.nvim_get_hl_by_name, d, true) + local color = dhl.foreground or dhl.background + if color then + dprint(('Deriving %s from %s'):format(hl_name, d)) + if isStdHl(d) then + hl_link(hl_name, d, true) + else + hl_link(hl_name, d) + end + return + end + end +end + +function M.setup_other_highlight(hl: string, from_hl: string) + local hl_pfx, hl_sfx = hl:sub(1, -3), hl:sub(-2, -1) + if isGitSignHl(hl_pfx) and (hl_sfx == 'Ln' or hl_sfx == 'Nr') then + dprint(('Deriving %s from %s'):format(hl, from_hl)) + hl_link(hl, from_hl, hl_sfx == 'Ln') + end +end + +return M diff --git a/test/gitsigns_spec.lua b/test/gitsigns_spec.lua index 6b7cea3..dd73878 100644 --- a/test/gitsigns_spec.lua +++ b/test/gitsigns_spec.lua @@ -18,7 +18,6 @@ local function check_status(status) eq(status, get_buf_var("gitsigns_status_dict")) end - local scratch = os.getenv('PJ_ROOT')..'/scratch' local test_file = scratch..'/dummy.txt' local newfile = scratch.."/newfile.txt" @@ -80,6 +79,10 @@ local function command_fmt(str, ...) command(str:format(...)) end +local function edit(path) + command_fmt("edit %s", path) +end + local function buf_var_exists(name) return pcall(get_buf_var, name) end @@ -106,24 +109,69 @@ local function match_lines(lines, spec) end end +local function match_lines2(lines, spec) + local i = 1 + for _, line in ipairs(lines) do + if line ~= '' then + local s = spec[i] + if s then + if s.pattern then + if string.match(line, s.text) then + i = i + 1 + end + elseif s.next then + eq(s.text, line) + i = i + 1 + else + if s == line then + i = i + 1 + end + end + end + end + end + + if i < #spec + 1 then + local unmatched = {} + for j = i, #spec do + table.insert(unmatched, spec[j].text or spec[j]) + end + print(require'inspect'(unmatched)) + error(('Did not match patterns:\n - %s'):format(table.concat(unmatched, '\n - '))) + end +end + +local function debug_messages() + return exec_lua("return require'gitsigns'.debug_messages()") +end + local function match_debug_messages(spec) - local res = exec_lua("return require'gitsigns'.debug_messages()") - match_lines(res, spec) + match_lines(debug_messages(), spec) +end + +local function match_dag(lines, spec) + for _, s in ipairs(spec) do + match_lines2(lines, {s}) + end end local function p(str) return {text=str, pattern=true} end +local function n(str) + return {text=str, next=true} +end + local function testsuite(variant, advanced_features) local test_config = { debug_mode = true, signs = { - add = {text = '+'}, - delete = {text = '_'}, - change = {text = '~'}, - topdelete = {text = '^'}, - changedelete = {text = '%'}, + add = {hl = 'DiffAdd' , text = '+'}, + delete = {hl = 'DiffDelete', text = '_'}, + change = {hl = 'DiffChange', text = '~'}, + topdelete = {hl = 'DiffDelete', text = '^'}, + changedelete = {hl = 'DiffChange', text = '%'}, }, keymaps = { noremap = true, @@ -188,9 +236,47 @@ local function testsuite(variant, advanced_features) }) end) - local function edit(path) - command_fmt("edit %s", path) - end + it('sets up highlights', function() + command("set termguicolors") + + local test_config2 = helpers.deepcopy(test_config) + test_config2.signs.add.hl = nil + test_config2.signs.change.hl = nil + test_config2.signs.delete.hl = nil + test_config2.signs.changedelete.hl = nil + test_config2.signs.topdelete.hl = nil + test_config2.numhl = true + test_config2.linehl = true + + exec_lua('gs.setup(...)', test_config2) + + local d = debug_messages() + + + match_dag(d, { + p'Deriving GitSignsChangeNr from GitSignsChange', + p'Deriving GitSignsChangeLn from GitSignsChange', + p'Deriving GitSignsDelete from DiffDelete', + p'Deriving GitSignsDeleteNr from GitSignsDelete', + p'Deriving GitSignsDeleteLn from GitSignsDelete', + p'Deriving GitSignsAdd from DiffAdd', + p'Deriving GitSignsAddNr from GitSignsAdd', + p'Deriving GitSignsAddLn from GitSignsAdd', + p'Deriving GitSignsDeleteNr from GitSignsDelete', + p'Deriving GitSignsDeleteLn from GitSignsDelete', + p'Deriving GitSignsChangeNr from GitSignsChange', + p'Deriving GitSignsChangeLn from GitSignsChange' + }) + + eq('GitSignsChange xxx gui=reverse guibg=#ffbbff', + exec_capture('hi GitSignsChange')) + + eq('GitSignsDelete xxx gui=reverse guifg=#0000ff guibg=#e0ffff', + exec_capture('hi GitSignsDelete')) + + eq('GitSignsAdd xxx gui=reverse guibg=#add8e6', + exec_capture('hi GitSignsAdd')) + end) it('basic signs', function() exec_lua('gs.setup(...)', test_config) diff --git a/types/types.d.tl b/types/types.d.tl index 7a445d7..a273c2b 100644 --- a/types/types.d.tl +++ b/types/types.d.tl @@ -270,6 +270,8 @@ global record vim tbl_isempty: function(table): boolean + tbl_contains: function(table, any): boolean + record InspectOptions depth: number newline: string