diff --git a/README.md b/README.md index 59cd695..c1c9b36 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,34 @@ This plugin aims to be something like vim-fugitive but for driving the jj-vcs CL - `squash` - Squash the current diff to it's parent - Picker for for [Snacks.nvim](https://github.com/folke/snacks.nvim) - `jj status` Displays the current changes diffs - - `jj log` Displays a buffer's history changes and allows to edit it's change + - `jj file_history` Displays a buffer's history changes and allows to edit it's change (including immutable changes) + +## Enhanced integrations + +Here are some cool features you can do with jj.nvim + +### Diff any change + +You can diff any change in your log history by simply pressing `d` on it's line, yeah just like that! +![Diff-from-log](https://github.com/NicolasGB/jj.nvim/raw/main/assets/diff-log.gif) + +### Edit mutable changes + +Jumping up and down your log history ? + +In your log ouptut press `CR` in a line to directly edit a `mutable` change. +![Edit-from-log](https://github.com/NicolasGB/jj.nvim/raw/main/assets/edit-log.gif) + +### Open a changed file + +Just press enter to open the a file from the `status` output in your current window. +![Open-status](https://github.com/NicolasGB/jj.nvim/raw/main/assets/enter-status.gif) + +### Restore a changed file + +Press `X` on a file from the `status` output and that's it, it's restored. + +![Restore-status](https://github.com/NicolasGB/jj.nvim/raw/main/assets/x-status.gif) ## Installation diff --git a/assets/diff-log.gif b/assets/diff-log.gif new file mode 100644 index 0000000..6216e09 Binary files /dev/null and b/assets/diff-log.gif differ diff --git a/assets/edit-log.gif b/assets/edit-log.gif new file mode 100644 index 0000000..60fea52 Binary files /dev/null and b/assets/edit-log.gif differ diff --git a/assets/enter-status.gif b/assets/enter-status.gif new file mode 100644 index 0000000..17da305 Binary files /dev/null and b/assets/enter-status.gif differ diff --git a/assets/x-status.gif b/assets/x-status.gif new file mode 100644 index 0000000..b81a929 Binary files /dev/null and b/assets/x-status.gif differ diff --git a/lua/jj/cmd.lua b/lua/jj/cmd.lua index dfd2bec..963b83e 100644 --- a/lua/jj/cmd.lua +++ b/lua/jj/cmd.lua @@ -14,6 +14,16 @@ local state = { --- The current job id for the terminal buffer --- @type integer|nil job_id = nil, + + -- The floating buffer if any + --- @type integer|nil + floating_buf = nil, + -- The floating channel to communciate with the terminal + --- @type integer|nil + floating_chan = nil, + --- The floating job id for the terminal buffer + --- @type integer|nil + floating_job_id = nil, } --- Close the current terminal buffer if it exists @@ -25,6 +35,22 @@ local function close_terminal_buffer() end end +--- Close the current terminal buffer if it exists +local function close_floating_buffer() + if state.buf and vim.api.nvim_buf_is_valid(state.floating_buf) then + vim.cmd("bwipeout! " .. state.floating_buf) + else + vim.cmd("close") + end +end + +--- Hide the current floating window +local function hide_floating_window() + if state.floating_buf and vim.api.nvim_buf_is_valid(state.floating_buf) then + vim.cmd("hide") + end +end + local function parse_file_info_from_status_line() local line = vim.api.nvim_get_current_line() @@ -188,10 +214,201 @@ local function handle_log_enter() if not success then return end + + utils.notify(string.format("Editing change: `%s`", revset), vim.log.levels.INFO) -- Close the terminal buffer close_terminal_buffer() end end +--- +--- +--- Create a floating window for terminal output +--- @param config table Window configuration options +--- @param enter boolean Whether to enter the window after creation +--- @return integer buf Buffer number +--- @return integer win Window number +local function create_floating_window(config, enter) + local default_config = { + width = math.floor(vim.o.columns * 0.8), + height = math.floor(vim.o.lines * 0.8), + row = math.floor((vim.o.lines - math.floor(vim.o.lines * 0.8)) / 2), + col = math.floor((vim.o.columns - math.floor(vim.o.columns * 0.8)) / 2), + relative = "editor", + style = "minimal", + border = "rounded", + title = " JJ Diff ", + title_pos = "center", + } + + local merged_config = vim.tbl_extend("force", default_config, config or {}) + + -- Create buffer + local buf = vim.api.nvim_create_buf(false, true) + + -- Create window + local win = vim.api.nvim_open_win(buf, enter or false, merged_config) + + -- Set buffer options + vim.bo[buf].bufhidden = "hide" + + -- Set window options + vim.wo[win].wrap = true + vim.wo[win].number = false + vim.wo[win].relativenumber = false + vim.wo[win].cursorline = false + vim.wo[win].signcolumn = "no" + + return buf, win +end + +--- Run the command in a floating window +--- @param cmd string The command to run in the floating window +local function run_floating(cmd) + -- Clean up previous state if invalid + if state.floating_buf and not vim.api.nvim_buf_is_valid(state.floating_buf) then + state.floating_buf = nil + state.floating_chan = nil + state.floating_job_id = nil + end + + -- Stop any running job first + if state.floating_job_id then + vim.fn.jobstop(state.floating_job_id) + state.floating_job_id = nil + end + + -- Close previous channel + if state.floating_chan then + vim.fn.chanclose(state.floating_chan) + state.floating_chan = nil + end + + local win, buf + if state.floating_buf and vim.api.nvim_buf_is_valid(state.floating_buf) then + -- Find or create a window for the buffer + local buf_wins = vim.fn.win_findbuf(state.floating_buf) + if #buf_wins > 0 then + vim.api.nvim_set_current_win(buf_wins[1]) + win = buf_wins[1] + else + -- If the buffer is hidden create a new window and override the buffer of it + _, win = create_floating_window({}, true) + vim.api.nvim_win_set_buf(win, state.floating_buf) + end + else + -- Otherwise create a new buffer + buf, win = create_floating_window({}, true) + state.floating_buf = buf + end + + -- Create new terminal channel + local chan = vim.api.nvim_open_term(state.floating_buf, {}) + if not chan or chan <= 0 then + vim.notify("Failed to create terminal channel", vim.log.levels.ERROR) + return + end + state.floating_chan = chan + + -- Clear terminal before running new command + vim.api.nvim_chan_send(chan, "\27[H\27[2J") + + local jid = vim.fn.jobstart(cmd, { + pty = true, + width = vim.api.nvim_win_get_width(win), + height = vim.api.nvim_win_get_height(win), + env = { + TERM = "xterm-256color", + PAGER = "cat", + DELTA_PAGER = "cat", + COLORTERM = "truecolor", + }, + on_stdout = function(_, data) + if not vim.api.nvim_buf_is_valid(state.floating_buf) then + return + end + local output = table.concat(data, "\n") + vim.api.nvim_chan_send(chan, output) + end, + on_exit = function(_, _) + vim.schedule(function() + if vim.api.nvim_buf_is_valid(state.floating_buf) then + vim.bo[state.floating_buf].modifiable = false + if vim.api.nvim_get_current_buf() == state.floating_buf then + vim.cmd("stopinsert") + end + end + end) + end, + }) + + -- Set keymaps only if they haven't been set for this buffer + if not vim.b[state.floating_buf].jj_keymaps_set then + vim.keymap.set( + { "n", "v" }, + "i", + function() end, + { buffer = state.floating_buf, noremap = true, silent = true } + ) + vim.keymap.set( + { "n", "v" }, + "c", + function() end, + { buffer = state.floating_buf, noremap = true, silent = true } + ) + vim.keymap.set( + { "n", "v" }, + "a", + function() end, + { buffer = state.floating_buf, noremap = true, silent = true } + ) + vim.keymap.set( + { "n", "v" }, + "q", + close_floating_buffer, + { buffer = state.floating_bufbuf, noremap = true, silent = true, desc = "Close the floating buffer" } + ) + vim.keymap.set( + { "n" }, + "", + hide_floating_window, + { buffer = state.floating_bufbuf, noremap = true, silent = true, desc = "Hide the buffer" } + ) + vim.b[state.floating_buf].jj_keymaps_set = true + end + + -- Set up cleanup autocmd only once per buffer + if not vim.b[state.floating_buf].jj_cleanup_set then + vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, { + buffer = state.floating_buf, + callback = function() + if state.floating_buf and vim.api.nvim_buf_is_valid(state.floating_buf) then + state.floating_buf = nil + end + if state.floating_chan then + vim.fn.chanclose(chan) + end + if jid then + vim.fn.jobstop(jid) + end + end, + }) + vim.b[state.floating_buf].jj_cleanup_set = true + end +end + +--- Handle diffing a log line +local function handle_log_diff() + local line = vim.api.nvim_get_current_line() + + local revset = get_rev_from_log_line(line) + + if revset then + local cmd = string.format("jj diff -r %s", revset) + run_floating(cmd) + else + utils.notify("No valid revision found in the log line", vim.log.levels.ERROR) + end +end --- Run a command and show it's output in a terminal buffer --- If a previous command already existed it smartly reuses the buffer cleaning the previous output @@ -291,16 +508,47 @@ local function run(cmd) vim.keymap.set({ "n", "v" }, "i", function() end, { buffer = state.buf, noremap = true, silent = true }) vim.keymap.set({ "n", "v" }, "c", function() end, { buffer = state.buf, noremap = true, silent = true }) vim.keymap.set({ "n", "v" }, "a", function() end, { buffer = state.buf, noremap = true, silent = true }) - vim.keymap.set({ "n", "v" }, "q", close_terminal_buffer, { buffer = state.buf, noremap = true, silent = true }) - vim.keymap.set({ "n" }, "", close_terminal_buffer, { buffer = state.buf, noremap = true, silent = true }) + vim.keymap.set( + { "n", "v" }, + "q", + close_terminal_buffer, + { buffer = state.buf, noremap = true, silent = true, desc = "Close the terminal buffer" } + ) + vim.keymap.set( + { "n" }, + "", + close_terminal_buffer, + { buffer = state.buf, noremap = true, silent = true, desc = "Close the terminal buffer" } + ) -- Add Enter key mapping for status buffers to open files local cmd_parts = vim.split(cmd, " ") if cmd_parts[2] == "st" or cmd_parts[2] == "status" then - vim.keymap.set({ "n" }, "", handle_status_enter, { buffer = state.buf, noremap = true, silent = true }) - vim.keymap.set({ "n" }, "X", handle_status_restore, { buffer = state.buf, noremap = true, silent = true }) + vim.keymap.set( + { "n" }, + "", + handle_status_enter, + { buffer = state.buf, noremap = true, silent = true, desc = "Open file under cursor" } + ) + vim.keymap.set( + { "n" }, + "X", + handle_status_restore, + { buffer = state.buf, noremap = true, silent = true, desc = "Restore file under cursor" } + ) elseif cmd_parts[2] == "log" then - vim.keymap.set({ "n" }, "", handle_log_enter, { buffer = state.buf, noremap = true, silent = true }) + vim.keymap.set( + { "n" }, + "", + handle_log_enter, + { buffer = state.buf, noremap = true, silent = true, desc = "Edit change under cursor" } + ) + vim.keymap.set( + { "n" }, + "d", + handle_log_diff, + { buffer = state.buf, noremap = true, silent = true, desc = "Diff change under cursor" } + ) end vim.b[state.buf].jj_keymaps_set = true