From b5da55b4782d423e7f732386d228db15bda0f1ce Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Thu, 12 Sep 2024 00:49:26 -0400 Subject: [PATCH] feat(nvim): neotest --- .config/nvim/TODO.md | 64 +++------ .config/nvim/lazy-lock.json | 5 + .config/nvim/lua/config/keymaps.lua | 6 +- .config/nvim/lua/plugins.lua | 171 +++++++++++++++++++++-- .config/nvim/lua/util/terminal.lua | 71 ++++++++++ .config/nvim/lua/util/toggle.lua | 209 ++++++++++++++++++++++++++++ 6 files changed, 462 insertions(+), 64 deletions(-) create mode 100644 .config/nvim/lua/util/terminal.lua create mode 100644 .config/nvim/lua/util/toggle.lua diff --git a/.config/nvim/TODO.md b/.config/nvim/TODO.md index acf5da0..7fde1c6 100644 --- a/.config/nvim/TODO.md +++ b/.config/nvim/TODO.md @@ -1,59 +1,35 @@ # TODO -- add snippet engine back -- code format uses two spaces -- sessions -- ts-comment -- edgy ide setup -- add rust tools back - ---- - -seperate repo for dotfiles (remove bare git repo) -nvim comments https://github.com/folke/ts-comments.nvim - -neotest -update plugins -yank color -syntax groups flash when loading files - -emmet plugin -elixir inlay hints -gx vim.ui.open() - -switch to default nvim keymaps - -indent text object -noice popupmenu background - ## Plugins -- keymaps https://www.lazyvim.org/keymaps#general -- https://github.com/LazyVim/LazyVim/blob/a50f92f7550fb6e9f21c0852e6cb190e6fcd50f5/lua/lazyvim/config/autocmds.lua#L53-L76 +- https://github.com/folke/neoconf.nvim - https://github.com/mfussenegger/nvim-dap -- copilot when trying to add parens/brackets -- https://github.com/neovim/nvim-lspconfig/#suggested-configuration - -- https://github.com/folke/persistence.nvim -- https://github.com/goolord/alpha-nvim -- https://github.com/andymass/vim-matchup -- https://github.com/folke/edgy.nvim - -- LSP set up https://github.com/LazyVim/LazyVim/blob/8c0e39c826f697d668aae336ea26a83be806a543/lua/lazyvim/plugins/lsp/init.lua#L90 -- trouble quickfix https://github.com/LazyVim/LazyVim/blob/0801e52118ef5f48d4ae389d0cefd79814d28a42/lua/lazyvim/plugins/editor.lua#L415 +- https://github.com/folke/persistence.nvim (nvim-lastplace captured by persist?) +- https://github.com/nvimdev/dashboard-nvim +- https://github.com/folke/todo-comments.nvim +- https://github.com/yetone/avante.nvim +- https://github.com/nvim-treesitter/nvim-treesitter-textobjects +- https://github.com/mfussenegger/nvim-lint +- emmet plugin ## Misc -- swap unimpaired mappings https://twitter.com/elijahmanor/status/1615927039529291776 +- trouble in lualine +- add rust tools back +- elixir inlay hints + - .files show up in telescope (e.g. `.env`) +- separate repo for dotfiles (remove bare git repo) + +## Issues + +- edgy terminal background color +- syntax groups flash when loading files +- icon size changes/missing icons in cmp +- neotest output does not auto-scroll in edgy ## Links - https://www.lazyvim.org - https://github.com/folke/dot - https://gist.github.com/rsms/fb463396c95ad8d9efa338a8050a01dc - -- https://nix-community.github.io/home-manager/options.html -- https://daiderd.com/nix-darwin/manual/index.html -- https://search.nixos.org/packages - diff --git a/.config/nvim/lazy-lock.json b/.config/nvim/lazy-lock.json index 77652f5..b7d1284 100644 --- a/.config/nvim/lazy-lock.json +++ b/.config/nvim/lazy-lock.json @@ -18,11 +18,16 @@ "mason.nvim": { "branch": "main", "commit": "e2f7f9044ec30067bc11800a9e266664b88cda22" }, "mini.nvim": { "branch": "main", "commit": "d231729b13da28fd1625c3d85f2315886ddeb05d" }, "neo-tree.nvim": { "branch": "main", "commit": "0774fa2085c62a147fcc7b56f0ac37053cc80217" }, + "neoconf.nvim": { "branch": "main", "commit": "ce074ec29a6a65bfe2064dbed99a25f3d6ddd1fa" }, + "neotest": { "branch": "master", "commit": "6d6ad113f56edc7c3f2a77a0836ea8c1b955ebea" }, + "neotest-elixir": { "branch": "master", "commit": "c5067bcc3ec6a0bba7b5694fa15aeb17f16aeb3c" }, + "neotest-vitest": { "branch": "main", "commit": "9e30dca989a2287cf3fde86b3e138ea7fa4de935" }, "noice.nvim": { "branch": "main", "commit": "448bb9c524a7601035449210838e374a30153172" }, "nui.nvim": { "branch": "main", "commit": "61574ce6e60c815b0a0c4b5655b8486ba58089a1" }, "nvim-cmp": { "branch": "main", "commit": "ae644feb7b67bf1ce4260c231d1d4300b19c6f30" }, "nvim-lastplace": { "branch": "main", "commit": "0bb6103c506315044872e0f84b1f736c4172bb20" }, "nvim-lspconfig": { "branch": "master", "commit": "bdbc65aadc708ce528efb22bca5f82a7cca6b54d" }, + "nvim-nio": { "branch": "master", "commit": "a428f309119086dc78dd4b19306d2d67be884eee" }, "nvim-scrollbar": { "branch": "main", "commit": "d09f14aa16c9f2748e77008f9da7b1f76e4e7b85" }, "nvim-snippets": { "branch": "main", "commit": "56b4052f71220144689caaa2e5b66222ba5661eb" }, "nvim-treesitter": { "branch": "master", "commit": "ccbaee59547425ef8b766433a6020ac191f3151f" }, diff --git a/.config/nvim/lua/config/keymaps.lua b/.config/nvim/lua/config/keymaps.lua index 986d0d3..844060f 100644 --- a/.config/nvim/lua/config/keymaps.lua +++ b/.config/nvim/lua/config/keymaps.lua @@ -173,12 +173,8 @@ map("n", "uI", "InspectTree", { desc = "Inspect Tree" }) -- floating terminal local lazyterm = function() - require("util.terminal")(nil, { cwd = require("util.root")() }) -end -map("n", "ft", lazyterm, { desc = "Terminal" }) -map("n", "fT", function() require("util.terminal")() -end, { desc = "Terminal (cwd)" }) +end map("n", "", lazyterm, { desc = "Terminal" }) map("n", "", lazyterm, { desc = "which_key_ignore" }) diff --git a/.config/nvim/lua/plugins.lua b/.config/nvim/lua/plugins.lua index 2eff67e..7f0f16a 100644 --- a/.config/nvim/lua/plugins.lua +++ b/.config/nvim/lua/plugins.lua @@ -129,6 +129,14 @@ return { { "uE", function() require("edgy").select() end, desc = "Edgy Select Window" }, }, opts = function() + local pos = { + filesystem = "left", + buffers = "top", + git_status = "right", + document_symbols = "bottom", + diagnostics = "bottom", + } + local opts = { animate = { enabled = false }, bottom = { @@ -146,10 +154,7 @@ return { filter = function(buf) return not vim.b[buf].lazyterm_cmd end, - wo = { - -- TODO: Figure out why `close` drops highlight when re-opening - winhighlight = "Normal:Normal,NormalNC:Normal", - }, + wo = { winhighlight = "Normal:Normal,NormalNC:Normal" }, }, "Trouble", { ft = "qf", title = "QuickFix" }, @@ -162,12 +167,11 @@ return { end, }, -- { title = "Spectre", ft = "spectre_panel", size = { height = 0.4 } }, - -- { title = "Neotest Output", ft = "neotest-output-panel", size = { height = 15 } }, - }, - left = { - -- { title = "Neotest Summary", ft = "neotest-summary" }, + { title = "Neotest Output", ft = "neotest-output-panel", size = { height = 15 } }, }, + left = {}, right = { + { title = "Neotest Summary", ft = "neotest-summary", size = { width = 0.25 } }, -- { title = "Grug Far", ft = "grug-far", size = { width = 0.4 } }, }, keys = { @@ -195,13 +199,6 @@ return { } if require("util.init").has("neo-tree.nvim") then - local pos = { - filesystem = "left", - buffers = "top", - git_status = "right", - document_symbols = "bottom", - diagnostics = "bottom", - } local sources = require("util.init").opts("neo-tree.nvim").sources or {} for i, v in ipairs(sources) do table.insert(opts.left, i, { @@ -512,6 +509,126 @@ return { }, }, + -- neotest (https://github.com/nvim-neotest/neotest) + { + "nvim-neotest/neotest", + dependencies = { + "nvim-neotest/nvim-nio", + -- adapters + "jfpedroza/neotest-elixir", + "marilari88/neotest-vitest", + }, + -- stylua: ignore + keys = { + {"t", "", desc = "+test"}, + { "tt", function() require("neotest").run.run(vim.fn.expand("%")) end, desc = "Run File" }, + { "tT", function() require("neotest").run.run(vim.uv.cwd()) end, desc = "Run All Test Files" }, + { "tr", function() require("neotest").run.run() end, desc = "Run Nearest" }, + { "tl", function() require("neotest").run.run_last() end, desc = "Run Last" }, + { "ts", function() require("neotest").summary.toggle() end, desc = "Toggle Summary" }, + { "to", function() require("neotest").output.open({ enter = true, auto_close = true }) end, desc = "Show Output" }, + { "tO", function() require("neotest").output_panel.toggle() end, desc = "Toggle Output Panel" }, + { "tS", function() require("neotest").run.stop() end, desc = "Stop" }, + { "tw", function() require("neotest").watch.toggle(vim.fn.expand("%")) end, desc = "Toggle Watch" }, + }, + opts = { + adapters = { + ["neotest-elixir"] = {}, + ["neotest-vitest"] = { + vitestCommand = "npx vitest", + }, + }, + output = { open_on_run = true }, + quickfix = { + open = function() + if require("util.init").has("trouble.nvim") then + require("trouble").open({ mode = "quickfix", focus = false }) + else + vim.cmd("copen") + end + end, + }, + status = { + signs = false, + virtual_text = true, + }, + }, + config = function(_, opts) + local neotest_ns = vim.api.nvim_create_namespace("neotest") + vim.diagnostic.config({ + virtual_text = { + format = function(diagnostic) + -- Replace newline and tab characters with space for more compact diagnostics + local message = diagnostic.message:gsub("\n", " "):gsub("\t", " "):gsub("%s+", " "):gsub("^%s+", "") + return message + end, + }, + }, neotest_ns) + + if require("util.init").has("trouble.nvim") then + opts.consumers = opts.consumers or {} + -- Refresh and auto close trouble after running tests + ---@type neotest.Consumer + opts.consumers.trouble = function(client) + client.listeners.results = function(adapter_id, results, partial) + if partial then + return + end + local tree = assert(client:get_position(nil, { adapter = adapter_id })) + + local failed = 0 + for pos_id, result in pairs(results) do + if result.status == "failed" and tree:get_key(pos_id) then + failed = failed + 1 + end + end + vim.schedule(function() + local trouble = require("trouble") + if trouble.is_open() then + trouble.refresh() + if failed == 0 then + trouble.close() + end + end + end) + return {} + end + end + end + + if opts.adapters then + local adapters = {} + for name, config in pairs(opts.adapters or {}) do + if type(name) == "number" then + if type(config) == "string" then + config = require(config) + end + adapters[#adapters + 1] = config + elseif config ~= false then + local adapter = require(name) + if type(config) == "table" and not vim.tbl_isempty(config) then + local meta = getmetatable(adapter) + if adapter.setup then + adapter.setup(config) + elseif adapter.adapter then + adapter.adapter(config) + adapter = adapter.adapter + elseif meta and meta.__call then + adapter(config) + else + error("Adapter " .. name .. " does not support setup") + end + end + adapters[#adapters + 1] = adapter + end + end + opts.adapters = adapters + end + + require("neotest").setup(opts) + end, + }, + -- neo-tree.nvim (https://github.com/nvim-neo-tree/neo-tree.nvim) { "nvim-neo-tree/neo-tree.nvim", @@ -903,6 +1020,30 @@ return { settings = {}, }, elixirls = { + keys = { + { + "cp", + function() + local params = vim.lsp.util.make_position_params() + require("util.lsp").execute({ + command = "manipulatePipes:serverid", + arguments = { "toPipe", params.textDocument.uri, params.position.line, params.position.character }, + }) + end, + desc = "To Pipe", + }, + { + "cP", + function() + local params = vim.lsp.util.make_position_params() + require("util.lsp").execute({ + command = "manipulatePipes:serverid", + arguments = { "fromPipe", params.textDocument.uri, params.position.line, params.position.character }, + }) + end, + desc = "From Pipe", + }, + }, settings = { elixirLS = { fetchDeps = false, diff --git a/.config/nvim/lua/util/terminal.lua b/.config/nvim/lua/util/terminal.lua new file mode 100644 index 0000000..b535192 --- /dev/null +++ b/.config/nvim/lua/util/terminal.lua @@ -0,0 +1,71 @@ +---@class util.terminal +---@overload fun(cmd: string|string[], opts: TermOpts): Float +local M = setmetatable({}, { + __call = function(m, ...) + return m.open(...) + end, +}) + +---@type table +local terminals = {} + +---@param shell? string +function M.setup(shell) + vim.o.shell = shell or vim.o.shell +end + +---@class TermOpts: CmdOptions +---@field interactive? boolean +---@field esc_esc? boolean +---@field ctrl_hjkl? boolean + +-- Opens a floating terminal (interactive by default) +---@param cmd? string[]|string +---@param opts? TermOpts +function M.open(cmd, opts) + opts = vim.tbl_deep_extend("force", { + ft = "lazyterm", + size = { width = 0.9, height = 0.9 }, + backdrop = require("util.init").has("edgy.nvim") and not cmd and 100 or nil, + }, opts or {}, { persistent = true }) --[[@as TermOpts]] + + local termkey = vim.inspect({ cmd = cmd or "shell", cwd = opts.cwd, env = opts.env, count = vim.v.count1 }) + + if terminals[termkey] and terminals[termkey]:buf_valid() then + terminals[termkey]:toggle() + else + terminals[termkey] = require("lazy.util").float_term(cmd, opts) + local buf = terminals[termkey].buf + vim.b[buf].lazyterm_cmd = cmd + if opts.esc_esc == false then + vim.keymap.set("t", "", "", { buffer = buf, nowait = true }) + end + if opts.ctrl_hjkl == false then + vim.keymap.set("t", "", "", { buffer = buf, nowait = true }) + vim.keymap.set("t", "", "", { buffer = buf, nowait = true }) + vim.keymap.set("t", "", "", { buffer = buf, nowait = true }) + vim.keymap.set("t", "", "", { buffer = buf, nowait = true }) + end + + vim.keymap.set("n", "gf", function() + local f = vim.fn.findfile(vim.fn.expand("")) + if f ~= "" then + vim.cmd("close") + vim.cmd("e " .. f) + end + end, { buffer = buf }) + + vim.api.nvim_create_autocmd("BufEnter", { + buffer = buf, + callback = function() + vim.cmd.startinsert() + end, + }) + + vim.cmd("noh") + end + + return terminals[termkey] +end + +return M diff --git a/.config/nvim/lua/util/toggle.lua b/.config/nvim/lua/util/toggle.lua new file mode 100644 index 0000000..1c2b161 --- /dev/null +++ b/.config/nvim/lua/util/toggle.lua @@ -0,0 +1,209 @@ +---@class util.toggle +local M = {} + +---@class Toggle +---@field name string +---@field get fun():boolean +---@field set fun(state:boolean) + +---@class Toggle.wrap: Toggle +---@operator call:boolean + +---@param toggle Toggle +function M.wrap(toggle) + return setmetatable(toggle, { + __call = function() + toggle.set(not toggle.get()) + local state = toggle.get() + if state then + require("util.init").info("Enabled " .. toggle.name, { title = toggle.name }) + else + require("util.init").warn("Disabled " .. toggle.name, { title = toggle.name }) + end + return state + end, + }) --[[@as Toggle.wrap]] +end + +---@param lhs string +---@param toggle Toggle +function M.map(lhs, toggle) + local t = M.wrap(toggle) + require("util.init").safe_keymap_set("n", lhs, function() + t() + end, { desc = "Toggle " .. toggle.name }) + M.wk(lhs, toggle) +end + +function M.wk(lhs, toggle) + if not require("util.init").has("which-key.nvim") then + return + end + local function safe_get() + local ok, enabled = pcall(toggle.get) + if not ok then + require("util.init").error( + { "Failed to get toggle state for **" .. toggle.name .. "**:\n", enabled }, + { once = true } + ) + end + return enabled + end + require("which-key").add({ + { + lhs, + icon = function() + return safe_get() and { icon = " ", color = "green" } or { icon = " ", color = "yellow" } + end, + desc = function() + return (safe_get() and "Disable " or "Enable ") .. toggle.name + end, + }, + }) +end + +M.treesitter = M.wrap({ + name = "Treesitter Highlight", + get = function() + return vim.b.ts_highlight + end, + set = function(state) + if state then + vim.treesitter.start() + else + vim.treesitter.stop() + end + end, +}) + +---@param buf? boolean +function M.format(buf) + return M.wrap({ + name = "Auto Format (" .. (buf and "Buffer" or "Global") .. ")", + get = function() + if not buf then + return vim.g.autoformat == nil or vim.g.autoformat + end + return require("util.format").enabled() + end, + set = function(state) + require("util.format").enable(state, buf) + end, + }) +end + +---@param opts? {values?: {[1]:any, [2]:any}, name?: string} +function M.option(option, opts) + opts = opts or {} + local name = opts.name or option + local on = opts.values and opts.values[2] or true + local off = opts.values and opts.values[1] or false + return M.wrap({ + name = name, + get = function() + return vim.opt_local[option]:get() == on + end, + set = function(state) + vim.opt_local[option] = state and on or off + end, + }) +end + +local nu = { number = true, relativenumber = true } +M.number = M.wrap({ + name = "Line Numbers", + get = function() + return vim.opt_local.number:get() or vim.opt_local.relativenumber:get() + end, + set = function(state) + if state then + vim.opt_local.number = nu.number + vim.opt_local.relativenumber = nu.relativenumber + else + nu = { number = vim.opt_local.number:get(), relativenumber = vim.opt_local.relativenumber:get() } + vim.opt_local.number = false + vim.opt_local.relativenumber = false + end + end, +}) + +M.diagnostics = M.wrap({ + name = "Diagnostics", + get = function() + local enabled = false + if vim.diagnostic.is_enabled then + enabled = vim.diagnostic.is_enabled() + elseif vim.diagnostic.is_disabled then + enabled = not vim.diagnostic.is_disabled() + end + return enabled + end, + set = function(state) + if vim.fn.has("nvim-0.10") == 0 then + if state then + pcall(vim.diagnostic.enable) + else + pcall(vim.diagnostic.disable) + end + else + vim.diagnostic.enable(state) + end + end, +}) + +M.inlay_hints = M.wrap({ + name = "Inlay Hints", + get = function() + return vim.lsp.inlay_hint.is_enabled({ bufnr = 0 }) + end, + set = function(state) + vim.lsp.inlay_hint.enable(state, { bufnr = 0 }) + end, +}) + +---@type {k:string, v:any}[] +M._maximized = nil +M.maximize = M.wrap({ + name = "Maximize", + get = function() + return M._maximized ~= nil + end, + set = function(state) + if state then + M._maximized = {} + local function set(k, v) + table.insert(M._maximized, 1, { k = k, v = vim.o[k] }) + vim.o[k] = v + end + set("winwidth", 999) + set("winheight", 999) + set("winminwidth", 10) + set("winminheight", 4) + vim.cmd("wincmd =") + -- `QuitPre` seems to be executed even if we quit a normal window, so we don't want that + -- `VimLeavePre` might be another consideration? Not sure about differences between the 2 + vim.api.nvim_create_autocmd("ExitPre", { + once = true, + group = vim.api.nvim_create_augroup("tmm_restore_max_exit_pre", { clear = true }), + desc = "Restore width/height when close Neovim while maximized", + callback = function() + M.maximize.set(false) + end, + }) + else + for _, opt in ipairs(M._maximized) do + vim.o[opt.k] = opt.v + end + M._maximized = nil + vim.cmd("wincmd =") + end + end, +}) + +setmetatable(M, { + __call = function(m, ...) + return m.option(...) + end, +}) + +return M