From e7e6492a2dc9405c209ffe17713771f5074bc3db Mon Sep 17 00:00:00 2001 From: Aaron Kollasch Date: Sat, 22 Jul 2023 17:35:52 -0400 Subject: [PATCH] feat(git): Add bcommits_range picker (#2398) * Filter bcommits by selection in visual mode * Split bcommits_range into new picker * Add option to run bcommits_range as operator Starts operator-pending mode and shows commits in the range of lines covered by the next text object or motion * Rename range arguments to "first" and "last" Can't use start/end, since end is an annoying keyword to use in lua and start/stop doesn't fit as well * Move operators functionality to new module * Run bcommits if no range given to bcommits_range * Make bcommits_range default to current line Instead of calling bcommits * Improve documentation of telescope.operators * Add default value for last_operator Default to a no-op callback * Update bcommits_range for detached worktrees See #2597 * Rename range arguments to "from" and "to" * Move shared bcommits picker into single function --- README.md | 1 + doc/telescope.txt | 30 +++++ lua/telescope/builtin/__git.lua | 188 ++++++++++++++++++++------------ lua/telescope/builtin/init.lua | 18 +++ lua/telescope/operators.lua | 23 ++++ 5 files changed, 189 insertions(+), 71 deletions(-) create mode 100644 lua/telescope/operators.lua diff --git a/README.md b/README.md index 9077980..ac883c1 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,7 @@ Built-in functions. Ready to be bound to any key you like. |-------------------------------------|------------------------------------------------------------------------------------------------------------| | `builtin.git_commits` | Lists git commits with diff preview, checkout action ``, reset mixed `m`, reset soft `s` and reset hard `h` | | `builtin.git_bcommits` | Lists buffer's git commits with diff preview and checks them out on `` | +| `builtin.git_bcommits_range` | Lists buffer's git commits in a range of lines. Use options `from` and `to` to specify the range. In visual mode, lists commits for the selected lines | | `builtin.git_branches` | Lists all branches with log preview, checkout action ``, track action ``, rebase action``, create action ``, switch action ``, delete action `` and merge action `` | | `builtin.git_status` | Lists current changes per file with diff preview and add action. (Multi-selection still WIP) | | `builtin.git_stash` | Lists stash items in current repository with ability to apply them on `` | diff --git a/doc/telescope.txt b/doc/telescope.txt index 18dc03b..a571bc1 100644 --- a/doc/telescope.txt +++ b/doc/telescope.txt @@ -1052,6 +1052,36 @@ builtin.git_bcommits({opts}) *telescope.builtin.git_bcommits()* {"git","log","--pretty=oneline","--abbrev-commit"} +builtin.git_bcommits_range({opts}) *telescope.builtin.git_bcommits_range()* + Lists commits for a range of lines in the current buffer with diff preview + In visual mode, lists commits for the selected lines + With operator mode enabled, lists commits inside the text object/motion + - Default keymaps or your overridden `select_` keys: + - ``: checks out the currently selected commit + - ``: opens a diff in a vertical split + - ``: opens a diff in a horizontal split + - ``: opens a diff in a new tab + + + Parameters: ~ + {opts} (table) options to pass to the picker + + Options: ~ + {cwd} (string) specify the path of the repo + {use_git_root} (boolean) if we should use git root as cwd or the cwd + (important for submodule) (default: true) + {current_file} (string) specify the current file that should be used + for bcommits (default: current buffer) + {git_command} (table) command that will be executed. the last + element must be "-L". + {"git","log","--pretty=oneline","--abbrev-commit","--no-patch","-L"} + {from} (number) the first line number in the range + (default: current line) + {to} (number) the last line number in the range + (default: the value of `from`) + {operator} (boolean) select lines in operator-pending mode + (default: false) + builtin.git_branches({opts}) *telescope.builtin.git_branches()* List branches for current directory, with output from `git log --oneline` shown in the preview window diff --git a/lua/telescope/builtin/__git.lua b/lua/telescope/builtin/__git.lua index 380570a..1478dc2 100644 --- a/lua/telescope/builtin/__git.lua +++ b/lua/telescope/builtin/__git.lua @@ -2,6 +2,7 @@ local actions = require "telescope.actions" local action_state = require "telescope.actions.state" local finders = require "telescope.finders" local make_entry = require "telescope.make_entry" +local operators = require "telescope.operators" local pickers = require "telescope.pickers" local previewers = require "telescope.previewers" local utils = require "telescope.utils" @@ -110,6 +111,75 @@ local get_current_buf_line = function(winnr) return vim.trim(vim.api.nvim_buf_get_lines(vim.api.nvim_win_get_buf(winnr), lnum - 1, lnum, false)[1]) end +local bcommits_picker = function(opts, title, finder) + return pickers.new(opts, { + prompt_title = title, + finder = finder, + previewer = { + previewers.git_commit_diff_to_parent.new(opts), + previewers.git_commit_diff_to_head.new(opts), + previewers.git_commit_diff_as_was.new(opts), + previewers.git_commit_message.new(opts), + }, + sorter = conf.file_sorter(opts), + attach_mappings = function() + actions.select_default:replace(actions.git_checkout_current_buffer) + local transfrom_file = function() + return opts.current_file and Path:new(opts.current_file):make_relative(opts.cwd) or "" + end + + local get_buffer_of_orig = function(selection) + local value = selection.value .. ":" .. transfrom_file() + local content = utils.get_os_command_output({ "git", "--no-pager", "show", value }, opts.cwd) + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) + vim.api.nvim_buf_set_name(bufnr, "Original") + return bufnr + end + + local vimdiff = function(selection, command) + local ft = vim.bo.filetype + vim.cmd "diffthis" + + local bufnr = get_buffer_of_orig(selection) + vim.cmd(string.format("%s %s", command, bufnr)) + vim.bo.filetype = ft + vim.cmd "diffthis" + + vim.api.nvim_create_autocmd("WinClosed", { + buffer = bufnr, + nested = true, + once = true, + callback = function() + vim.api.nvim_buf_delete(bufnr, { force = true }) + end, + }) + end + + actions.select_vertical:replace(function(prompt_bufnr) + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + vimdiff(selection, "leftabove vert sbuffer") + end) + + actions.select_horizontal:replace(function(prompt_bufnr) + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + vimdiff(selection, "belowright sbuffer") + end) + + actions.select_tab:replace(function(prompt_bufnr) + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + vim.cmd("tabedit " .. transfrom_file()) + vimdiff(selection, "leftabove vert sbuffer") + end) + return true + end, + }) +end + git.bcommits = function(opts) opts.current_line = (opts.current_file == nil) and get_current_buf_line(opts.winnr) or nil opts.current_file = vim.F.if_nil(opts.current_file, vim.api.nvim_buf_get_name(opts.bufnr)) @@ -117,80 +187,56 @@ git.bcommits = function(opts) opts.git_command = vim.F.if_nil(opts.git_command, git_command({ "log", "--pretty=oneline", "--abbrev-commit", "--follow" }, opts)) - pickers - .new(opts, { - prompt_title = "Git BCommits", - finder = finders.new_oneshot_job( - vim.tbl_flatten { - opts.git_command, - opts.current_file, - }, - opts - ), - previewer = { - previewers.git_commit_diff_to_parent.new(opts), - previewers.git_commit_diff_to_head.new(opts), - previewers.git_commit_diff_as_was.new(opts), - previewers.git_commit_message.new(opts), - }, - sorter = conf.file_sorter(opts), - attach_mappings = function() - actions.select_default:replace(actions.git_checkout_current_buffer) - local transfrom_file = function() - return opts.current_file and Path:new(opts.current_file):make_relative(opts.cwd) or "" - end + local title = "Git BCommits" + local finder = finders.new_oneshot_job( + vim.tbl_flatten { + opts.git_command, + opts.current_file, + }, + opts + ) + bcommits_picker(opts, title, finder):find() +end - local get_buffer_of_orig = function(selection) - local value = selection.value .. ":" .. transfrom_file() - local content = utils.get_os_command_output({ "git", "--no-pager", "show", value }, opts.cwd) +git.bcommits_range = function(opts) + opts.current_line = (opts.current_file == nil) and get_current_buf_line(opts.winnr) or nil + opts.current_file = vim.F.if_nil(opts.current_file, vim.api.nvim_buf_get_name(opts.bufnr)) + opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_commits(opts)) + opts.git_command = vim.F.if_nil( + opts.git_command, + git_command({ "log", "--pretty=oneline", "--abbrev-commit", "--no-patch", "-L" }, opts) + ) + local visual = string.find(vim.fn.mode(), "[vV]") ~= nil - local bufnr = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, content) - vim.api.nvim_buf_set_name(bufnr, "Original") - return bufnr - end + local line_number_first = opts.from + local line_number_last = vim.F.if_nil(opts.to, line_number_first) + if visual then + line_number_first = vim.F.if_nil(line_number_first, vim.fn.line "v") + line_number_last = vim.F.if_nil(line_number_last, vim.fn.line ".") + elseif opts.operator then + opts.operator = false + opts.operator_callback = true + operators.run_operator(git.bcommits_range, opts) + return + elseif opts.operator_callback then + line_number_first = vim.fn.line "'[" + line_number_last = vim.fn.line "']" + elseif line_number_first == nil then + line_number_first = vim.F.if_nil(line_number_first, vim.fn.line ".") + line_number_last = vim.F.if_nil(line_number_last, vim.fn.line ".") + end + local line_range = + string.format("%d,%d:%s", line_number_first, line_number_last, Path:new(opts.current_file):make_relative(opts.cwd)) - local vimdiff = function(selection, command) - local ft = vim.bo.filetype - vim.cmd "diffthis" - - local bufnr = get_buffer_of_orig(selection) - vim.cmd(string.format("%s %s", command, bufnr)) - vim.bo.filetype = ft - vim.cmd "diffthis" - - vim.api.nvim_create_autocmd("WinClosed", { - buffer = bufnr, - nested = true, - once = true, - callback = function() - vim.api.nvim_buf_delete(bufnr, { force = true }) - end, - }) - end - - actions.select_vertical:replace(function(prompt_bufnr) - actions.close(prompt_bufnr) - local selection = action_state.get_selected_entry() - vimdiff(selection, "leftabove vert sbuffer") - end) - - actions.select_horizontal:replace(function(prompt_bufnr) - actions.close(prompt_bufnr) - local selection = action_state.get_selected_entry() - vimdiff(selection, "belowright sbuffer") - end) - - actions.select_tab:replace(function(prompt_bufnr) - actions.close(prompt_bufnr) - local selection = action_state.get_selected_entry() - vim.cmd("tabedit " .. transfrom_file()) - vimdiff(selection, "leftabove vert sbuffer") - end) - return true - end, - }) - :find() + local title = "Git BCommits in range" + local finder = finders.new_oneshot_job( + vim.tbl_flatten { + opts.git_command, + line_range, + }, + opts + ) + bcommits_picker(opts, title, finder):find() end git.branches = function(opts) diff --git a/lua/telescope/builtin/init.lua b/lua/telescope/builtin/init.lua index 979615b..99b3971 100644 --- a/lua/telescope/builtin/init.lua +++ b/lua/telescope/builtin/init.lua @@ -172,6 +172,24 @@ builtin.git_commits = require_on_exported_call("telescope.builtin.__git").commit ---@field git_command table: command that will be executed. {"git","log","--pretty=oneline","--abbrev-commit"} builtin.git_bcommits = require_on_exported_call("telescope.builtin.__git").bcommits +--- Lists commits for a range of lines in the current buffer with diff preview +--- In visual mode, lists commits for the selected lines +--- With operator mode enabled, lists commits inside the text object/motion +--- - Default keymaps or your overridden `select_` keys: +--- - ``: checks out the currently selected commit +--- - ``: opens a diff in a vertical split +--- - ``: opens a diff in a horizontal split +--- - ``: opens a diff in a new tab +---@param opts table: options to pass to the picker +---@field cwd string: specify the path of the repo +---@field use_git_root boolean: if we should use git root as cwd or the cwd (important for submodule) (default: true) +---@field current_file string: specify the current file that should be used for bcommits (default: current buffer) +---@field git_command table: command that will be executed. the last element must be "-L". {"git","log","--pretty=oneline","--abbrev-commit","--no-patch","-L"} +---@field from number: the first line number in the range (default: current line) +---@field to number: the last line number in the range (default: the value of `from`) +---@field operator boolean: select lines in operator-pending mode (default: false) +builtin.git_bcommits_range = require_on_exported_call("telescope.builtin.__git").bcommits_range + --- List branches for current directory, with output from `git log --oneline` shown in the preview window --- - Default keymaps: --- - ``: checks out the currently selected branch diff --git a/lua/telescope/operators.lua b/lua/telescope/operators.lua new file mode 100644 index 0000000..e7ee8b5 --- /dev/null +++ b/lua/telescope/operators.lua @@ -0,0 +1,23 @@ +local operators = {} + +local last_operator = { callback = function(_) end, opts = {} } + +--- Execute the last saved operator callback and options +operators.operator_callback = function() + last_operator.callback(last_operator.opts) +end + +--- Enters operator-pending mode, then executes callback. +--- See `:h map-operator` +--- +---@param callback function: the function to call after exiting operator-pending +---@param opts table: options to pass to the callback +operators.run_operator = function(callback, opts) + last_operator = { callback = callback, opts = opts } + vim.o.operatorfunc = "v:lua.require'telescope.operators'.operator_callback" + -- feed g@ to enter operator-pending mode + -- 'i' required for which-key compatibility, etc. + vim.api.nvim_feedkeys("g@", "mi", false) +end + +return operators