Skip to content

Commit 12aece9

Browse files
sk91polarmutex
authored andcommitted
feat: add snacks picker
1 parent 72ac140 commit 12aece9

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

lua/snacks-worktree/init.lua

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
local git_worktree = require('git-worktree')
2+
local Git = require('git-worktree.git')
3+
local Snacks = require('snacks')
4+
local uv = vim.uv or vim.loop
5+
6+
local force_next_deletion = false
7+
8+
---@module "snacks-worktree"
9+
local snacks_worktree = {}
10+
11+
-- Switch to the selected worktree
12+
-- @param picker Snacks.picker
13+
-- @param item Snacks.picker.Item
14+
-- @return nil
15+
local switch_worktree = function(picker, item)
16+
local worktree_path = item.path
17+
if worktree_path == nil then
18+
vim.print('no worktree selected')
19+
end
20+
21+
picker:close()
22+
git_worktree.switch_worktree(worktree_path)
23+
end
24+
25+
-- Toggle the forced deletion of the next worktree
26+
-- @return nil
27+
local toggle_forced_deletion = function()
28+
-- redraw otherwise the message is not displayed when in insert mode
29+
if force_next_deletion then
30+
print('The next deletion will not be forced')
31+
vim.fn.execute('redraw')
32+
else
33+
print('The next deletion will be forced')
34+
vim.fn.execute('redraw')
35+
force_next_deletion = true
36+
end
37+
end
38+
39+
-- Confirm the deletion of a worktree
40+
-- @param item snacks.picker.Item
41+
-- @param proceed fun(val: string)
42+
-- @return nil
43+
local confirm_worktree_deletion = function(item, proceed)
44+
local prompt = 'Delete worktree %q?'
45+
if force_next_deletion then
46+
prompt = 'Force deletion of worktree %q?'
47+
end
48+
Snacks.picker.select({ 'Yes', 'No' }, { prompt = (prompt):format(item.path) }, function(_, idx)
49+
if idx ~= 1 then
50+
print("Didn't delete worktree")
51+
return
52+
end
53+
proceed()
54+
end)
55+
end
56+
57+
-- Confirm the deletion of a branch
58+
-- @return boolean: whether the deletion is confirmed
59+
local confirm_branch_deletion = function()
60+
local confirmed = vim.fn.input('Worktree deleted, now force deletion of branch? [y/n]: ')
61+
62+
if string.sub(string.lower(confirmed), 0, 1) == 'y' then
63+
return true
64+
end
65+
66+
print("Didn't delete branch")
67+
return false
68+
end
69+
70+
local delete_success_handler = function(opts)
71+
opts = opts or {}
72+
force_next_deletion = false
73+
if opts.branch ~= nil and opts.branch ~= 'HEAD' and confirm_branch_deletion() then
74+
local delete_branch_job = Git.delete_branch_job(opts.branch)
75+
if delete_branch_job ~= nil then
76+
delete_branch_job:after_success(vim.schedule_wrap(function()
77+
print('Branch deleted')
78+
end))
79+
delete_branch_job:start()
80+
end
81+
end
82+
end
83+
84+
-- Handler for failed deletion
85+
-- @return nil
86+
local delete_failure_handler = function()
87+
print('Deletion failed, use <C-f> to force the next deletion')
88+
end
89+
90+
-- Delete the selected worktree
91+
-- @param picker Snacks.picker
92+
-- @param item Snacks.picker.Item
93+
-- @return nil
94+
local delete_worktree = function(picker, item)
95+
if not item then
96+
Snacks.notify.warn('No worktree to delete', { title = 'Snacks Picker' })
97+
end
98+
confirm_worktree_deletion(item, function()
99+
local worktree_path = item.path
100+
picker:close()
101+
if worktree_path ~= nil then
102+
git_worktree.delete_worktree(worktree_path, force_next_deletion, {
103+
on_failure = delete_failure_handler,
104+
on_success = delete_success_handler,
105+
})
106+
end
107+
end)
108+
end
109+
110+
-- Create a prompt to get the path of the new worktree
111+
-- @param cb fun(path: string): the callback to call with the path
112+
-- @return nil
113+
local create_input_prompt = function(cb)
114+
vim.ui.input({
115+
prompt = 'Path to subtree',
116+
}, cb)
117+
end
118+
119+
function snacks_worktree.create_worktree()
120+
Snacks.picker {
121+
all = false,
122+
finder = 'git_branches',
123+
format = 'git_branch',
124+
preview = 'git_log',
125+
confirm = function(picker, item)
126+
if not item then
127+
print('No item provided')
128+
end
129+
local branch = item.branch
130+
picker:close()
131+
create_input_prompt(function(name)
132+
if name == '' then
133+
name = branch
134+
end
135+
git_worktree.create_worktree(name, branch)
136+
end)
137+
end,
138+
}
139+
end
140+
141+
local finder = function(opts, ctx)
142+
local args = { 'worktree', 'list' }
143+
local cwd = svim.fs.normalize(opts and opts.cwd or uv.cwd() or '.') or nil
144+
cwd = Snacks.git.get_root(cwd)
145+
local current = Git.toplevel_dir()
146+
return require('snacks.picker.source.proc').proc({
147+
opts,
148+
{
149+
cwd = cwd,
150+
cmd = 'git',
151+
args = args,
152+
---@param item snacks.picker.finder.Item
153+
transform = function(item)
154+
item.cwd = cwd
155+
local fields = vim.split(string.gsub(item.text, '%s+', ' '), ' ')
156+
item.path = fields[1]
157+
item.current = current == item.path
158+
item.sha = fields[2]
159+
item.branch = fields[3]
160+
if item.sha == '(bare)' then
161+
return false
162+
end
163+
end,
164+
},
165+
}, ctx)
166+
end
167+
168+
local format = function(item, picker)
169+
local a = Snacks.picker.util.align
170+
local ret = {} ---@type snacks.picker.Highlight[]
171+
if item.current then
172+
ret[#ret + 1] = { a('', 2), 'SnacksPickerGitBranchCurrent' }
173+
else
174+
ret[#ret + 1] = { a('', 2) }
175+
end
176+
ret[#ret + 1] = { a(item.branch, 30, { truncate = true }), 'SnacksPickerGitBranch' }
177+
ret[#ret + 1] = { a(item.sha, 8, { truncate = true }), 'SnacksPickerGitCommit' }
178+
ret[#ret + 1] = { ' ' }
179+
ret[#ret + 1] = { a(item.path, 100, { truncate = true }), 'SnacksPickerDirectory' }
180+
return ret
181+
end
182+
183+
function snacks_worktree.pick_git_worktree()
184+
if not Snacks then
185+
return
186+
end
187+
local config = {
188+
all = false,
189+
preview = 'none',
190+
finder = finder,
191+
format = format,
192+
layout = {
193+
preview = false,
194+
},
195+
confirm = switch_worktree,
196+
actions = {
197+
delete_worktree = delete_worktree,
198+
toggle_forced_deletion = toggle_forced_deletion,
199+
},
200+
win = {
201+
input = {
202+
keys = {
203+
['<c-d>'] = { 'delete_worktree', mode = { 'n', 'i' } },
204+
['<c-f>'] = { 'toggle_forced_deletion', mode = { 'n', 'i' } },
205+
},
206+
},
207+
},
208+
---@param picker snacks.Picker
209+
on_show = function(picker)
210+
for i, item in ipairs(picker:items()) do
211+
if item.current then
212+
picker.list:view(i)
213+
Snacks.picker.actions.list_scroll_center(picker)
214+
break
215+
end
216+
end
217+
end,
218+
}
219+
220+
Snacks.picker.pick(config)
221+
end
222+
223+
return snacks_worktree

0 commit comments

Comments
 (0)