diff --git a/README.md b/README.md index 91096de..f5c0072 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ This plugin aims to be something like vim-fugitive but for piloting the jj-vcs C - `new` - Create a new change - `edit` - Edit a change - `squash` - Squash the current diff to it's parent +- Picker `git status` like for [Snacks.nvim](https://github.com/folke/snacks.nvim) ## Installation @@ -65,6 +66,10 @@ The plugin provides a `:J` command that accepts jj subcommands: vim.keymap.set("n", "js", cmd.status, { desc = "JJ status" }) vim.keymap.set("n", "dj", cmd.diff, { desc = "JJ diff" }) vim.keymap.set("n", "sj", cmd.squash, { desc = "JJ squash" }) + vim.keymap.set("n", "gj", function() + require("jj.picker").status() + end, { desc = "JJ Picker status" }) + -- Some functions like `describe` or `log` can take parameters vim.keymap.set("n", "jl", function() diff --git a/lua/jj/init.lua b/lua/jj/init.lua index ea23fcb..e285a77 100644 --- a/lua/jj/init.lua +++ b/lua/jj/init.lua @@ -1,22 +1,24 @@ local M = {} local cmd = require("jj.cmd") - -M.picker_config = { - snacks = { - layout = "horizontal", - }, -} +local picker = require("jj.picker") --- Jujutsu plugin configuration --- @class jj.Config M.config = { -- Default configuration + --- @type jj.picker.config + picker = { + snacks = {}, + }, } --- Setup the plugin --- @param opts jj.Config: Options to configure the plugin function M.setup(opts) M.config = vim.tbl_deep_extend("force", M.config, opts or {}) + + picker.setup(opts.picker) + cmd.register_command() end diff --git a/lua/jj/picker.lua b/lua/jj/picker.lua new file mode 100644 index 0000000..cc77ac6 --- /dev/null +++ b/lua/jj/picker.lua @@ -0,0 +1,75 @@ +local utils = require("jj.utils") + +--- @class jj.picker + +--- @class jj.picker.config +--- @field snacks table|boolean The snacks config + +--- @class jj.picker.file +--- @field file string The path of the file +--- @field change string The type of change in the file +--- @field diff_cmd string The command to get the diff of the file + +local M = { + --- @type jj.picker.config + config = { + snacks = {}, + }, +} + +--- Initializes the picker +--- @param opts jj.picker.config +function M.setup(opts) + M.config = vim.tbl_deep_extend("force", M.config, opts or {}) +end + +--- Gets the files in the current jj repository +--- @return jj.picker.file[]|nil A list of files with their changes or nil if not in a jj repo +local function get_files() + local diff_ouptut, ok = utils.execute_command("jj diff --summary --quiet") + if not ok then + return + end + + if type(diff_ouptut) ~= "string" then + return utils.notify("Could not get diff output", vim.log.levels.ERROR) + end + + local files = {} + + -- Split the output into lines + local lines = vim.split(diff_ouptut, "\n", { trimempty = true }) + + for _, line in ipairs(lines) do + local change, file_path = line:match("^(%a)%s(.+)$") + + table.insert(files, { + file = file_path, + change = change, + diff_cmd = string.format("jj diff %s", file_path), + }) + end + + return files +end + +--- Displays in the configurated picker the status of the files +function M.status() + -- Ensure jj is installed + if not utils.ensure_jj() then + return + end + + local files = get_files() + if not files or #files == 0 then + return utils.notify("`Picker`: No diffs found", vim.log.levels.INFO) + end + + if M.config.snacks then + require("jj.picker.snacks").status(M.config, files) + else + return utils.notify("No `picker` enabled", vim.log.levels.INFO) + end +end + +return M diff --git a/lua/jj/picker/snacks.lua b/lua/jj/picker/snacks.lua new file mode 100644 index 0000000..fe9f0c9 --- /dev/null +++ b/lua/jj/picker/snacks.lua @@ -0,0 +1,40 @@ +local utils = require("jj.utils") + +--- @class jj.picker.snacks +local M = {} + +--- Displays the status files in a snacks picker +---@param opts jj.picker.config +---@param files jj.picker.file[] +function M.status(opts, files) + if not opts.snacks then + return utils.notify("Snacks picker is `disabled`", vim.log.levels.INFO) + end + + local snacks = require("snacks") + + local snacks_opts + -- If its true we default to an empty table + if opts.snacks == true then + snacks_opts = {} + else + --- Otherwise we get the table from the config + ---@type table + snacks_opts = opts.snacks + end + + local merged_opts = vim.tbl_deep_extend("force", snacks_opts, { + source = "jj", + items = files, + title = "JJ Status", + preview = function(ctx) + if ctx.item.file then + snacks.picker.preview.cmd(ctx.item.diff_cmd, ctx, {}) + end + end, + }) + + snacks.picker.pick(merged_opts) +end + +return M diff --git a/lua/jj/utils.lua b/lua/jj/utils.lua index 891fbeb..5927092 100644 --- a/lua/jj/utils.lua +++ b/lua/jj/utils.lua @@ -1,25 +1,44 @@ --- @class jj.utils -local M = {} +local M = { + executable_cache = {}, + dependency_cache = {}, +} --- Cache for executable checks to avoid repeated system calls -local executable_cache = {} --- Check if an executable exists in PATH --- @param name string The name of the executable to check --- @return boolean True if executable exists, false otherwise function M.has_executable(name) - if executable_cache[name] ~= nil then - return executable_cache[name] + if M.executable_cache[name] ~= nil then + return M.executable_cache[name] end local exists = vim.fn.executable(name) == 1 - executable_cache[name] = exists + M.executable_cache[name] = exists return exists end +--- Check if the dependency is currently installed +--- @param module string The dependency module +--- @return boolean +function M.has_dependency(module) + if M.dependency_cache[module] ~= nil then + return true + end + + local exists, _ = pcall(require, module) + if not exists then + M.notify(string.format("Module %s not installed", module), vim.log.levels.ERROR) + return false + end + + return true +end + --- Clear the executable cache (useful for testing or if PATH changes) function M.clear_executable_cache() - executable_cache = {} + M.executable_cache = {} end --- Check if jj executable exists, show error if not @@ -34,15 +53,20 @@ end --- Execute a system command and return output with error handling --- @param cmd string The command to execute ---- @param error_prefix string Optional error message prefix +--- @param error_prefix string|nil Optional error message prefix --- @return string|nil output The command output, or nil if failed --- @return boolean success Whether the command succeeded function M.execute_command(cmd, error_prefix) local output = vim.fn.system(cmd) local success = vim.v.shell_error == 0 - if not success and error_prefix then - local error_message = string.format("%s: %s", error_prefix, output) + if not success then + local error_message + if error_prefix then + error_message = string.format("%s: %s", error_prefix, output) + else + error_message = output + end M.notify(error_message, vim.log.levels.ERROR) return nil, false end @@ -57,7 +81,7 @@ function M.is_jj_repo() return false end - local _, success = M.execute_command("jj status", "") + local _, success = M.execute_command("jj status") return success end @@ -68,7 +92,7 @@ function M.get_jj_root() return nil end - local output, success = M.execute_command("jj root", "") + local output, success = M.execute_command("jj root") if success and output then return vim.trim(output) end