Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(git): Add bcommits_range picker #2398

Merged
merged 12 commits into from
Jul 22, 2023
Merged
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<cr>`, reset mixed `<C-r>m`, reset soft `<C-r>s` and reset hard `<C-r>h` |
| `builtin.git_bcommits` | Lists buffer's git commits with diff preview and checks them out on `<cr>` |
| `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 `<cr>`, track action `<C-t>`, rebase action`<C-r>`, create action `<C-a>`, switch action `<C-s>`, delete action `<C-d>` and merge action `<C-y>` |
| `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 `<cr>` |
Expand Down
30 changes: 30 additions & 0 deletions doc/telescope.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- `<cr>`: checks out the currently selected commit
- `<c-v>`: opens a diff in a vertical split
- `<c-x>`: opens a diff in a horizontal split
- `<c-t>`: 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
Expand Down
192 changes: 119 additions & 73 deletions lua/telescope/builtin/__git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -110,87 +111,132 @@ 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))
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", "--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 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
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

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()
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 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 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)
Expand Down
18 changes: 18 additions & 0 deletions lua/telescope/builtin/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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:
--- - `<cr>`: checks out the currently selected commit
--- - `<c-v>`: opens a diff in a vertical split
--- - `<c-x>`: opens a diff in a horizontal split
--- - `<c-t>`: 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:
--- - `<cr>`: checks out the currently selected branch
Expand Down
23 changes: 23 additions & 0 deletions lua/telescope/operators.lua
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think after running this line we should clear the operator? Or perhaps on line 7? This way we drop the reference to the picker and it can be garbage collected later, which seems like a good idea.

Maybe it should be:

operators.operator_callback = function()
  last_operator.callback(last_operator.opts)
  last_operator = { callback = function(_) end, opts = {} }
end

Copy link
Contributor Author

@aaronkollasch aaronkollasch Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I tried something like that, but it breaks the key g@ to re-run the last operator. Not sure if that's a big deal or not as I don't use that keymap, but I didn't see a huge reason to break it.

How important is it to garbage collect the picker reference and opts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense.

It is kind of important, but it should be alright cause there is at most one outstanding, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it's only a reference to the most recent picker/callback function used with operators.run_operator. At least for this picker it would just keep a reference to git.bcommits_range and its opts.

end

return operators