diff --git a/nvim/.config/nvim/after/plugin/gitsigns.lua b/nvim/.config/nvim/after/plugin/gitsigns.lua new file mode 100644 index 0000000..1951cc2 --- /dev/null +++ b/nvim/.config/nvim/after/plugin/gitsigns.lua @@ -0,0 +1,19 @@ +if vim.g.vscode then + return +end + +if require("greg.gitsigns").setup() then + return +end + +-- First launch may install gitsigns asynchronously; PackChanged retriggers setup. +vim.api.nvim_create_autocmd("PackChanged", { + callback = function(ev) + if ev.data.spec.name ~= "gitsigns.nvim" then + return + end + vim.schedule(function() + require("greg.gitsigns").setup() + end) + end, +}) diff --git a/nvim/.config/nvim/after/plugin/starter.lua b/nvim/.config/nvim/after/plugin/starter.lua index cad45dc..2b68bd6 100644 --- a/nvim/.config/nvim/after/plugin/starter.lua +++ b/nvim/.config/nvim/after/plugin/starter.lua @@ -32,6 +32,11 @@ require("mini.starter").setup({ }, -- Oil { name = "Oil", action = ":Oil", section = "Explorer" }, + { + name = "Git Changed Tabs", + action = ":lua require('greg.git').open_changed_tabs()", + section = "Explorer", + }, -- Configuration { name = "Theme", action = ":Themery", section = "Config" }, { name = "Pack Update", action = ":lua vim.pack.update()", section = "Config" }, diff --git a/nvim/.config/nvim/after/plugin/transparent.lua b/nvim/.config/nvim/after/plugin/transparent.lua index d385a38..459ab60 100644 --- a/nvim/.config/nvim/after/plugin/transparent.lua +++ b/nvim/.config/nvim/after/plugin/transparent.lua @@ -35,5 +35,15 @@ transparent.setup({ extra_groups = { "NormalFloat", -- float panels (Mason, LspInfo, etc.) }, - exclude_groups = {}, + exclude_groups = { + "GitSignsAdd", + "GitSignsChange", + "GitSignsDelete", + "GitSignsAddNr", + "GitSignsChangeNr", + "GitSignsDeleteNr", + "GitSignsAddLn", + "GitSignsChangeLn", + "GitSignsDeleteLn", + }, }) diff --git a/nvim/.config/nvim/lua/greg/git.lua b/nvim/.config/nvim/lua/greg/git.lua new file mode 100644 index 0000000..ca0aeb7 --- /dev/null +++ b/nvim/.config/nvim/lua/greg/git.lua @@ -0,0 +1,117 @@ +local M = {} + +---@return string? +local function git_root() + local result = vim.system({ "git", "rev-parse", "--show-toplevel" }):wait() + if result.code ~= 0 then + return nil + end + return vim.trim(result.stdout or "") +end + +---@param stdout string? +local function append_nul_paths(stdout, seen, files) + if not stdout or stdout == "" then + return + end + for path in stdout:gmatch("[^%z]+") do + if not seen[path] then + seen[path] = true + files[#files + 1] = path + end + end +end + +--- Unstaged modifications and untracked new files (paths relative to repo root). +---@return string[] files +---@return string? root +function M.changed_files() + local root = git_root() + if not root then + return {}, nil + end + + local seen = {} + local files = {} + + local diff = vim + .system({ + "git", + "-C", + root, + "diff", + "--name-only", + "-z", + "--diff-filter=d", + }, { text = false }) + :wait() + if diff.code == 0 then + append_nul_paths(diff.stdout, seen, files) + end + + local untracked = vim + .system({ + "git", + "-C", + root, + "ls-files", + "-z", + "--others", + "--exclude-standard", + }, { text = false }) + :wait() + if untracked.code == 0 then + append_nul_paths(untracked.stdout, seen, files) + end + + table.sort(files) + return files, root +end + +---@param tabnr integer +---@return boolean +local function is_starter_tab(tabnr) + local tabpage = vim.api.nvim_list_tabpages()[tabnr] + if not tabpage then + return false + end + + for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tabpage)) do + if vim.bo[vim.api.nvim_win_get_buf(win)].filetype == "ministarter" then + return true + end + end + + return false +end + +function M.open_changed_tabs() + local files, root = M.changed_files() + if not root then + vim.notify("GitChangedTabs: not a git repository", vim.log.levels.ERROR) + return + end + if #files == 0 then + vim.notify("GitChangedTabs: no unstaged or new files", vim.log.levels.INFO) + return + end + + local home_tab = vim.fn.tabpagenr() + local first_changed_tab = home_tab + 1 + local opened = 0 + for _, rel in ipairs(files) do + local path = vim.fs.joinpath(root, rel) + vim.cmd.tabnew({ args = { vim.fn.fnameescape(path) } }) + opened = opened + 1 + end + + local closed_starter = false + if is_starter_tab(home_tab) then + closed_starter = pcall(vim.cmd.tabclose, home_tab) + end + + vim.cmd.tabn(closed_starter and home_tab or first_changed_tab) + vim.notify(("GitChangedTabs: opened %d file(s)"):format(opened), vim.log.levels.INFO) +end + +return M diff --git a/nvim/.config/nvim/lua/greg/gitsigns.lua b/nvim/.config/nvim/lua/greg/gitsigns.lua new file mode 100644 index 0000000..56908c5 --- /dev/null +++ b/nvim/.config/nvim/lua/greg/gitsigns.lua @@ -0,0 +1,60 @@ +local M = {} + +local setup_done = false + +function M.setup() + if setup_done or vim.g.vscode then + return setup_done + end + + local ok, gitsigns = pcall(require, "gitsigns") + if not ok then + return false + end + + gitsigns.setup({ + word_diff = true, + numhl = true, + signs = { + add = { text = "+" }, + change = { text = "│" }, + delete = { text = "_" }, + topdelete = { text = "‾" }, + changedelete = { text = "~" }, + untracked = { text = "?" }, + }, + on_attach = function(bufnr) + local function map(mode, lhs, rhs, desc) + vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, desc = desc, silent = true }) + end + + map("n", "]c", function() + if vim.wo.diff then + vim.cmd.normal({ "]c", bang = true }) + else + gitsigns.nav_hunk("next") + end + end, "Git: next hunk") + + map("n", "[c", function() + if vim.wo.diff then + vim.cmd.normal({ "[c", bang = true }) + else + gitsigns.nav_hunk("prev") + end + end, "Git: previous hunk") + + map("n", "gi", gitsigns.preview_hunk_inline, "Git: preview hunk inline") + end, + }) + + vim.keymap.set("n", "gt", function() + require("greg.git").open_changed_tabs() + end, { desc = "Git: open changed files in tabs" }) + vim.keymap.set("n", "gw", gitsigns.toggle_word_diff, { desc = "Git: toggle inline word diff" }) + + setup_done = true + return true +end + +return M diff --git a/nvim/.config/nvim/lua/greg/pack.lua b/nvim/.config/nvim/lua/greg/pack.lua index 22172ee..12a1f9b 100644 --- a/nvim/.config/nvim/lua/greg/pack.lua +++ b/nvim/.config/nvim/lua/greg/pack.lua @@ -17,6 +17,11 @@ vim.api.nvim_create_autocmd("PackChanged", { pcall(vim.cmd, "TSUpdate") end) end + if name == "gitsigns.nvim" and (kind == "install" or kind == "update") then + vim.schedule(function() + pcall(require, "greg.gitsigns") + end) + end end, }) @@ -45,6 +50,7 @@ vim.pack.add({ { src = gh("mbbill/undotree"), version = "6fa6b57cda8459e1e4b2ca34df702f55242f4e4d" }, { src = gh("numToStr/Comment.nvim"), version = "e30b7f2008e52442154b66f7c519bfd2f1e32acb" }, { src = gh("tpope/vim-fugitive"), version = "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0" }, + { src = gh("lewis6991/gitsigns.nvim"), version = "2038c666bd9d8a0b7349a0b6ee00dc83104b9ecf" }, { src = gh("nvim-lualine/lualine.nvim"), version = "131a558e13f9f28b15cd235557150ccb23f89286" }, { src = gh("echasnovski/mini.starter"), version = "7bdc9decc8b623f245c1e42a64bc41e61d574c5e" }, diff --git a/nvim/.config/nvim/lua/greg/set.lua b/nvim/.config/nvim/lua/greg/set.lua index 702b71e..72afd5e 100644 --- a/nvim/.config/nvim/lua/greg/set.lua +++ b/nvim/.config/nvim/lua/greg/set.lua @@ -23,7 +23,7 @@ vim.opt.incsearch = true vim.opt.termguicolors = true vim.opt.scrolloff = 8 -vim.opt.signcolumn = "yes" +vim.opt.signcolumn = "auto" vim.opt.isfname:append("@-@") vim.opt.updatetime = 50 diff --git a/nvim/.config/nvim/nvim-pack-lock.json b/nvim/.config/nvim/nvim-pack-lock.json index 298533d..659d97e 100644 --- a/nvim/.config/nvim/nvim-pack-lock.json +++ b/nvim/.config/nvim/nvim-pack-lock.json @@ -25,6 +25,11 @@ "src": "https://github.com/stevearc/conform.nvim", "version": "'dca1a190aa85f9065979ef35802fb77131911106'" }, + "gitsigns.nvim": { + "rev": "2038c666bd9d8a0b7349a0b6ee00dc83104b9ecf", + "src": "https://github.com/lewis6991/gitsigns.nvim", + "version": "'2038c666bd9d8a0b7349a0b6ee00dc83104b9ecf'" + }, "harpoon": { "rev": "1bc17e3e42ea3c46b33c0bbad6a880792692a1b3", "src": "https://github.com/ThePrimeagen/harpoon",