From b76ddb4d9b383ee7104dd333c9577214468e03ea Mon Sep 17 00:00:00 2001 From: Ali Can Zeybek Date: Sun, 8 Feb 2026 15:02:24 +0300 Subject: [PATCH] tests and terminal --- lua/core/keymaps.lua | 117 +++++++++++++++++++++++++++++- lua/plugins/config/neotest.lua | 65 +++++++++++++++++ lua/plugins/config/toggleterm.lua | 117 ++++++++++++++++++++++++++++++ lua/plugins/config/treesitter.lua | 2 +- lua/plugins/config/whichkey.lua | 4 + lua/plugins/init.lua | 17 +++++ 6 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 lua/plugins/config/neotest.lua create mode 100644 lua/plugins/config/toggleterm.lua diff --git a/lua/core/keymaps.lua b/lua/core/keymaps.lua index 5583535..e651031 100644 --- a/lua/core/keymaps.lua +++ b/lua/core/keymaps.lua @@ -5,6 +5,10 @@ keymap.set("n", "h", "h", { noremap = true, silent = true, desc = " keymap.set("n", "j", "j", { noremap = true, silent = true, desc = "Window down" }) keymap.set("n", "k", "k", { noremap = true, silent = true, desc = "Window up" }) keymap.set("n", "l", "l", { noremap = true, silent = true, desc = "Window right" }) +keymap.set("t", "h", [[h]], { noremap = true, silent = true, desc = "Window left" }) +keymap.set("t", "j", [[j]], { noremap = true, silent = true, desc = "Window down" }) +keymap.set("t", "k", [[k]], { noremap = true, silent = true, desc = "Window up" }) +keymap.set("t", "l", [[l]], { noremap = true, silent = true, desc = "Window right" }) local reload = require("utils.reload") @@ -38,6 +42,113 @@ keymap.set("n", "xr", "Trouble lsp_references toggle", { norema keymap.set("n", "xs", "Trouble symbols toggle focus=false", { noremap = true, silent = true, desc = "Problems document symbols" }) keymap.set("n", "xS", "Trouble lsp toggle focus=false win.position=right", { noremap = true, silent = true, desc = "Problems workspace symbols" }) +local function with_toggleterm(fn) + local ok, toggleterm = pcall(require, "plugins.config.toggleterm") + if not ok then + pcall(function() + require("lazy").load({ plugins = { "toggleterm.nvim" } }) + end) + ok, toggleterm = pcall(require, "plugins.config.toggleterm") + end + if not ok then + vim.notify("toggleterm is not available", vim.log.levels.WARN) + return + end + fn(toggleterm) +end + +keymap.set("n", "mm", function() + with_toggleterm(function(toggleterm) + toggleterm.toggle_shell() + end) +end, { noremap = true, silent = true, desc = "Build terminal" }) +keymap.set("t", "mm", [[lua require("plugins.config.toggleterm").toggle_shell()]], { noremap = true, silent = true, desc = "Build terminal" }) +keymap.set("n", "mc", function() + with_toggleterm(function(toggleterm) + toggleterm.rerun_last() + end) +end, { noremap = true, silent = true, desc = "Build rerun last" }) +keymap.set("n", "m1", function() + with_toggleterm(function(toggleterm) + toggleterm.toggle_build_terminal() + end) +end, { noremap = true, silent = true, desc = "Build toggle terminal" }) +keymap.set("t", "m1", [[lua require("plugins.config.toggleterm").toggle_build_terminal()]], { noremap = true, silent = true, desc = "Build toggle terminal" }) +keymap.set("n", "m2", function() + with_toggleterm(function(toggleterm) + toggleterm.toggle_test_terminal() + end) +end, { noremap = true, silent = true, desc = "Build test terminal" }) +keymap.set("t", "m2", [[lua require("plugins.config.toggleterm").toggle_test_terminal()]], { noremap = true, silent = true, desc = "Build test terminal" }) + +local function with_neotest(fn) + local ok, neotest = pcall(require, "neotest") + if not ok then + vim.notify("neotest is not available", vim.log.levels.WARN) + return + end + fn(neotest) +end + +local function project_root_for_tests() + local current_file = vim.api.nvim_buf_get_name(0) + local start_path = current_file ~= "" and vim.fs.dirname(current_file) or vim.loop.cwd() + local markers = { "CMakePresets.json", "CMakeLists.txt", ".git" } + local found = vim.fs.find(markers, { upward = true, path = start_path })[1] + + if found then + return vim.fs.dirname(found) + end + + return vim.loop.cwd() +end + +keymap.set("n", "tt", function() + with_neotest(function(neotest) + neotest.run.run() + end) +end, { noremap = true, silent = true, desc = "Tests run nearest" }) +keymap.set("n", "tf", function() + with_neotest(function(neotest) + neotest.run.run(vim.fn.expand("%")) + end) +end, { noremap = true, silent = true, desc = "Tests run file" }) +keymap.set("n", "ta", function() + with_neotest(function(neotest) + neotest.run.run(project_root_for_tests()) + end) +end, { noremap = true, silent = true, desc = "Tests run all" }) +keymap.set("n", "tg", function() + with_toggleterm(function(toggleterm) + toggleterm.run_tests() + end) +end, { noremap = true, silent = true, desc = "Tests run via ctest" }) +keymap.set("n", "td", function() + with_neotest(function(neotest) + neotest.run.run({ strategy = "dap" }) + end) +end, { noremap = true, silent = true, desc = "Tests debug nearest" }) +keymap.set("n", "to", function() + with_neotest(function(neotest) + neotest.output.open({ enter = true, auto_close = true }) + end) +end, { noremap = true, silent = true, desc = "Tests open output" }) +keymap.set("n", "tp", function() + with_neotest(function(neotest) + neotest.output_panel.toggle() + end) +end, { noremap = true, silent = true, desc = "Tests toggle output panel" }) +keymap.set("n", "ts", function() + with_neotest(function(neotest) + neotest.summary.toggle() + end) +end, { noremap = true, silent = true, desc = "Tests toggle summary" }) +keymap.set("n", "tS", function() + with_neotest(function(neotest) + neotest.run.stop() + end) +end, { noremap = true, silent = true, desc = "Tests stop" }) + vim.keymap.set("n", "gn", vim.diagnostic.goto_next, { noremap = true, silent = true }) local function with_dap(fn) @@ -163,7 +274,11 @@ keymap.set("n", "dR", "Telescope dap configurations", { noremap keymap.set("n", "dK", "Telescope dap list_breakpoints", { noremap = true, silent = true, desc = "Debug breakpoints" }) keymap.set("n", "cg", "CMakeGenerate", { noremap = true, silent = true, desc = "CMake generate" }) -keymap.set("n", "cb", "CMakeBuild", { noremap = true, silent = true, desc = "CMake build" }) +keymap.set("n", "cb", function() + with_toggleterm(function(toggleterm) + toggleterm.run_build() + end) +end, { noremap = true, silent = true, desc = "CMake build (terminal)" }) keymap.set("n", "cB", "CMakeBuild!", { noremap = true, silent = true, desc = "CMake clean build" }) keymap.set("n", "cc", "CMakeClean", { noremap = true, silent = true, desc = "CMake clean" }) keymap.set("n", "ct", "CMakeSelectBuildTarget", { noremap = true, silent = true, desc = "CMake select build target" }) diff --git a/lua/plugins/config/neotest.lua b/lua/plugins/config/neotest.lua new file mode 100644 index 0000000..9b38059 --- /dev/null +++ b/lua/plugins/config/neotest.lua @@ -0,0 +1,65 @@ +local lib = require("neotest.lib") + +local has_cpp_parser = false +local ok_parsers, parsers = pcall(require, "nvim-treesitter.parsers") +if ok_parsers and parsers.has_parser then + has_cpp_parser = parsers.has_parser("cpp") +end + +if not has_cpp_parser then + vim.schedule(function() + vim.notify( + "neotest-gtest needs Treesitter cpp parser. Run :TSInstall cpp", + vim.log.levels.WARN + ) + end) + return +end + +require("neotest").setup({ + adapters = { + require("neotest-gtest").setup({ + root = lib.files.match_root_pattern( + "CMakeLists.txt", + ".git" + ), + debug_adapter = "codelldb", + is_test_file = function(file) + local normalized = file:gsub("\\", "/") + local is_in_tests_dir = normalized:match("/tests/") ~= nil + local has_test_suffix = normalized:match("_test%.cpp$") + or normalized:match("_test%.cc$") + or normalized:match("_test%.cxx$") + or normalized:match("_test%.c%+%+$") + + return is_in_tests_dir and has_test_suffix ~= nil + end, + filter_dir = function(name, rel_path) + if rel_path == "" then + return true + end + + local blocked = { + [".git"] = true, + [".cache"] = true, + ["build"] = true, + ["out"] = true, + ["_deps"] = true, + } + + if blocked[name] then + return false + end + + if rel_path:match("^build/") or rel_path:match("^out/") then + return false + end + + return true + end, + mappings = { + configure = "C", + }, + }), + }, +}) diff --git a/lua/plugins/config/toggleterm.lua b/lua/plugins/config/toggleterm.lua new file mode 100644 index 0000000..279a272 --- /dev/null +++ b/lua/plugins/config/toggleterm.lua @@ -0,0 +1,117 @@ +local toggleterm = require("toggleterm") +local Terminal = require("toggleterm.terminal").Terminal + +toggleterm.setup({ + start_in_insert = true, + insert_mappings = true, + terminal_mappings = true, + persist_size = true, + persist_mode = true, + close_on_exit = false, + direction = "horizontal", + size = 14, +}) + +local M = {} + +local runners = { + build = { + cmd = "cmake --build --preset debug-with-tidy", + title = "Build", + }, + test = { + cmd = "ctest --test-dir build --output-on-failure", + title = "Tests", + }, +} + +local terminals = { + shell = Terminal:new({ + hidden = true, + close_on_exit = false, + direction = "horizontal", + }), +} + +local last_runner = nil + +local function run_in_terminal(term, cmd) + term:open() + vim.defer_fn(function() + term:send(cmd .. "\r", false) + end, 20) +end + +local function project_root() + local current_file = vim.api.nvim_buf_get_name(0) + local start_path = current_file ~= "" and vim.fs.dirname(current_file) or vim.loop.cwd() + local markers = { "CMakePresets.json", "CMakeLists.txt", ".git" } + local found = vim.fs.find(markers, { upward = true, path = start_path })[1] + + if found then + return vim.fs.dirname(found) + end + + return vim.loop.cwd() +end + +local function get_runner_term(name) + if terminals[name] then + return terminals[name] + end + + terminals[name] = Terminal:new({ + hidden = true, + close_on_exit = false, + direction = "horizontal", + display_name = runners[name].title, + }) + + return terminals[name] +end + +function M.toggle_shell() + terminals.shell.dir = project_root() + terminals.shell:toggle() +end + +function M.run_build() + local term = get_runner_term("build") + term.dir = project_root() + run_in_terminal(term, runners.build.cmd) + last_runner = "build" +end + +function M.run_tests() + local term = get_runner_term("test") + term.dir = project_root() + run_in_terminal(term, runners.test.cmd) + last_runner = "test" +end + +function M.rerun_last() + if not last_runner then + vim.notify("No build/test command has been run yet", vim.log.levels.WARN) + return + end + + if last_runner == "build" then + M.run_build() + else + M.run_tests() + end +end + +function M.toggle_build_terminal() + local term = get_runner_term("build") + term.dir = project_root() + term:toggle() +end + +function M.toggle_test_terminal() + local term = get_runner_term("test") + term.dir = project_root() + term:toggle() +end + +return M diff --git a/lua/plugins/config/treesitter.lua b/lua/plugins/config/treesitter.lua index c943648..6fa0eb4 100644 --- a/lua/plugins/config/treesitter.lua +++ b/lua/plugins/config/treesitter.lua @@ -1,6 +1,6 @@ require'nvim-treesitter.configs'.setup { -- A list of parser names, or "all" (the listed parsers MUST always be installed) - ensure_installed = { "c", "lua", "vim", "vimdoc", "query", "markdown", "markdown_inline", "zig" }, + ensure_installed = { "c", "cpp", "lua", "vim", "vimdoc", "query", "markdown", "markdown_inline", "zig" }, -- Install parsers synchronously (only applied to `ensure_installed`) sync_install = false, diff --git a/lua/plugins/config/whichkey.lua b/lua/plugins/config/whichkey.lua index 1d3610c..949ec66 100644 --- a/lua/plugins/config/whichkey.lua +++ b/lua/plugins/config/whichkey.lua @@ -22,7 +22,9 @@ if wk.add then { "c", group = "CMake" }, { "d", group = "Debug" }, { "f", group = "Find" }, + { "m", group = "Build" }, { "o", group = "OpenCode" }, + { "t", group = "Tests" }, { "x", group = "Problems" }, }) else @@ -30,7 +32,9 @@ else c = { name = "+CMake" }, d = { name = "+Debug" }, f = { name = "+Find" }, + m = { name = "+Build" }, o = { name = "+OpenCode" }, + t = { name = "+Tests" }, x = { name = "+Problems" }, }, { prefix = "" }) end diff --git a/lua/plugins/init.lua b/lua/plugins/init.lua index 850d8bd..13fb1b0 100644 --- a/lua/plugins/init.lua +++ b/lua/plugins/init.lua @@ -85,6 +85,12 @@ return { require("plugins.config.trouble") end, }, + { + "akinsho/toggleterm.nvim", + config = function() + require("plugins.config.toggleterm") + end, + }, { "folke/which-key.nvim", config = function() @@ -103,6 +109,17 @@ return { require("plugins.config.dap") end, }, + { + "nvim-neotest/neotest", + dependencies = { + "nvim-neotest/nvim-nio", + "nvim-lua/plenary.nvim", + "alfaix/neotest-gtest", + }, + config = function() + require("plugins.config.neotest") + end, + }, { "Civitasv/cmake-tools.nvim", config = function()