local bind = require('go.keybind') local utils = require('go.utils') local log = utils.log local sep = '.' .. utils.sep() local getopt = require('go.alt_getopt') local dapui_setuped local keys = {} local long_opts = { compile = 'c', run = 'r', attach = 'a', test = 't', restart = 'R', stop = 's', help = 'h', nearest = 'n', package = 'p', file = 'f', breakpoint = 'b', tag = 'T', } local opts = 'tcraRsnpfsbhT:' local function help() return 'Usage: GoDebug [OPTION]\n' .. 'Options:\n' .. ' -c, --compile compile\n' .. ' -r, --run run\n' .. ' -t, --test run tests\n' .. ' -R, --restart restart\n' .. ' -s, --stop stop\n' .. ' -h, --help display this help and exit\n' .. ' -n, --nearest debug nearest file\n' .. ' -p, --package debug package\n' .. ' -f, --file display file\n' .. ' -b, --breakpoint set breakpoint' end -- not sure if anyone still use telescope for debug local function setup_telescope() require('telescope').setup() require('telescope').load_extension('dap') local ts_keys = { ['n|lb'] = 'lua require"telescope".extensions.dap.list_breakpoints{}', ['n|tv'] = 'lua require"telescope".extensions.dap.variables{}', ['n|bt'] = 'lua require"telescope".extensions.dap.frames{}', } bind.nvim_load_mapping(ts_keys) end local keymaps_backup local function keybind() if not _GO_NVIM_CFG.dap_debug_keymap then return end -- TODO: put keymaps back keymaps_backup = vim.api.nvim_get_keymap('n') keys = { -- DAP -- -- run ['r'] = { f = require('go.dap').run, desc = 'run' }, ['c'] = { f = require('dap').continue, desc = 'continue' }, ['n'] = { f = require('dap').step_over, desc = 'step_over' }, ['s'] = { f = require('dap').step_into, desc = 'step_into' }, ['o'] = { f = require('dap').step_out, desc = 'step_out' }, ['S'] = { f = function() require('go.dap').stop(true) end, desc = 'stop debug session', }, ['u'] = { f = require('dap').up, desc = 'up' }, ['D'] = { f = require('dap').down, desc = 'down' }, ['C'] = { f = require('dap').run_to_cursor, desc = 'run_to_cursor' }, ['b'] = { f = require('dap').toggle_breakpoint, desc = 'toggle_breakpoint' }, ['P'] = { f = require('dap').pause, desc = 'pause' }, -- } if _GO_NVIM_CFG.dap_debug_gui then keys['p'] = { f = require('dapui').eval, m = { 'n', 'v' }, desc = 'eval' } keys['K'] = { f = require('dapui').float_element, desc = 'float_element' } keys['B'] = { f = function() require('dapui').float_element('breakpoints') end, desc = "float_element('breakpoints')", } keys['R'] = { f = function() require('dapui').float_element('repl') end, desc = "float_element('repl')", } keys['O'] = { f = function() require('dapui').float_element('scopes') end, desc = "float_element('scopes')", } keys['a'] = { f = function() require('dapui').float_element('stacks') end, desc = "float_element('stacks')", } keys['w'] = { f = function() require('dapui').float_element('watches') end, desc = "float_element('watches')", } else keys['p'] = { f = require('dap.ui.widgets').hover, m = { 'n', 'v' }, desc = 'hover' } end bind.nvim_load_mapping(keys) end local function get_test_build_tags() local get_build_tags = require('go.gotest').get_build_tags local tags = get_build_tags({}) if tags then return tags else return '' end end local M = {} function M.debug_keys() local keymap_help = {} local width = 0 local line = '' for key, val in pairs(keys) do -- local m = vim.fn.matchlist(val, [[\v(\p+)\.(\p+\(\p*\))]]) -- match last function e.g.float_element("repl") line = key .. ' -> ' .. val.desc table.insert(keymap_help, line) if #line > width then width = #line end end local guihua = utils.load_plugin('guihua.lua', 'guihua.listview') if guihua then local ListView = require('guihua.listview') return ListView:new({ loc = 'top_center', border = 'rounded', prompt = true, enter = true, rect = { height = #keymap_help, width = width }, data = keymap_help, }) end local close_events = { 'CursorMoved', 'CursorMovedI', 'BufHidden', 'InsertCharPre' } local config = { close_events = close_events, focusable = true, border = 'single' } vim.lsp.util.open_floating_preview(keymap_help, 'lua', config) end M.prepare = function() utils.load_plugin('nvim-dap', 'dap') if _GO_NVIM_CFG.icons ~= false then vim.fn.sign_define('DapBreakpoint', { text = _GO_NVIM_CFG.icons.breakpoint, texthl = '', linehl = '', numhl = '', }) vim.fn.sign_define('DapStopped', { text = _GO_NVIM_CFG.icons.currentpos, texthl = '', linehl = '', numhl = '', }) end local dapui_cfg = _GO_NVIM_CFG.dap_debug_gui if dapui_cfg then utils.load_plugin('nvim-dap-ui', 'dapui') if dapui_setuped ~= true then if dapui_cfg == true then dapui_cfg = {} end require('dapui').setup(dapui_cfg) dapui_setuped = true end end if _GO_NVIM_CFG.dap_debug_vt then if _GO_NVIM_CFG.dap_debug_vt == true then _GO_NVIM_CFG.dap_debug_vt = { enabled_commands = true, all_frames = true } end local vt = utils.load_plugin('nvim-dap-virtual-text') if vt then vt.setup(_GO_NVIM_CFG.dap_debug_vt) else vim.notify('nvim-dap-virtual-text not found', vim.log.levels.INOF) end end end M.breakpt = function() M.prepare() require('dap').toggle_breakpoint() end M.save_brks = function() M.prepare() local bks = require('dap.breakpoints').get() local all_bks = {} if bks and next(bks) then local _, fld = require('go.project').setup() for bufnr, bk in pairs(bks) do local uri = vim.uri_from_bufnr(bufnr) local _bk = {} for _, value in pairs(bk) do table.insert(_bk, { line = value.line }) end all_bks[uri] = _bk end local bkfile = fld .. utils.sep() .. 'breakpoints.lua' local writeStr = 'return ' .. vim.inspect(all_bks) local writeLst = vim.split(writeStr, '\n') vim.fn.writefile(writeLst, bkfile, 'b') end end M.load_brks = function() M.prepare() local _, brkfile = require('go.project').project_existed() if vim.fn.filereadable(brkfile) == 0 then return end local f = assert(loadfile(brkfile)) local brks = f() for uri, brk in pairs(brks) do local bufnr = vim.uri_to_bufnr(uri) if not vim.api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) end for index, lnum in ipairs(brk) do require('dap.breakpoints').set({}, bufnr, lnum.line) end end end M.clear_bks = function() utils.load_plugin('nvim-dap', 'dap') require('dap.breakpoints').clear() M.save_bks() local _, brkfile = require('go.project').project_existed() if vim.fn.filereadable(brkfile) == 0 then return end local f = assert(loadfile(brkfile)) local brks = f() for uri, brk in pairs(brks) do local bufnr = vim.uri_to_bufnr(uri) if not vim.api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) end for _, lnum in ipairs(brk) do require('dap.breakpoints').set({}, bufnr, lnum.line) end end end local function dapui_opened() local lys = require('dapui.windows').layouts or {} local opened = false for _, ly in ipairs(lys) do if ly:is_open() == true then opened = true end end return opened end local stdout, stderr, handle M.run = function(...) local args = { ... } local mode = 'test' local optarg, optind = getopt.get_opts(args, opts, long_opts) log(optarg, optind) if optarg['h'] then return utils.info(help()) end if optarg['c'] then local fpath = vim.fn.expand('%:p:h') local out = vim.fn.systemlist(table.concat({ 'go', 'test', '-v', '-cover', '-covermode=atomic', '-coverprofile=cover.out', '-tags ' .. _GO_NVIM_CFG.build_tags, '-c', fpath, }, ' ')) if #out ~= 0 then utils.info('building ' .. vim.inspect(out)) end return end if optarg['b'] then return require('dap').toggle_breakpoint() end local original_select = vim.ui.select vim.ui.select = _GO_NVIM_CFG.go_select() -- testopts = {"test", "nearest", "file", "stop", "restart"} log('plugin loaded', mode, optarg) if optarg['s'] and (optarg['t'] or optarg['r']) then M.stop(false) elseif optarg['s'] then return M.stop(true) end -- restart if optarg['R'] then M.stop(false) if optarg['t'] then mode = 'test' else mode = M.pre_mode or 'file' end else M.pre_mode = mode end M.prepare() local session = require('dap').session() if session ~= nil and session.initialized == true then if not optarg['R'] then utils.info('debug session already started, press c to continue') return else utils.info('debug session already started, press c to restart and stop the session') end end local run_cur = optarg['r'] -- undocumented mode, smartrun current program in interactive mode -- e.g. edit and run local testfunc if not run_cur then keybind() else M.stop() -- rerun testfunc = require('go.gotest').get_test_func_name() if testfunc and not string.find(testfunc.name, '[T|t]est') then log('no test func found', testfunc.name) testfunc = nil -- no test func avalible, run main end end if _GO_NVIM_CFG.dap_debug_gui and not run_cur then if not dapui_opened() then require('dapui').open() end end local port = _GO_NVIM_CFG.dap_port if _GO_NVIM_CFG.dap_port == -1 then math.randomseed(os.time()) port = 38000 + math.random(1, 1000) _GO_NVIM_CFG.dap_port = port end local dap = require('dap') local con_options = { max_retries = _GO_NVIM_CFG.dap_retries, initialize_timeout_sec = _GO_NVIM_CFG.dap_timeout, } dap.adapters.go = function(callback, config) if config.request == 'attach' and config.mode == 'remote' and config.host then callback({ type = 'server', host = config.host, port = config.port, options = con_options }) return end stdout = vim.loop.new_pipe(false) stderr = vim.loop.new_pipe(false) local pid_or_err port = config.port or port local host = config.host or '127.0.0.1' local addr = string.format('%s:%d', host, port) local function onread(err, data) if err then log(err, data) -- print('ERROR: ', err) vim.notify('dlv exited with code ' + tostring(err), vim.log.levels.WARN) end if not data or data == '' then return end if data:find("couldn't start") then vim.schedule(function() utils.error(data) end) end vim.schedule(function() require('dap.repl').append(data) end) end handle, pid_or_err = vim.loop.spawn('dlv', { stdio = { nil, stdout, stderr }, args = { 'dap', '-l', addr }, initialize_timeout_sec = con_options.initialize_timeout_sec, detached = true, }, function(code) if code ~= 0 then vim.schedule(function() log('Dlv exited', code) vim.notify(string.format('Delve exited with exit code: %d', code), vim.log.levels.WARN) _GO_NVIM_CFG.dap_port = _GO_NVIM_CFG.dap_port + 1 end) end _ = stdout and stdout:close() _ = stderr and stderr:close() _ = handle and handle:close() stdout = nil stderr = nil handle = nil end) assert(handle, 'Error running dlv: ' .. tostring(pid_or_err)) stdout:read_start(onread) stderr:read_start(onread) vim.defer_fn(function() callback({ type = 'server', host = host, port = port, options = con_options }) end, 1000) end log(get_test_build_tags()) local dap_cfg = { type = 'go', name = 'Debug', request = 'launch', dlvToolPath = vim.fn.exepath('dlv'), buildFlags = get_test_build_tags(), options = con_options, } local empty = utils.empty local launch = require('go.launch') local cfg_exist, cfg_file = launch.vs_launch() log(mode, cfg_exist, cfg_file) -- if breakpoint is not set add breakpoint at current pos local pts = require('dap.breakpoints').get() if utils.empty(pts) then vim.notify('no breakpoint set, add breakpoint at current line', vim.log.levels.INFO) require('dap').set_breakpoint() end testfunc = require('go.gotest').get_test_func_name() log(testfunc) if testfunc then if testfunc.name ~= 'main' then optarg['t'] = true end end if optarg['t'] then local tblcase_ns = require('go.gotest').get_testcase_name() log(tblcase_ns) if not tblcase_ns then vim.notify('no test case found', vim.log.levels.DEBUG) end local tbl_name = '' if tblcase_ns and tblcase_ns.name then vim.notify('running test case: ' .. tblcase_ns.name) tbl_name = tblcase_ns.name tbl_name = tbl_name:gsub('"', '') -- remove " tbl_name = tbl_name:gsub(' ', '_') -- remove space tbl_name = tbl_name:gsub('/', '//') tbl_name = tbl_name:gsub('%(', '\\(') tbl_name = tbl_name:gsub('%)', '\\)') tbl_name = '/' .. tbl_name end log(tblcase_ns) dap_cfg.name = dap_cfg.name .. ' test' dap_cfg.mode = 'test' dap_cfg.request = 'launch' dap_cfg.program = sep .. '${relativeFileDirname}' if testfunc then if testfunc.name:lower():find('bench') then dap_cfg.args = { '-test.bench', '^' .. testfunc.name .. '$' } else dap_cfg.args = { '-test.run=^' .. testfunc.name .. '$' .. tbl_name } -- dap_cfg.args = { [[-test.run=^TestTransactionCheckEngine_Check$/should_process]]} end end dap.configurations.go = { dap_cfg } dap.continue() elseif optarg['n'] then local ns = require('go.ts.go').get_func_method_node_at_pos() if empty(ns) then log('ts not not found, debug while file') end dap_cfg.name = dap_cfg.name .. ' test_nearest' dap_cfg.mode = 'test' dap_cfg.request = 'launch' dap_cfg.program = sep .. '${relativeFileDirname}' if not empty(ns) then dap_cfg.args = { '-test.run', '^' .. ns.name } end dap.configurations.go = { dap_cfg } dap.continue() elseif optarg['a'] then dap_cfg.name = dap_cfg.name .. ' attach' dap_cfg.mode = 'local' dap_cfg.request = 'attach' dap_cfg.processId = require('dap.utils').pick_process dap.configurations.go = { dap_cfg } dap.continue() elseif optarg['p'] then dap_cfg.name = dap_cfg.name .. ' package' dap_cfg.mode = 'test' dap_cfg.request = 'launch' dap_cfg.program = sep .. '${fileDirname}' dap.configurations.go = { dap_cfg } dap.continue() elseif run_cur then dap_cfg.name = dap_cfg.name .. ' run current' dap_cfg.request = 'launch' dap_cfg.mode = 'debug' if testfunc then dap_cfg.args = { '-test.run', '^' .. testfunc.name .. '$' } dap_cfg.mode = 'test' end dap_cfg.program = sep .. '${relativeFileDirname}' dap.configurations.go = { dap_cfg } dap.continue() -- dap.run_to_cursor() elseif cfg_exist then log('using launch cfg') launch.load() log(dap.configurations.go) for _, cfg in ipairs(dap.configurations.go) do cfg.dlvToolPath = vim.fn.exepath('dlv') end dap.continue() else -- no args log('debug main') dap_cfg.program = sep .. '${relativeFileDirname}' dap_cfg.args = args dap_cfg.mode = 'debug' dap_cfg.request = 'launch' dap.configurations.go = { dap_cfg } dap.continue() end log(dap_cfg, args, optarg) M.pre_mode = dap_cfg.mode or M.pre_mode vim.ui.select = original_select end local unmap = function() if not _GO_NVIM_CFG.dap_debug_keymap then return end local unmap_keys = { 'r', 'c', 'n', 's', 'o', 'S', 'u', 'D', 'C', 'b', 'P', 'p', 'K', 'B', 'R', 'O', 'a', 'w', } for _, value in pairs(unmap_keys) do local cmd = 'silent! unmap ' .. value vim.cmd(cmd) end vim.cmd([[silent! vunmap p]]) for _, k in pairs(unmap_keys) do for _, v in pairs(keymaps_backup or {}) do if v.lhs == k then local nr = (v.noremap == 1) local sl = (v.slient == 1) local exp = (v.expr == 1) local mode = v.mode local desc = v.desc or 'go-dap' if v.mode == ' ' then mode = { 'n', 'v' } end log(v) vim.keymap.set( mode, v.lhs, v.rhs or v.callback, { noremap = nr, silent = sl, expr = exp, desc = desc } ) -- vim.api.nvim_set_keymap('n', v.lhs, v.rhs, {noremap=nr, silent=sl, expr=exp}) end end end keymaps_backup = {} end M.disconnect_dap = function() local has_dap, dap = pcall(require, 'dap') if has_dap then dap.disconnect() vim.cmd('sleep 100m') -- allow cleanup else vim.notify('dap not found') end end M.stop = function(unm) if unm then unmap() end M.disconnect_dap() local has_dapui, dapui = pcall(require, 'dapui') if has_dapui then if dapui_opened() then log('closing dapui') dapui.close() end end if stdout then stdout:close() stdout = nil end if stderr then stderr:close() stderr = nil end if handle then handle:close() handle = nil end end function M.ultest_post() vim.g.ultest_use_pty = 1 local builders = { ['go#richgo'] = function(cmd) local args = {} for i = 3, #cmd, 1 do local arg = cmd[i] if vim.startswith(arg, '-') then arg = '-test.' .. string.sub(arg, 2) end args[#args + 1] = arg end return { dap = { type = 'go', request = 'launch', mode = 'test', program = sep .. '${relativeFileDirname}', dlvToolPath = vim.fn.exepath('dlv'), args = args, buildFlags = get_test_build_tags(), }, parse_result = function(lines) return lines[#lines] == 'FAIL' and 1 or 0 end, } end, } local ul = utils.load_plugin('vim-ultest', 'ultest') if ul then ul.setup({ builders = builders }) end end return M