diff --git a/README.md b/README.md index 2e610acb..cdf91ab6 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Here is a suggested example: require('gitsigns').setup{ ... on_attach = function(bufnr) - local gs = package.loaded.gitsigns + local gitsigns = require('gitsigns') local function map(mode, l, r, opts) opts = opts or {} @@ -123,31 +123,35 @@ require('gitsigns').setup{ -- Navigation map('n', ']c', function() - if vim.wo.diff then return ']c' end - vim.schedule(function() gs.next_hunk() end) - return '' - end, {expr=true}) + if vim.wo.diff then + vim.cmd.normal({']c', bang = true}) + else + gitsigns.nav_hunk('next') + end + end) map('n', '[c', function() - if vim.wo.diff then return '[c' end - vim.schedule(function() gs.prev_hunk() end) - return '' - end, {expr=true}) + if vim.wo.diff then + vim.cmd.normal({'[c', bang = true}) + else + gitsigns.nav_hunk('prev') + end + end) -- Actions - map('n', 'hs', gs.stage_hunk) - map('n', 'hr', gs.reset_hunk) - map('v', 'hs', function() gs.stage_hunk {vim.fn.line('.'), vim.fn.line('v')} end) - map('v', 'hr', function() gs.reset_hunk {vim.fn.line('.'), vim.fn.line('v')} end) - map('n', 'hS', gs.stage_buffer) - map('n', 'hu', gs.undo_stage_hunk) - map('n', 'hR', gs.reset_buffer) - map('n', 'hp', gs.preview_hunk) - map('n', 'hb', function() gs.blame_line{full=true} end) - map('n', 'tb', gs.toggle_current_line_blame) - map('n', 'hd', gs.diffthis) - map('n', 'hD', function() gs.diffthis('~') end) - map('n', 'td', gs.toggle_deleted) + map('n', 'hs', gitsigns.stage_hunk) + map('n', 'hr', gitsigns.reset_hunk) + map('v', 'hs', function() gitsigns.stage_hunk {vim.fn.line('.'), vim.fn.line('v')} end) + map('v', 'hr', function() gitsigns.reset_hunk {vim.fn.line('.'), vim.fn.line('v')} end) + map('n', 'hS', gitsigns.stage_buffer) + map('n', 'hu', gitsigns.undo_stage_hunk) + map('n', 'hR', gitsigns.reset_buffer) + map('n', 'hp', gitsigns.preview_hunk) + map('n', 'hb', function() gitsigns.blame_line{full=true} end) + map('n', 'tb', gitsigns.toggle_current_line_blame) + map('n', 'hd', gitsigns.diffthis) + map('n', 'hD', function() gitsigns.diffthis('~') end) + map('n', 'td', gitsigns.toggle_deleted) -- Text object map({'o', 'x'}, 'ih', ':Gitsigns select_hunk') diff --git a/doc/gitsigns.txt b/doc/gitsigns.txt index 250ba600..a23d26b2 100644 --- a/doc/gitsigns.txt +++ b/doc/gitsigns.txt @@ -343,6 +343,8 @@ preview_hunk() *gitsigns.preview_hunk()* will cause the window to get focus. prev_hunk({opts}, {callback?}) *gitsigns.prev_hunk()* + DEPRECATED: use |gitsigns.nav_hunk()| + Jump to the previous hunk in the current buffer. If a hunk preview (popup or inline) was previously opened, it will be re-opened at the previous hunk. @@ -351,9 +353,11 @@ prev_hunk({opts}, {callback?}) *gitsigns.prev_hunk()* {async} Parameters: ~ - See |gitsigns.next_hunk()|. + See |gitsigns.nav_hunk()|. next_hunk({opts}, {callback?}) *gitsigns.next_hunk()* + DEPRECATED: use |gitsigns.nav_hunk()| + Jump to the next hunk in the current buffer. If a hunk preview (popup or inline) was previously opened, it will be re-opened at the next hunk. @@ -362,23 +366,37 @@ next_hunk({opts}, {callback?}) *gitsigns.next_hunk()* {async} Parameters: ~ - {opts} (table|nil): Configuration table. Keys: - • {wrap}: (boolean) - Whether to loop around file or not. Defaults - to the value 'wrapscan' - • {navigation_message}: (boolean) - Whether to show navigation messages or not. - Looks at 'shortmess' for default behaviour. - • {foldopen}: (boolean) - Expand folds when navigating to a hunk which is - inside a fold. Defaults to `true` if 'foldopen' - contains `search`. - • {preview}: (boolean) - Automatically open preview_hunk() upon navigating - to a hunk. - • {greedy}: (boolean) - Only navigate between non-contiguous hunks. Only useful if - 'diff_opts' contains `linematch`. Defaults to `true`. + See |gitsigns.nav_hunk()|. + +nav_hunk({direction}, {opts}, {callback?}) *gitsigns.nav_hunk()* + Jump to hunk in the current buffer. If a hunk preview + (popup or inline) was previously opened, it will be re-opened + at the next hunk. + + Attributes: ~ + {async} + + Parameters: ~ + {direction} ('first'|'last'|'next'|'prev'): + {opts} (table|nil): Configuration table. Keys: + • {wrap}: (boolean) + Whether to loop around file or not. Defaults + to the value 'wrapscan' + • {navigation_message}: (boolean) + Whether to show navigation messages or not. + Looks at 'shortmess' for default behaviour. + • {foldopen}: (boolean) + Expand folds when navigating to a hunk which is + inside a fold. Defaults to `true` if 'foldopen' + contains `search`. + • {preview}: (boolean) + Automatically open preview_hunk() upon navigating + to a hunk. + • {greedy}: (boolean) + Only navigate between non-contiguous hunks. Only useful if + 'diff_opts' contains `linematch`. Defaults to `true`. + • {count}: (integer) + Number of times to advance. Defaults to |v:count1|. reset_buffer_index() *gitsigns.reset_buffer_index()* Unstage all hunks for current buffer in the index. Note: diff --git a/gen_help.lua b/gen_help.lua index 36d78427..90b6a863 100755 --- a/gen_help.lua +++ b/gen_help.lua @@ -295,13 +295,22 @@ end --- @param block string[] --- @param params {[1]: string, [2]: string, [3]: string[]}[] --- @param returns {[1]: string, [2]: string, [3]: string[]}[] +--- @param deprecated string? --- @return string[]? -local function render_block(header, block, params, returns) +local function render_block(header, block, params, returns, deprecated) if vim.startswith(header, '_') then return end local res = { header } + + if deprecated then + list_extend(res, { + ' DEPRECATED: '..deprecated, + '' + }) + end + list_extend(res, block) -- filter arguments beginning with '_' @@ -356,21 +365,28 @@ local function gen_functions_doc_from_file(path) local desc = {} --- @type string[] local params = {} --- @type {[1]: string, [2]: string, [3]: string[]}[] local returns = {} --- @type {[1]: string, [2]: string, [3]: string[]}[] + local deprecated --- @type string? for l in i do local doc_comment = l:match('^%-%-%- ?(.*)') --- @type string? if doc_comment then - state = process_doc_comment(state, doc_comment, desc, params, returns) + local depre = doc_comment:match('@deprecated ?(.*)') + if depre then + deprecated = depre + else + state = process_doc_comment(state, doc_comment, desc, params, returns) + end elseif state ~= 'none' then -- First line after block local ok, header = pcall(parse_func_header, l) if ok then - blocks[#blocks + 1] = render_block(header, desc, params, returns) + blocks[#blocks + 1] = render_block(header, desc, params, returns, deprecated) end state = 'none' desc = {} params = {} returns = {} + deprecated = nil end end diff --git a/lua/gitsigns/actions.lua b/lua/gitsigns/actions.lua index d5c31f45..5290c195 100644 --- a/lua/gitsigns/actions.lua +++ b/lua/gitsigns/actions.lua @@ -463,6 +463,7 @@ end) --- @field navigation_message boolean --- @field greedy boolean --- @field preview boolean +--- @field count integer --- @param x string --- @param word string @@ -494,6 +495,10 @@ local function process_nav_opts(opts) opts.greedy = true end + if opts.count == nil then + opts.count = vim.v.count1 + end + return opts end @@ -514,9 +519,9 @@ local function has_preview_inline(bufnr) end --- @async +--- @param direction 'first'|'last'|'next'|'prev' --- @param opts? Gitsigns.NavOpts ---- @param forwards boolean -local function nav_hunk(opts, forwards) +local function nav_hunk(direction, opts) opts = process_nav_opts(opts) local bufnr = current_buf() local bcache = cache[bufnr] @@ -524,10 +529,9 @@ local function nav_hunk(opts, forwards) return end - local hunks = {} - vim.list_extend(hunks, get_hunks(bufnr, bcache, opts.greedy, false) or {}) + local hunks = get_hunks(bufnr, bcache, opts.greedy, false) or {} local hunks_head = get_hunks(bufnr, bcache, opts.greedy, true) or {} - vim.list_extend(hunks, Hunks.filter_common(hunks_head, bcache.hunks) or {}) + vim.list_extend(hunks, Hunks.filter_common(hunks_head, hunks) or {}) if not hunks or vim.tbl_isempty(hunks) then if opts.navigation_message then @@ -535,54 +539,65 @@ local function nav_hunk(opts, forwards) end return end + local line = api.nvim_win_get_cursor(0)[1] + local index --- @type integer? - local hunk, index = Hunks.find_nearest_hunk(line, hunks, forwards, opts.wrap) + local forwards = direction == 'next' or direction == 'last' - if hunk == nil then - if opts.navigation_message then - api.nvim_echo({ { 'No more hunks', 'WarningMsg' } }, false, {}) + for _ = 1, opts.count do + index = Hunks.find_nearest_hunk(line, hunks, direction, opts.wrap) + + if not index then + if opts.navigation_message then + api.nvim_echo({ { 'No more hunks', 'WarningMsg' } }, false, {}) + end + local _, col = vim.fn.getline(line):find('^%s*') + api.nvim_win_set_cursor(0, { line, col }) + return end - return + + line = forwards and hunks[index].added.start or hunks[index].vend end - local row = forwards and hunk.added.start or hunk.vend - if row then - -- Handle topdelete - if row == 0 then - row = 1 - end - vim.cmd([[ normal! m' ]]) -- add current cursor position to the jump list - api.nvim_win_set_cursor(0, { row, 0 }) - if opts.foldopen then - vim.cmd('silent! foldopen!') - end - if opts.preview or popup.is_open('hunk') ~= nil then - -- Use defer so the cursor change can settle, otherwise the popup might - -- appear in the old position - defer(function() - -- Close the popup in case one is open which will cause it to focus the - -- popup - popup.close('hunk') - M.preview_hunk() - end) - elseif has_preview_inline(bufnr) then - defer(M.preview_hunk_inline) - end + -- Handle topdelete + line = math.max(line, 1) - if index ~= nil and opts.navigation_message then - api.nvim_echo({ { string.format('Hunk %d of %d', index, #hunks), 'None' } }, false, {}) - end + vim.cmd([[ normal! m' ]]) -- add current cursor position to the jump list + + local _, col = vim.fn.getline(line):find('^%s*') + api.nvim_win_set_cursor(0, { line, col }) + + if opts.foldopen then + vim.cmd('silent! foldopen!') + end + + if opts.preview or popup.is_open('hunk') ~= nil then + -- Use defer so the cursor change can settle, otherwise the popup might + -- appear in the old position + defer(function() + -- Close the popup in case one is open which will cause it to focus the + -- popup + popup.close('hunk') + M.preview_hunk() + end) + elseif has_preview_inline(bufnr) then + defer(M.preview_hunk_inline) + end + + if index and opts.navigation_message then + api.nvim_echo({ { string.format('Hunk %d of %d', index, #hunks), 'None' } }, false, {}) end end ---- Jump to the next hunk in the current buffer. If a hunk preview +--- Jump to hunk in the current buffer. If a hunk preview --- (popup or inline) was previously opened, it will be re-opened --- at the next hunk. --- --- Attributes: ~ --- {async} --- +--- @param direction 'first'|'last'|'next'|'prev' --- @param opts table|nil Configuration table. Keys: --- • {wrap}: (boolean) --- Whether to loop around file or not. Defaults @@ -600,14 +615,35 @@ end --- • {greedy}: (boolean) --- Only navigate between non-contiguous hunks. Only useful if --- 'diff_opts' contains `linematch`. Defaults to `true`. +--- • {count}: (integer) +--- Number of times to advance. Defaults to |v:count1|. +M.nav_hunk = async.create(2, function(direction, opts) + nav_hunk(direction, opts) +end) + +C.nav_hunk = function(args, _) + M.nav_hunk(args[1], args) +end + +--- @deprecated use |gitsigns.nav_hunk()| +--- Jump to the next hunk in the current buffer. If a hunk preview +--- (popup or inline) was previously opened, it will be re-opened +--- at the next hunk. +--- +--- Attributes: ~ +--- {async} +--- +--- Parameters: ~ +--- See |gitsigns.nav_hunk()|. M.next_hunk = async.create(1, function(opts) - nav_hunk(opts, true) + nav_hunk('next', opts) end) C.next_hunk = function(args, _) - M.next_hunk(args) + M.nav_hunk('next', args) end +--- @deprecated use |gitsigns.nav_hunk()| --- Jump to the previous hunk in the current buffer. If a hunk preview --- (popup or inline) was previously opened, it will be re-opened --- at the previous hunk. @@ -616,13 +652,13 @@ end --- {async} --- --- Parameters: ~ ---- See |gitsigns.next_hunk()|. +--- See |gitsigns.nav_hunk()|. M.prev_hunk = async.create(1, function(opts) - nav_hunk(opts, false) + nav_hunk('prev', opts) end) C.prev_hunk = function(args, _) - M.prev_hunk(args) + M.nav_hunk('prev', args) end --- @param fmt Gitsigns.LineSpec diff --git a/lua/gitsigns/hunks.lua b/lua/gitsigns/hunks.lua index 096785fb..f383fa2f 100644 --- a/lua/gitsigns/hunks.lua +++ b/lua/gitsigns/hunks.lua @@ -316,39 +316,35 @@ end --- @param lnum integer --- @param hunks Gitsigns.Hunk.Hunk[] ---- @param forwards boolean +--- @param direction 'first'|'last'|'next'|'prev' --- @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 +--- @return integer? +function M.find_nearest_hunk(lnum, hunks, direction, wrap) + if direction == 'first' then + return 1 + elseif direction == 'last' then + return #hunks + elseif direction == 'next' then + for i = #hunks, 1, -1 do + if hunks[i].added.start <= lnum then + if i + 1 <= #hunks and hunks[i + 1].added.start > lnum then + return i + 1 + elseif wrap then + return 1 + end end end - else - for i = #hunks, 1, -1 do - local hunk = hunks[i] - local dist = lnum - math.max(hunk.vend, 1) - if dist > 0 and dist < distance then - distance = dist - ret = hunk - index = i + elseif direction == 'prev' then + for i = 1, #hunks do + if lnum <= math.max(hunks[i].vend, 1) then + if i > 1 and math.max(hunks[i - 1].vend, 1) < lnum then + return i - 1 + elseif wrap then + return #hunks + end end 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[]?