|
| 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