feat(debugger): golang adapter for dap

This commit is contained in:
Guz
2025-12-12 10:34:38 -03:00
parent a9a59ec260
commit be99745640
4 changed files with 478 additions and 0 deletions

27
lua/dap-go/COPYRIGHT Normal file
View File

@@ -0,0 +1,27 @@
Contents of `dap-go/init.lua` and `dap-go/ts.lua` were copied from Leonardo Luz Almeida's
nvim-dap-go plugin at Git commit `b4421153ead5d726603b02743ea40cf26a51ed5f` <https://github.com/leoluz/nvim-dap-go/tree/b4421153ead5d726603b02743ea40cf26a51ed5f>,
which is licensed under the MIT License. A copy of the original license can be found at
the nvim-dap-go's GitHub repositoy <https://github.com/leoluz/nvim-dap-go/blob/b4421153ead5d726603b02743ea40cf26a51ed5f/LICENSE>
and below:
MIT License
Copyright (c) 2023 Leonardo Luz Almeida
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

272
lua/dap-go/init.lua Normal file
View File

@@ -0,0 +1,272 @@
local ts = require("dap-go.ts")
local M = {
last_testname = "",
last_testpath = "",
test_buildflags = "",
test_verbose = false,
}
local default_config = {
delve = {
path = "dlv",
initialize_timeout_sec = 20,
port = "${port}",
args = {},
build_flags = "",
-- Automatically handle the issue on delve Windows versions < 1.24.0
-- where delve needs to be run in attched mode or it will fail (actually crashes).
detached = vim.fn.has("win32") == 0,
output_mode = "remote",
},
tests = {
verbose = false,
},
}
local internal_global_config = {}
local function load_module(module_name)
local ok, module = pcall(require, module_name)
assert(ok, string.format("dap-go dependency error: %s not installed", module_name))
return module
end
local function get_arguments()
return coroutine.create(function(dap_run_co)
local args = {}
vim.ui.input({ prompt = "Args: " }, function(input)
args = vim.split(input or "", " ")
coroutine.resume(dap_run_co, args)
end)
end)
end
local function get_build_flags(config)
return coroutine.create(function(dap_run_co)
local build_flags = config.build_flags
vim.ui.input({ prompt = "Build Flags: " }, function(input)
build_flags = vim.split(input or "", " ")
coroutine.resume(dap_run_co, build_flags)
end)
end)
end
local function filtered_pick_process()
local opts = {}
vim.ui.input(
{ prompt = "Search by process name (lua pattern), or hit enter to select from the process list: " },
function(input)
opts["filter"] = input or ""
end
)
return require("dap.utils").pick_process(opts)
end
local function setup_delve_adapter(dap, config)
local args = { "dap", "-l", "127.0.0.1:" .. config.delve.port }
vim.list_extend(args, config.delve.args)
local delve_config = {
type = "server",
port = config.delve.port,
executable = {
command = config.delve.path,
args = args,
detached = config.delve.detached,
cwd = config.delve.cwd,
},
options = {
initialize_timeout_sec = config.delve.initialize_timeout_sec,
},
}
dap.adapters.go = function(callback, client_config)
if client_config.port == nil then
callback(delve_config)
return
end
local host = client_config.host
if host == nil then
host = "127.0.0.1"
end
local listener_addr = host .. ":" .. client_config.port
delve_config.port = client_config.port
delve_config.executable.args = { "dap", "-l", listener_addr }
callback(delve_config)
end
end
local function setup_go_configuration(dap, configs)
local common_debug_configs = {
{
type = "go",
name = "Debug",
request = "launch",
program = "${file}",
buildFlags = configs.delve.build_flags,
outputMode = configs.delve.output_mode,
},
{
type = "go",
name = "Debug (Arguments)",
request = "launch",
program = "${file}",
args = get_arguments,
buildFlags = configs.delve.build_flags,
outputMode = configs.delve.output_mode,
},
{
type = "go",
name = "Debug (Arguments & Build Flags)",
request = "launch",
program = "${file}",
args = get_arguments,
buildFlags = get_build_flags,
outputMode = configs.delve.output_mode,
},
{
type = "go",
name = "Debug Package",
request = "launch",
program = "${fileDirname}",
buildFlags = configs.delve.build_flags,
outputMode = configs.delve.output_mode,
},
{
type = "go",
name = "Attach",
mode = "local",
request = "attach",
processId = filtered_pick_process,
buildFlags = configs.delve.build_flags,
},
{
type = "go",
name = "Debug test",
request = "launch",
mode = "test",
program = "${file}",
buildFlags = configs.delve.build_flags,
outputMode = configs.delve.output_mode,
},
{
type = "go",
name = "Debug test (go.mod)",
request = "launch",
mode = "test",
program = "./${relativeFileDirname}",
buildFlags = configs.delve.build_flags,
outputMode = configs.delve.output_mode,
},
}
if dap.configurations.go == nil then
dap.configurations.go = {}
end
for _, config in ipairs(common_debug_configs) do
table.insert(dap.configurations.go, config)
end
if configs == nil or configs.dap_configurations == nil then
return
end
for _, config in ipairs(configs.dap_configurations) do
if config.type == "go" then
table.insert(dap.configurations.go, config)
end
end
end
function M.setup(opts)
internal_global_config = vim.tbl_deep_extend("force", default_config, opts or {})
M.test_buildflags = internal_global_config.delve.build_flags
M.test_verbose = internal_global_config.tests.verbose
local dap = load_module("dap")
setup_delve_adapter(dap, internal_global_config)
setup_go_configuration(dap, internal_global_config)
end
local function debug_test(testname, testpath, build_flags, extra_args, custom_config)
local dap = load_module("dap")
local config = {
type = "go",
name = testname,
request = "launch",
mode = "test",
program = testpath,
args = { "-test.run", "^" .. testname .. "$" },
buildFlags = build_flags,
outputMode = "remote",
}
config = vim.tbl_deep_extend("force", config, custom_config or {})
if not vim.tbl_isempty(extra_args) then
table.move(extra_args, 1, #extra_args, #config.args + 1, config.args)
end
dap.run(config)
end
function M.debug_test(custom_config)
local test = ts.closest_test()
if test.name == "" or test.name == nil then
vim.notify("no test found")
return false
end
M.last_testname = test.name
M.last_testpath = test.package
local msg = string.format("starting debug session '%s : %s'...", test.package, test.name)
vim.notify(msg)
local extra_args = {}
if M.test_verbose then
extra_args = { "-test.v" }
end
debug_test(test.name, test.package, M.test_buildflags, extra_args, custom_config)
return true
end
function M.debug_last_test()
local testname = M.last_testname
local testpath = M.last_testpath
if testname == "" then
vim.notify("no last run test found")
return false
end
local msg = string.format("starting debug session '%s : %s'...", testpath, testname)
vim.notify(msg)
local extra_args = {}
if M.test_verbose then
extra_args = { "-test.v" }
end
debug_test(testname, testpath, M.test_buildflags, extra_args)
return true
end
function M.get_build_flags()
return get_build_flags(internal_global_config)
end
function M.get_arguments()
return get_arguments()
end
return M

175
lua/dap-go/ts.lua Normal file
View File

@@ -0,0 +1,175 @@
local M = {}
local tests_query = [[
(function_declaration
name: (identifier) @testname
parameters: (parameter_list
. (parameter_declaration
type: (pointer_type) @type) .)
(#match? @type "*testing.(T|M)")
(#match? @testname "^Test.+$")) @parent
]]
local subtests_query = [[
(call_expression
function: (selector_expression
operand: (identifier)
field: (field_identifier) @run)
arguments: (argument_list
(interpreted_string_literal) @testname
[
(func_literal)
(identifier)
])
(#eq? @run "Run")) @parent
]]
local function format_subtest(testcase, test_tree)
local parent
if testcase.parent then
for _, curr in pairs(test_tree) do
if curr.name == testcase.parent then
parent = curr
break
end
end
return string.format("%s/%s", format_subtest(parent, test_tree), testcase.name)
else
return testcase.name
end
end
local function get_closest_above_cursor(test_tree)
local result
for _, curr in pairs(test_tree) do
if not result then
result = curr
else
local node_row1, _, _, _ = curr.node:range()
local result_row1, _, _, _ = result.node:range()
if node_row1 > result_row1 then
result = curr
end
end
end
if result then
return format_subtest(result, test_tree)
end
return nil
end
local function is_parent(dest, source)
if not (dest and source) then
return false
end
if dest == source then
return false
end
local current = source
while current ~= nil do
if current == dest then
return true
end
current = current:parent()
end
return false
end
local function get_closest_test()
local stop_row = vim.api.nvim_win_get_cursor(0)[1]
local ft = vim.api.nvim_buf_get_option(0, "filetype")
assert(ft == "go", "can only find test in go files, not " .. ft)
local parser = vim.treesitter.get_parser(0)
local root = (parser:parse()[1]):root()
local test_tree = {}
local test_query = vim.treesitter.query.parse(ft, tests_query)
assert(test_query, "could not parse test query")
for _, match, _ in test_query:iter_matches(root, 0, 0, stop_row, { all = true }) do
local test_match = {}
for id, nodes in pairs(match) do
for _, node in ipairs(nodes) do
local capture = test_query.captures[id]
if capture == "testname" then
local name = vim.treesitter.get_node_text(node, 0)
test_match.name = name
end
if capture == "parent" then
test_match.node = node
end
end
end
table.insert(test_tree, test_match)
end
local subtest_query = vim.treesitter.query.parse(ft, subtests_query)
assert(subtest_query, "could not parse test query")
for _, match, _ in subtest_query:iter_matches(root, 0, 0, stop_row, { all = true }) do
local test_match = {}
for id, nodes in pairs(match) do
for _, node in ipairs(nodes) do
local capture = subtest_query.captures[id]
if capture == "testname" then
local name = vim.treesitter.get_node_text(node, 0)
test_match.name = string.gsub(string.gsub(name, " ", "_"), '"', "")
end
if capture == "parent" then
test_match.node = node
end
end
end
table.insert(test_tree, test_match)
end
table.sort(test_tree, function(a, b)
return is_parent(a.node, b.node)
end)
for _, parent in ipairs(test_tree) do
for _, child in ipairs(test_tree) do
if is_parent(parent.node, child.node) then
child.parent = parent.name
end
end
end
return get_closest_above_cursor(test_tree)
end
local function get_package_name()
local test_dir = vim.fn.fnamemodify(vim.fn.expand("%:.:h"), ":r")
return "./" .. test_dir
end
M.closest_test = function()
local package_name = get_package_name()
local test_case = get_closest_test()
local test_scope
if test_case then
test_scope = "testcase"
else
test_scope = "package"
end
return {
package = package_name,
name = test_case,
scope = test_scope,
}
end
M.get_root_dir = function()
local id, client = next(vim.lsp.buf_get_clients())
if id == nil then
error({ error_msg = "lsp client not attached" })
end
if not client.config.root_dir then
error({ error_msg = "lsp root_dir not defined" })
end
return client.config.root_dir
end
return M

View File

@@ -16,3 +16,7 @@ dap.listeners.before.event_exited.dapui_config = function()
dapui.close()
end
-- Languages
-- Go
require("dap-go").setup()