---@mod dap-lldb LLDB extension for nvim-dap ---@brief [[ ---An extension for nvim-dap to provide C, C++, and Rust debugging support. ---@brief ]] local M = {} local ts_query = [[ (mod_item name: (identifier) @module body: (declaration_list (attribute_item (attribute (identifier) @attribute (#eq? @attribute "test"))) (function_item name: (identifier) @function))) ]] local function require_dap() local ok, dap = pcall(require, "dap") assert(ok, "nvim-dap is required to use dap-lldb") return dap end local function compiler_error(input) local _, json = pcall(vim.fn.json_decode, input) if type(json) == "table" and json.reason == "compiler-message" then return json.message.rendered end return nil end local function compiler_target(input) local _, json = pcall(vim.fn.json_decode, input) if type(json) == "table" and json.reason == "compiler-artifact" and json.executable ~= nil and (vim.tbl_contains(json.target.kind, "bin") or json.profile.test) then return json.executable end return nil end local function read_target() local cwd = vim.fs.joinpath(vim.fn.getcwd(), "") return vim.fn.input("Path to executable: ", cwd, "file") end local function list_targets(selection) local arg = string.format("--%s", selection or "bins") local cmd = { "cargo", "build", arg, "--quiet", "--message-format", "json" } local out = vim.fn.systemlist(cmd) if vim.v.shell_error ~= 0 then local errors = vim.tbl_map(compiler_error, out) vim.notify(table.concat(errors, "\n"), vim.log.levels.ERROR) return nil end local function filter(e) return e ~= nil end return vim.tbl_filter(filter, vim.tbl_map(compiler_target, out)) end local function select_target(selection) local targets = list_targets(selection) if targets == nil then return nil end if #targets == 0 then return read_target() end if #targets == 1 then return targets[1] end local options = { "Select a target:" } for index, target in ipairs(targets) do local name = vim.fs.basename(target) local option = string.format("%d. %s", index, name) table.insert(options, option) end local choice = vim.fn.inputlist(options) return targets[choice] end local function select_test() local filetype = vim.bo.filetype if filetype ~= "rust" or vim.treesitter.language.get_lang(filetype) == nil then return nil end local bufnr = vim.api.nvim_get_current_buf() local query = vim.treesitter.query.parse(filetype, ts_query) local parser = vim.treesitter.get_parser(bufnr, filetype) local tree = parser:parse()[1] local root = tree:root() local stop = vim.api.nvim_win_get_cursor(0)[1] local mod = nil local fun = nil for id, node in query:iter_captures(root, bufnr, 0, stop) do local capture = query.captures[id] if capture == "module" then mod = vim.treesitter.get_node_text(node, 0) elseif capture == "function" then fun = vim.treesitter.get_node_text(node, 0) end end if not mod or not fun then return nil end return string.format("%s::%s", mod, fun) end local function read_args() local args = vim.fn.input("Enter args: ") return vim.split(args, " ", { trimempty = true }) end local function default_configurations(dap) local cfg = { name = "Debug", type = "lldb", request = "launch", cwd = "${workspaceFolder}", program = read_target, stopOnEntry = false, } dap.configurations.c = { cfg, vim.tbl_extend("force", cfg, { name = "Debug (+args)", args = read_args }), vim.tbl_extend("force", cfg, { name = "Attach debugger", request = "attach" }), } dap.configurations.cpp = vim.tbl_extend("keep", {}, dap.configurations.c) dap.configurations.rust = { vim.tbl_extend("force", cfg, { program = select_target }), vim.tbl_extend("force", cfg, { name = "Debug (+args)", program = select_target, args = read_args }), vim.tbl_extend("force", cfg, { name = "Debug tests", program = function() return select_target("tests") end, args = { "--test-threads=1" }, }), vim.tbl_extend("force", cfg, { name = "Debug tests (+args)", program = function() return select_target("tests") end, args = function() return vim.list_extend(read_args(), { "--test-threads=1" }) end, }), vim.tbl_extend("force", cfg, { name = "Debug test (cursor)", program = function() return select_target("tests") end, args = function() local test = select_test() local args = test and { "--exact", test } or {} return vim.list_extend(args, { "--test-threads=1" }) end, }), vim.tbl_extend("force", cfg, { name = "Attach debugger", request = "attach", program = select_target }), } end local function custom_configurations(dap, opts) if type(opts.configurations) == "table" then for lang, cfg in pairs(opts.configurations) do local config = dap.configurations[lang] or {} dap.configurations[lang] = vim.list_extend(config, cfg) end end end ---@class SetupOpts ---@field codelldb_path string|nil Path to CodeLLDB extension ---@field configurations table|nil Per programming language configuration ---@see https://github.com/vadimcn/codelldb/blob/master/MANUAL.md ---Register LLDB debug adapter ---@param opts SetupOpts|nil See |dap-lldb.SetupOpts| function M.setup(opts) opts = type(opts) == "table" and opts or {} local dap = require_dap() local codelldb = opts.codelldb_path or "codelldb" dap.adapters.lldb = { type = "server", port = "${port}", executable = { command = codelldb, args = { "--port", "${port}" }, detached = vim.loop.os_uname().sysname ~= "Windows", }, } default_configurations(dap) custom_configurations(dap, opts) end ---Debug test function above the cursor function M.debug_test() if vim.bo.filetype ~= "rust" then vim.notify("This feature is available only for Rust", vim.log.levels.ERROR) return nil end local dap = require_dap() local cfg = dap.configurations.rust[5] dap.run(cfg) end return M