From d63761e1bf1a42d61ca28362b9e8cb2d0d91f309 Mon Sep 17 00:00:00 2001 From: Nicolas GB Date: Thu, 27 Nov 2025 22:07:22 +0100 Subject: [PATCH] feat(bookmark): add bookmark management from log buffer and bookmark_move API (#51) --- README.md | 38 ++++++++++++++++++++-- lua/jj/cmd/init.lua | 72 +++++++++++++++++++++++++++++++++++++++--- lua/jj/cmd/log.lua | 66 ++++++++++++++++++++++++++++++++++---- lua/jj/core/runner.lua | 7 ++++ 4 files changed, 169 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d2b5b87..03e023e 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,16 @@ You can fetch and push directly from the log buffer: - `` - Push all changes to remote - `p` - Push bookmark of revision under cursor to remote +### Manage bookmarks from the log buffer + +- `b` - Create a new bookmark or move an existing one to the revision under cursor + - Select from existing bookmarks to move them + - Or create a new bookmark at that revision + ### Open a PR/MR from the log buffer - `o` - Open a PR/MR for the revision under cursor -- `` - Select a bookmark from all available bookmarks and open a PR/MR +- `` - Select a remote from all available bookmarks and open a PR/MR The plugin automatically: @@ -142,6 +148,7 @@ The plugin provides a `:J` command that accepts jj subcommands: :J fetch " Fetch from remote :J open_pr " Open PR for current change's bookmark :J open_pr --list " Select bookmark from all and open PR +:J bookmark create/move/delete :J # This will use your defined default command :J ``` @@ -220,6 +227,7 @@ The plugin also provides `:Jdiff`, `:Jvdiff`, and `:Jhdiff` commands for diffing undo = "", -- Undo last operation redo = "", -- Redo last undone operation abandon = "a", -- Abandon revision under cursor + bookmark = "b", -- Create or move bookmark to revision under cursor fetch = "f", -- Fetch from remote push = "p", -- Push bookmark of revision under cursor push_all = "", -- Push all changes to remote @@ -359,6 +367,29 @@ cmd.push({ bookmark = "main" }) -- Push only main bookmark cmd.push({ bookmark = "feature" }) -- Push only feature bookmark ``` +### Bookmark Management Command Options + +The `bookmark_create` function creates a new bookmark: + +```lua +local cmd = require("jj.cmd") +cmd.bookmark_create() -- Prompts for bookmark name, then prompts the revision +``` + +The `bookmark_move` function moves an existing bookmark to a new revision: + +```lua +local cmd = require("jj.cmd") +cmd.bookmark_move() -- Select bookmark, then specify new revset +``` + +The `bookmark_delete` function deletes a bookmark: + +```lua +local cmd = require("jj.cmd") +cmd.bookmark_delete() -- Select bookmark to delete +``` + ### Open PR/MR Command Options The `open_pr` function accepts an options table: @@ -445,8 +476,9 @@ diff.open_hsplit({ rev = "@-2" }) -- Horizontal split against @-2 vim.keymap.set("n", "ju", cmd.undo, { desc = "JJ undo" }) vim.keymap.set("n", "jy", cmd.redo, { desc = "JJ redo" }) vim.keymap.set("n", "jr", cmd.rebase, { desc = "JJ rebase" }) - vim.keymap.set("n", "jb", cmd.bookmark_create, { desc = "JJ bookmark create" }) - vim.keymap.set("n", "jB", cmd.bookmark_delete, { desc = "JJ bookmark delete" }) + vim.keymap.set("n", "jbc", cmd.bookmark_create, { desc = "JJ bookmark create" }) + vim.keymap.set("n", "jbd", cmd.bookmark_delete, { desc = "JJ bookmark delete" }) + vim.keymap.set("n", "jbm", cmd.bookmark_move, { desc = "JJ bookmark move" }) vim.keymap.set("n", "ja", cmd.abandon, { desc = "JJ abandon" }) vim.keymap.set("n", "jf", cmd.fetch, { desc = "JJ fetch" }) vim.keymap.set("n", "jp", cmd.push, { desc = "JJ push" }) diff --git a/lua/jj/cmd/init.lua b/lua/jj/cmd/init.lua index 945d242..baf3bba 100644 --- a/lua/jj/cmd/init.lua +++ b/lua/jj/cmd/init.lua @@ -41,6 +41,7 @@ local status_module = require("jj.cmd.status") --- @field push? string|string[] --- @field open_pr? string|string[] --- @field open_pr_list? string|string[] +--- @field bookmark? string|string[] --- @class jj.cmd.status.keymaps --- @field open_file? string|string[] Keymaps for the status command buffer, setting a keymap to nil will disable it @@ -104,6 +105,7 @@ M.config = { push = "p", open_pr = "o", open_pr_list = "", + bookmark = "b", }, status = { open_file = "", @@ -356,11 +358,60 @@ function M.bookmark_create() prompt = "Bookmark name: ", }, function(input) if input then - local cmd = string.format("jj b c %s", input) - runner.execute_command_async(cmd, function() - utils.notify(string.format("Bookmark `%s` created successfully for @", input), vim.log.levels.INFO) - M.log({}) - end, "Error creating bookmark") + -- Get the revset + vim.ui.input({ + prompt = "Revset (default: @): ", + default = "@", + }, function(revset) + revset = revset or "@" + local cmd = string.format("jj b c %s %s", input, revset) + runner.execute_command_async(cmd, function() + utils.notify( + string.format("Bookmark `%s` created successfully for %s", input, revset), + vim.log.levels.INFO + ) + M.log({}) + end, "Error creating bookmark") + end) + else + terminal.close_terminal_buffer() + end + end) +end + +-- Jujutsu bookmark move +function M.bookmark_move() + if not utils.ensure_jj() then + return + end + M.log({}) + local bookmarks = utils.get_all_bookmarks() + if #bookmarks == 0 then + utils.notify("No bookmarks found to move", vim.log.levels.ERROR) + return + end + + vim.ui.select(bookmarks, { + prompt = "Select bookmark to move: ", + }, function(choice) + if choice then + vim.ui.input({ + prompt = "New revset for bookmark '" .. choice .. "': ", + default = "@", + }, function(revset) + if revset then + local cmd = string.format("jj b m %s --to %s", choice, revset) + runner.execute_command_async(cmd, function() + utils.notify( + string.format("Bookmark `%s` moved successfully to %s", choice, revset), + vim.log.levels.INFO + ) + M.log({}) + end, "Error moving bookmark") + else + terminal.close_terminal_buffer() + end + end) else terminal.close_terminal_buffer() end @@ -665,6 +716,17 @@ function M.j(args) M.open_pr() end end, + bookmark = function() + if remaining_args[1] == "create" or remaining_args[1] == "c" then + M.bookmark_create() + elseif remaining_args[1] == "move" or remaining_args[1] == "m" then + M.bookmark_move() + elseif remaining_args[1] == "delete" or remaining_args[1] == "d" then + M.bookmark_delete() + else + terminal.run(cmd, M.terminal_keymaps()) + end + end, } if handlers[subcommand] then diff --git a/lua/jj/cmd/log.lua b/lua/jj/cmd/log.lua index b78fdec..980caae 100644 --- a/lua/jj/cmd/log.lua +++ b/lua/jj/cmd/log.lua @@ -235,9 +235,13 @@ end function M.handle_log_push_all() local cmd = "jj git push" utils.notify("Pushing `ALL` bookmarks", vim.log.levels.INFO) - runner.execute_command_async(cmd, function() - utils.notify("Successfully pushed all to remote", vim.log.levels.INFO) - M.log({}) + runner.execute_command_async(cmd, function(output) + if output and string.find(output, "Nothing changed%.") then + utils.notify("Nothing changed.", vim.log.levels.INFO) + else + utils.notify("Successfully pushed all to remote", vim.log.levels.INFO) + M.log({}) + end end, "Error pushing to remote") end @@ -270,9 +274,13 @@ function M.handle_log_push_bookmark() -- Push the bookmark from the revset found local cmd = string.format("jj git push --bookmark %s -N", bookmark) utils.notify(string.format("Pushing bookmark `%s`...", bookmark), vim.log.levels.INFO) - runner.execute_command_async(cmd, function() - utils.notify(string.format("Successfully pushed bookmark for `%s`", revset), vim.log.levels.INFO) - M.log({}) + runner.execute_command_async(cmd, function(output) + if output and string.find(output, "Nothing changed%.") then + utils.notify("Nothing changed.", vim.log.levels.INFO) + else + utils.notify(string.format("Successfully pushed bookmark for `%s`", revset), vim.log.levels.INFO) + M.log({}) + end end, string.format("Error pushing bookmark for `%s`", revset)) end @@ -331,6 +339,48 @@ function M.handle_log_open_pr(list_bookmarks) utils.open_pr_for_bookmark(bookmark) end +-- Create or move bookmark at revision under cursor in `jj log` buffer +local function handle_log_bookmark() + local line = vim.api.nvim_get_current_line() + local revset = parser.get_rev_from_log_line(line) + if not revset or revset == "" then + return + end + + -- Get all bookmarks + local bookmarks = utils.get_all_bookmarks() + table.insert(bookmarks, 1, "[Create new]") + -- Prompt to select or create a bookmark + vim.ui.select(bookmarks, { + prompt = "Select a bookmark to move or create a new: ", + }, function(choice) + if choice then + if choice == "[Create new]" then + -- Prompt for new bookmark name + vim.ui.input({ prompt = "Enter new bookmark name: " }, function(input) + if input and input ~= "" then + local cmd = string.format("jj bookmark create %s -r %s", input, revset) + runner.execute_command_async(cmd, function() + utils.notify( + string.format("Created bookmark `%s` at `%s`", input, revset), + vim.log.levels.INFO + ) + M.log({}) + end, "Error creating bookmark") + end + end) + else + -- Move existing bookmark to the revision + local cmd = string.format("jj bookmark move %s --to %s", choice, revset) + runner.execute_command_async(cmd, function() + utils.notify(string.format("Moved bookmark `%s` to `%s`", choice, revset), vim.log.levels.INFO) + M.log({}) + end, "Error moving bookmark") + end + end + end) +end + --- Resolve log keymaps from config, filtering out nil values --- @return jj.core.buffer.keymap[] function M.log_keymaps() @@ -414,6 +464,10 @@ function M.log_keymaps() handler = M.handle_log_open_pr, args = { true }, }, + bookmark = { + desc = "Create or move bookmark at revision under cursor", + handler = handle_log_bookmark, + }, } return cmd.merge_keymaps(cmd.resolve_keymaps_from_specs(keymaps, specs), cmd.terminal_keymaps()) diff --git a/lua/jj/core/runner.lua b/lua/jj/core/runner.lua index 457938f..ad64130 100644 --- a/lua/jj/core/runner.lua +++ b/lua/jj/core/runner.lua @@ -46,6 +46,13 @@ function M.execute_command_async(cmd, on_success, error_prefix, input, silent) end end end, + on_stderr = function(_, data) + for _, line in ipairs(data) do + if line ~= "" then + table.insert(output_lines, line) + end + end + end, on_exit = function(_, exit_code) local output = table.concat(output_lines, "\n") if exit_code == 0 then