From 83f9f90f1f6c69d4595c76240ecd2ce87f8b00c6 Mon Sep 17 00:00:00 2001 From: iofq Date: Sat, 21 Jun 2025 00:32:20 -0500 Subject: [PATCH] jj mini/snacks rice --- nvim/lazy-lock.json | 26 +++++ nvim/lua/plugins/lib/minidiff_jj.lua | 165 +++++++++++++++++++++++++++ nvim/lua/plugins/lib/snacks_jj.lua | 133 +++++++++++++++++++++ nvim/lua/plugins/mini.lua | 23 ++-- nvim/lua/plugins/snacks.lua | 18 +++ 5 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 nvim/lazy-lock.json create mode 100644 nvim/lua/plugins/lib/minidiff_jj.lua create mode 100644 nvim/lua/plugins/lib/snacks_jj.lua diff --git a/nvim/lazy-lock.json b/nvim/lazy-lock.json new file mode 100644 index 0000000..736c290 --- /dev/null +++ b/nvim/lazy-lock.json @@ -0,0 +1,26 @@ +{ + "blink-copilot": { "branch": "main", "commit": "bdc45bbbed2ec252b3a29f4adecf031e157b5573" }, + "blink-ripgrep.nvim": { "branch": "main", "commit": "0a2c3c1ce8c3c56e7490cae835a981d5dbeb472c" }, + "blink.cmp": { "branch": "main", "commit": "4f38ce99a472932d5776337f08f7a8180f1f571a" }, + "conform.nvim": { "branch": "master", "commit": "372fc521f8421b7830ea6db4d6ea3bae1c77548c" }, + "copilot.lua": { "branch": "master", "commit": "a5c390f8d8e85b501b22dcb2f30e0cbbd69d5ff0" }, + "diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" }, + "eyeliner.nvim": { "branch": "main", "commit": "8f197eb30cecdf4c2cc9988a5eecc6bc34c0c7d6" }, + "lazy.nvim": { "branch": "main", "commit": "6c3bda4aca61a13a9c63f1c1d1b16b9d3be90d7a" }, + "mini.nvim": { "branch": "main", "commit": "ee23e1abc206efc6d6cce19ec8c0a3da7a897035" }, + "nightfox.nvim": { "branch": "main", "commit": "ba47d4b4c5ec308718641ba7402c143836f35aa9" }, + "nvim-bqf": { "branch": "main", "commit": "9cbec7cf8ad2a902a0a41241ad16c3489620321b" }, + "nvim-lint": { "branch": "master", "commit": "9dfb77ef6c5092a19502883c02dc5a02ec648729" }, + "nvim-lspconfig": { "branch": "master", "commit": "1b801f68d09e70e59e6dd967b663b6d84ee3e87d" }, + "nvim-treesitter": { "branch": "master", "commit": "94ea4f436d2b59c80f02e293466c374584f03b8c" }, + "nvim-treesitter-context": { "branch": "master", "commit": "6daca3ad780f045550b820f262002f35175a6c04" }, + "nvim-treesitter-textobjects": { "branch": "master", "commit": "ed373482db797bbf71bdff37a15c7555a84dce47" }, + "oil.nvim": { "branch": "master", "commit": "685cdb4ffa74473d75a1b97451f8654ceeab0f4a" }, + "refactoring.nvim": { "branch": "master", "commit": "2be7ea3f10b7e59658f5abf6dffc50b5d61964d6" }, + "render-markdown.nvim": { "branch": "main", "commit": "b2d857c848c2c27440c8e5efc8e49a9b5bcf13c6" }, + "scope.nvim": { "branch": "main", "commit": "3fc963e75f88990a9467ff72b8eea667a69c30a2" }, + "snacks.nvim": { "branch": "main", "commit": "bc0630e43be5699bb94dadc302c0d21615421d93" }, + "treewalker.nvim": { "branch": "main", "commit": "34bf0a6044e0b5e3d93b7012ae7bdf457de91ba1" }, + "trouble.nvim": { "branch": "main", "commit": "85bedb7eb7fa331a2ccbecb9202d8abba64d37b3" }, + "yanky.nvim": { "branch": "main", "commit": "04775cc6e10ef038c397c407bc17f00a2f52b378" } +} diff --git a/nvim/lua/plugins/lib/minidiff_jj.lua b/nvim/lua/plugins/lib/minidiff_jj.lua new file mode 100644 index 0000000..81ac4d9 --- /dev/null +++ b/nvim/lua/plugins/lib/minidiff_jj.lua @@ -0,0 +1,165 @@ +local diff = require('mini.diff') +local JJ = { + cache = {}, + jj_cache = {}, +} + +JJ.get_buf_realpath = function(buf_id) + return vim.loop.fs_realpath(vim.api.nvim_buf_get_name(buf_id)) or '' +end + +JJ.jj_start_watching_tree_state = function(buf_id, path) + local stdout = vim.loop.new_pipe() + local args = { 'workspace', 'root' } + local spawn_opts = { + args = args, + cwd = vim.fn.fnamemodify(path, ':h'), + stdio = { nil, stdout, nil }, + } + + local on_not_in_jj = vim.schedule_wrap(function() + if not vim.api.nvim_buf_is_valid(buf_id) then + JJ.cache[buf_id] = nil + return + end + diff.fail_attach(buf_id) + JJ.jj_cache[buf_id] = {} + end) + + local process, stdout_feed = nil, {} + local on_exit = function(exit_code) + process:close() + + -- Watch index only if there was no error retrieving path to it + if exit_code ~= 0 or stdout_feed[1] == nil then + return on_not_in_jj() + end + + -- Set up index watching + local jj_dir_path = table.concat(stdout_feed, ''):gsub('\n+$', '') .. '/.jj/working_copy' + JJ.jj_setup_tree_state_watch(buf_id, jj_dir_path) + + -- Set reference text immediately + JJ.jj_set_ref_text(buf_id) + end + + process = vim.loop.spawn('jj', spawn_opts, on_exit) + JJ.jj_read_stream(stdout, stdout_feed) +end + +JJ.jj_setup_tree_state_watch = function(buf_id, jj_dir_path) + local buf_fs_event, timer = vim.loop.new_fs_event(), vim.loop.new_timer() + local buf_jj_set_ref_text = function() + JJ.jj_set_ref_text(buf_id) + end + + local watch_tree_state = function(_, filename, _) + if filename ~= 'tree_state' then + return + end + -- Debounce to not overload during incremental staging (like in script) + timer:stop() + timer:start(50, 0, buf_jj_set_ref_text) + end + buf_fs_event:start(jj_dir_path, { recursive = false }, watch_tree_state) + + JJ.jj_invalidate_cache(JJ.jj_cache[buf_id]) + JJ.jj_cache[buf_id] = { fs_event = buf_fs_event, timer = timer } +end + +JJ.jj_set_ref_text = vim.schedule_wrap(function(buf_id) + if not vim.api.nvim_buf_is_valid(buf_id) then + return + end + + local buf_set_ref_text = vim.schedule_wrap(function(text) + pcall(diff.set_ref_text, buf_id, text) + end) + + -- NOTE: Do not cache buffer's name to react to its possible rename + local path = JJ.get_buf_realpath(buf_id) + if path == '' then + return buf_set_ref_text {} + end + local cwd, basename = vim.fn.fnamemodify(path, ':h'), vim.fn.fnamemodify(path, ':t') + + -- Set + local stdout = vim.loop.new_pipe() + local spawn_opts = { + args = { 'file', 'show', '-r', '@-', './' .. basename }, + cwd = cwd, + stdio = { nil, stdout, nil }, + } + + local process, stdout_feed = nil, {} + local on_exit = function(exit_code) + process:close() + + if exit_code ~= 0 or stdout_feed[1] == nil then + return buf_set_ref_text {} + end + + -- Set reference text accounting for possible 'crlf' end of line in index + local text = table.concat(stdout_feed, ''):gsub('\r\n', '\n') + buf_set_ref_text(text) + end + + process = vim.loop.spawn('jj', spawn_opts, on_exit) + JJ.jj_read_stream(stdout, stdout_feed) +end) + +JJ.jj_read_stream = function(stream, feed) + local callback = function(err, data) + if data ~= nil then + return table.insert(feed, data) + end + if err then + feed[1] = nil + end + stream:close() + end + stream:read_start(callback) +end + +JJ.jj_invalidate_cache = function(cache) + if cache == nil then + return + end + pcall(vim.loop.fs_event_stop, cache.fs_event) + pcall(vim.loop.timer_stop, cache.timer) +end + +local jj = function() + local attach = function(buf_id) + -- Try attaching to a buffer only once + if JJ.jj_cache[buf_id] ~= nil then + return false + end + -- - Possibly resolve symlinks to get data from the original repo + local path = JJ.get_buf_realpath(buf_id) + if path == '' then + return false + end + + JJ.jj_cache[buf_id] = {} + JJ.jj_start_watching_tree_state(buf_id, path) + end + + local detach = function(buf_id) + local cache = JJ.jj_cache[buf_id] + JJ.jj_cache[buf_id] = nil + JJ.jj_invalidate_cache(cache) + end + + local apply_hunks = function(_, _) + -- staging does not apply for jj + end + + return { + name = 'jj', + attach = attach, + detach = detach, + apply_hunks = apply_hunks, + } +end +return jj diff --git a/nvim/lua/plugins/lib/snacks_jj.lua b/nvim/lua/plugins/lib/snacks_jj.lua new file mode 100644 index 0000000..c69f0e1 --- /dev/null +++ b/nvim/lua/plugins/lib/snacks_jj.lua @@ -0,0 +1,133 @@ +local M = {} + +function M.status() + local function get_files() + local status_raw = vim.fn.system('jj diff --no-pager --quiet --summary') + local files = {} + + for status in status_raw:gmatch('[^\r\n]+') do + local state, text = string.match(status, '^(%a)%s(.+)$') + + if state and text then + local file = text + + local hl = '' + if state == 'A' then + hl = 'SnacksPickerGitStatusAdded' + elseif state == 'M' then + hl = 'SnacksPickerGitStatusModified' + elseif state == 'D' then + hl = 'SnacksPickerGitStatusDeleted' + elseif state == 'R' then + hl = 'SnacksPickerGitStatusRenamed' + file = string.match(text, '{.-=>%s*(.-)}') + end + + local diff = vim.fn.system('jj diff ' .. file .. ' --no-pager --stat --git') + table.insert(files, { + text = text, + file = file, + filename_hl = hl, + state = state, + diff = diff, + }) + end + end + + return files + end + + local files = get_files() + + Snacks.picker.pick { + source = 'jj_status', + items = files, + format = 'file', + title = 'jj status', + preview = function(ctx) + if ctx.item.file then + Snacks.picker.preview.diff(ctx) + else + ctx.preview:reset() + ctx.preview:set_title('No preview') + end + end, + } +end + +function M.revs() + local function jj_new() + return function(picker, item) + picker:close() + if item then + if not item.rev then + Snacks.notify.warn('No branch or commit found', { title = 'Snacks Picker' }) + return + end + local cmd = { 'jj', 'new', '-r', item.rev } + Snacks.picker.util.cmd(cmd, function() + Snacks.notify('Checking out revision: ' .. item.rev, { title = 'Snacks Picker' }) + vim.cmd.checktime() + end, { cwd = item.cwd }) + end + end + end + + local function jj_rev_cmd(rev) + if rev ~= nil then + return vim.fn.system { 'jj', 'show', '--git', '-r', rev } + else + return 'No preview available.' + end + end + + local function jj_log(revset) + if revset == nil then + revset = '-r @' + else + revset = '-r ' .. revset + end + local status_raw = vim.fn.system( + 'jj log ' + .. revset + .. + ' --template \'if(root, format_root_commit(self), label(if(current_working_copy, "working_copy"), concat( format_short_commit_header(self) ++ " ", separate(" ", if(empty, label("empty", "(empty)")), if(description, description.first_line(), label(if(empty, "empty"), description_placeholder),),) ++ "\n",),))\'' + ) + local lines = {} + + for line in status_raw:gmatch('[^\r\n]+') do + local sign, rev, description = string.match(line, '(.)%s*(%a+)(.*)') + table.insert(lines, { + text = line, + file = line, + sign = sign, + rev = rev, + hl = 'SnacksPickerGitMsg', + description = description, + diff = jj_rev_cmd(rev), + }) + end + + return lines + end + + local lines = jj_log('::@') + + Snacks.picker.pick { + source = 'jj_revs', + items = lines, + format = 'text', + title = 'jj log', + confirm = jj_new(), + preview = function(ctx) + if ctx.item.file then + Snacks.picker.preview.diff(ctx) + else + ctx.preview:reset() + ctx.preview:set_title('No rev found') + end + end, + } +end + +return M diff --git a/nvim/lua/plugins/mini.lua b/nvim/lua/plugins/mini.lua index 62dff12..7cff3df 100644 --- a/nvim/lua/plugins/mini.lua +++ b/nvim/lua/plugins/mini.lua @@ -100,9 +100,9 @@ return { '%<', -- Mark general truncate point { hl = 'MiniStatuslineFilename', strings = { '' } }, '%=', -- End left alignment - { hl = 'MiniStatusDevinfo', strings = { diagnostics, lsp } }, + { hl = 'MiniStatusDevinfo', strings = { diagnostics, lsp } }, { hl = 'MiniStatuslineFilename', strings = { search } }, - { hl = mode_hl, strings = { mode } }, + { hl = mode_hl, strings = { mode } }, } end, inactive = function() @@ -146,7 +146,16 @@ return { require('mini.surround').setup() require('mini.splitjoin').setup { detect = { separator = '[,;\n]' } } - require('mini.diff').setup { options = { wrap_goto = true } } + + local jj = require('nvim.lua.plugins.lib.minidiff_jj') + local diff = require('mini.diff') + diff.setup { + options = { wrap_goto = true }, + source = { + jj(), + diff.gen_source.git(), + }, + } local miniclue = require('mini.clue') miniclue.setup { triggers = { @@ -169,10 +178,10 @@ return { miniclue.gen_clues.registers(), miniclue.gen_clues.windows(), miniclue.gen_clues.z(), - { mode = 'n', keys = 'wj', postkeys = 'w', desc = 'TS Down' }, - { mode = 'n', keys = 'wk', postkeys = 'w', desc = 'TS Up' }, - { mode = 'n', keys = 'wh', postkeys = 'w', desc = 'TS Left' }, - { mode = 'n', keys = 'wl', postkeys = 'w', desc = 'TS Right' }, + { mode = 'n', keys = 'wj', postkeys = 'w', desc = 'TS Down' }, + { mode = 'n', keys = 'wk', postkeys = 'w', desc = 'TS Up' }, + { mode = 'n', keys = 'wh', postkeys = 'w', desc = 'TS Left' }, + { mode = 'n', keys = 'wl', postkeys = 'w', desc = 'TS Right' }, { mode = 'n', keys = 'w', postkeys = 'w', desc = 'Swap TS Down' }, { mode = 'n', keys = 'w', postkeys = 'w', desc = 'Swap TS Up' }, { mode = 'n', keys = 'w', postkeys = 'w', desc = 'Swap TS Left' }, diff --git a/nvim/lua/plugins/snacks.lua b/nvim/lua/plugins/snacks.lua index 2e8d55f..2a92033 100644 --- a/nvim/lua/plugins/snacks.lua +++ b/nvim/lua/plugins/snacks.lua @@ -234,6 +234,24 @@ return { silent = true, desc = 'pick notifications', }, + { + 'fj', + function() + require('nvim.lua.plugins.lib.snacks_jj').status() + end, + noremap = true, + silent = true, + desc = 'pick notifications', + }, + { + 'fr', + function() + require('nvim.lua.plugins.lib.snacks_jj').revs() + end, + noremap = true, + silent = true, + desc = 'pick notifications', + }, }, }, }