diff --git a/README.md b/README.md index 463171fc..6cedff33 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,15 @@ home-manager.users..home.packages = [ ### Git +- `codediff.nix`: Configures a read-only review workflow for working-tree, branch, and commit-history diffs. - `jj.nix`: Configures Jujutsu commands and key mappings. - `lazygit.nix`: Configures the LazyGit plugin for Git integration. -- `gitsigns.nix`: Configures the GitSigns plugin for displaying Git diff information. +- `gitsigns.nix`: Configures GitSigns diff markers, hunk navigation, inline previews, blame, and word diffs. ### Utils +- `trouble.nix`: Provides persistent diagnostics, quickfix, location-list, and TODO triage views. +- `overseer.nix`: Runs bounded, repo-aware validation tasks while keeping persistent sessions in cmux. - `telescope.nix`: Configures the Telescope plugin for fuzzy finding and picking. - `whichkey.nix`: Configures the WhichKey plugin for displaying key mappings. - `extra_plugins.nix`: Configures additional plugins. diff --git a/config/default.nix b/config/default.nix index 6dcd2b80..e8b61ff9 100644 --- a/config/default.nix +++ b/config/default.nix @@ -44,11 +44,14 @@ _: { ./plugins/lsp/fidget.nix # Git + ./plugins/git/codediff.nix ./plugins/git/jj.nix ./plugins/git/lazygit.nix ./plugins/git/gitsigns.nix # Utils + ./plugins/utils/trouble.nix + ./plugins/utils/overseer.nix ./plugins/utils/telescope.nix ./plugins/utils/whichkey.nix ./plugins/utils/extra_plugins.nix diff --git a/config/plugins/git/codediff.nix b/config/plugins/git/codediff.nix new file mode 100644 index 00000000..d64edd79 --- /dev/null +++ b/config/plugins/git/codediff.nix @@ -0,0 +1,143 @@ +{ lib, ... }: +let + reviewLua = # lua + '' + local function git_root() + local name = vim.api.nvim_buf_get_name(0) + local start = name ~= "" and vim.fs.dirname(name) or vim.uv.cwd() + return (start and vim.fs.root(start, ".git")) or vim.uv.cwd() + end + + local function git_ref_exists(root, ref) + local result = vim.system( + { "git", "rev-parse", "--verify", "--quiet", ref }, + { cwd = root, text = true } + ):wait() + return result.code == 0 + end + + local function review_base() + local root = git_root() + local result = vim.system( + { "git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD" }, + { cwd = root, text = true } + ):wait() + + if result.code == 0 then + local ref = vim.trim(result.stdout or "") + if ref ~= "" then + return ref + end + end + + for _, ref in ipairs({ "origin/main", "origin/master", "main", "master" }) do + if git_ref_exists(root, ref) then + return ref + end + end + + return nil + end + + local function with_review_base(callback) + local base = review_base() + if not base then + vim.notify("Could not determine the default review branch", vim.log.levels.ERROR) + return + end + callback(base) + end + + vim.api.nvim_create_user_command("CodeReviewBranch", function() + with_review_base(function(base) + vim.cmd("CodeDiff " .. vim.fn.fnameescape(base .. "...HEAD")) + end) + end, { desc = "Review branch changes against its merge base" }) + + vim.api.nvim_create_user_command("CodeReviewHistory", function() + with_review_base(function(base) + vim.cmd("CodeDiff history " .. vim.fn.fnameescape(base .. "..HEAD") .. " --reverse") + end) + end, { desc = "Review branch commits in chronological order" }) + ''; +in +{ + plugins.codediff = { + enable = true; + settings = { + diff = { + layout = "side-by-side"; + compact = true; + compact_context_lines = 4; + compute_moves = true; + cycle_hunks_across_files = true; + disable_inlay_hints = true; + jump_to_first_change = true; + }; + explorer = { + initial_focus = "explorer"; + view_mode = "tree"; + }; + history = { + initial_focus = "history"; + view_mode = "tree"; + }; + keymaps = { + conflict = { + accept_all_both = false; + accept_all_current = false; + accept_all_incoming = false; + accept_both = false; + accept_current = false; + accept_incoming = false; + diffget_current = false; + diffget_incoming = false; + discard = false; + discard_all = false; + }; + view = { + diff_get = false; + diff_put = false; + discard_hunk = false; + stage_hunk = false; + toggle_stage = false; + unstage_hunk = false; + }; + explorer = { + restore = false; + stage_all = false; + unstage_all = false; + }; + }; + }; + }; + + extraConfigLua = lib.mkAfter reviewLua; + + keymaps = [ + { + mode = "n"; + key = "gd"; + action = "CodeDiff"; + options = { + desc = "Review working tree"; + }; + } + { + mode = "n"; + key = "gD"; + action = "CodeReviewBranch"; + options = { + desc = "Review branch changes"; + }; + } + { + mode = "n"; + key = "gH"; + action = "CodeReviewHistory"; + options = { + desc = "Review branch history"; + }; + } + ]; +} diff --git a/config/plugins/git/gitsigns.nix b/config/plugins/git/gitsigns.nix index f66cffe8..0a1201ac 100644 --- a/config/plugins/git/gitsigns.nix +++ b/config/plugins/git/gitsigns.nix @@ -1,7 +1,10 @@ -_: { +{ plugins.gitsigns = { enable = true; settings = { + preview_config = { + border = "rounded"; + }; signs = { add = { text = " "; @@ -22,6 +25,50 @@ _: { text = "󱂧 "; }; }; + word_diff = false; }; }; + + keymaps = [ + { + mode = "n"; + key = "]h"; + action = "Gitsigns nav_hunk next"; + options = { + desc = "Next Git hunk"; + }; + } + { + mode = "n"; + key = "[h"; + action = "Gitsigns nav_hunk prev"; + options = { + desc = "Previous Git hunk"; + }; + } + { + mode = "n"; + key = "ghp"; + action = "Gitsigns preview_hunk_inline"; + options = { + desc = "Preview Git hunk inline"; + }; + } + { + mode = "n"; + key = "ghb"; + action = "Gitsigns blame_line"; + options = { + desc = "Blame Git line"; + }; + } + { + mode = "n"; + key = "gW"; + action = "Gitsigns toggle_word_diff"; + options = { + desc = "Toggle Git word diff"; + }; + } + ]; } diff --git a/config/plugins/git/jj.nix b/config/plugins/git/jj.nix index 3323a976..e16e8516 100644 --- a/config/plugins/git/jj.nix +++ b/config/plugins/git/jj.nix @@ -5,6 +5,7 @@ cmd.describe.editor = { type = "buffer"; }; + diff.backend = "codediff"; }; }; diff --git a/config/plugins/utils/overseer.nix b/config/plugins/utils/overseer.nix new file mode 100644 index 00000000..3bbdefe6 --- /dev/null +++ b/config/plugins/utils/overseer.nix @@ -0,0 +1,147 @@ +{ lib, ... }: +let + projectTasksLua = # lua + '' + local overseer = require("overseer") + + local function find_root(marker, start, kind) + local opts = { + upward = true, + path = start, + } + if kind then + opts.type = kind + end + + local match = vim.fs.find(marker, opts)[1] + return match and vim.fs.dirname(match) or nil + end + + local function validation_task(name, description, command, cwd) + return { + name = name, + desc = description, + tags = { overseer.TAG.TEST }, + builder = function() + return { + cmd = command, + cwd = cwd, + components = { + { + "on_output_quickfix", + items_only = true, + open = false, + set_diagnostics = true, + }, + "default", + }, + } + end, + } + end + + overseer.register_template({ + name = "project checks", + generator = function(search) + local tasks = {} + + local flake_root = find_root("flake.nix", search.dir, "file") + if flake_root and vim.fn.executable("nix") == 1 then + table.insert(tasks, validation_task( + "project: nix flake check", + "Run flake checks, including untracked files, without changing flake.lock", + { "nix", "flake", "check", "path:.", "--no-write-lock-file" }, + flake_root + )) + end + + local go_root = find_root("go.mod", search.dir, "file") + if go_root and vim.fn.executable("go") == 1 then + table.insert(tasks, validation_task( + "project: go test ./...", + "Run all Go tests in the nearest module", + { "go", "test", "./..." }, + go_root + )) + end + + local git_root = find_root(".git", search.dir) + if git_root and vim.fn.executable("git") == 1 then + table.insert(tasks, validation_task( + "project: git diff --check", + "Check the working tree for whitespace errors", + { "git", "diff", "--check" }, + git_root + )) + end + + if vim.tbl_isempty(tasks) then + return "No supported project checks found" + end + return tasks + end, + }) + + vim.api.nvim_create_user_command("OverseerRestartLast", function() + local task_list = require("overseer.task_list") + local tasks = overseer.list_tasks({ + status = { + overseer.STATUS.SUCCESS, + overseer.STATUS.FAILURE, + overseer.STATUS.CANCELED, + }, + sort = task_list.sort_finished_recently, + }) + + if vim.tbl_isempty(tasks) then + vim.notify("No completed tasks found", vim.log.levels.WARN) + return + end + overseer.run_action(tasks[1], "restart") + end, { desc = "Restart the most recently completed task" }) + ''; +in +{ + plugins.overseer = { + enable = true; + settings = { + dap = false; + templates = [ "builtin" ]; + task_list = { + default_detail = 1; + direction = "bottom"; + min_height = 10; + max_height = 20; + }; + }; + }; + + extraConfigLua = lib.mkAfter projectTasksLua; + + keymaps = [ + { + mode = "n"; + key = "or"; + action = "OverseerRun"; + options.desc = "Run project task"; + } + { + mode = "n"; + key = "ot"; + action = "OverseerToggle"; + options.desc = "Toggle task list"; + } + { + mode = "n"; + key = "oa"; + action = "OverseerTaskAction"; + options.desc = "Task action"; + } + { + mode = "n"; + key = "ol"; + action = "OverseerRestartLast"; + options.desc = "Restart last task"; + } + ]; +} diff --git a/config/plugins/utils/trouble.nix b/config/plugins/utils/trouble.nix new file mode 100644 index 00000000..56a3b6f1 --- /dev/null +++ b/config/plugins/utils/trouble.nix @@ -0,0 +1,46 @@ +_: { + plugins.trouble = { + enable = true; + settings = { + auto_close = true; + auto_preview = true; + auto_refresh = true; + focus = true; + follow = true; + multiline = true; + }; + }; + + keymaps = [ + { + mode = "n"; + key = "xx"; + action = "Trouble diagnostics toggle"; + options.desc = "Workspace diagnostics"; + } + { + mode = "n"; + key = "xX"; + action = "Trouble diagnostics toggle filter.buf=0"; + options.desc = "Buffer diagnostics"; + } + { + mode = "n"; + key = "xq"; + action = "Trouble qflist toggle"; + options.desc = "Quickfix list"; + } + { + mode = "n"; + key = "xl"; + action = "Trouble loclist toggle"; + options.desc = "Location list"; + } + { + mode = "n"; + key = "xt"; + action = "TodoTrouble toggle"; + options.desc = "Todo comments"; + } + ]; +} diff --git a/config/settings.nix b/config/settings.nix index 357331e3..4e93f01b 100644 --- a/config/settings.nix +++ b/config/settings.nix @@ -76,6 +76,9 @@ # Don't show mode in the command line showmode = false; + # Improve change alignment in native diff views + diffopt = "internal,filler,closeoff,algorithm:histogram,indent-heuristic,linematch:60"; + # Open new split below the current window splitbelow = true;