feat(debugger): golang adapter for dap
This commit is contained in:
27
lua/dap-go/COPYRIGHT
Normal file
27
lua/dap-go/COPYRIGHT
Normal 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
272
lua/dap-go/init.lua
Normal 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
175
lua/dap-go/ts.lua
Normal 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
|
||||
@@ -16,3 +16,7 @@ dap.listeners.before.event_exited.dapui_config = function()
|
||||
dapui.close()
|
||||
end
|
||||
|
||||
-- Languages
|
||||
|
||||
-- Go
|
||||
require("dap-go").setup()
|
||||
|
||||
Reference in New Issue
Block a user