diff --git a/doc/dap.txt b/doc/dap.txt index 062fcb9a..8e5a88fe 100644 --- a/doc/dap.txt +++ b/doc/dap.txt @@ -1307,6 +1307,9 @@ For events, the listeners are called with two arguments: 2. The event payload +Both command and event listeners can return a boolean `true` to remove the +registered listener. + ============================================================================== BUILT-IN CLIENT EVENTS *dap-listeners-ext* diff --git a/lua/dap.lua b/lua/dap.lua index 92c0960e..b18f79d7 100644 --- a/lua/dap.lua +++ b/lua/dap.lua @@ -56,66 +56,69 @@ M.repl = setmetatable({}, { end }) +---@alias dap.RequestListener fun(session: dap.Session, err: dap.ErrorResponse?, response: T, args: U, seq: number):boolean? + +---@alias dap.EventListener fun(session: dap.Session, body: T):boolean? ---@class dap.listeners ----@field event_breakpoint table ----@field event_capabilities table ----@field event_continued table ----@field event_exited table ----@field event_initialized table ----@field event_invalidated table ----@field event_loadedSource table ----@field event_memory table ----@field event_module table ----@field event_output table ----@field event_process table ----@field event_progressEnd table ----@field event_progressStart table ----@field event_progressUpdate table ----@field event_stopped table ----@field event_terminated table ----@field event_thread table ----@field attach table ----@field breakpointLocations table ----@field completions table ----@field configurationDone table ----@field continue table ----@field dataBreakpointInfo table ----@field disassemble table ----@field disconnect table ----@field evaluate table ----@field exceptionInfo table ----@field goto table ----@field gotoTargets table ----@field initialize table ----@field launch table ----@field loadedSources table ----@field modules table ----@field next table ----@field pause table ----@field readMemory table ----@field restart table ----@field restartFrame table ----@field reverseContinue table ----@field scopes table ----@field setBreakpoints table ----@field setDataBreakpoints table ----@field setExceptionBreakpoints table ----@field setExpression table ----@field setFunctionBreakpoints table ----@field setInstructionBreakpoints table ----@field setVariable table ----@field source table ----@field stackTrace table ----@field stepBack table ----@field stepIn table ----@field stepInTargets table ----@field stepOut table ----@field terminate table ----@field terminateThreads table ----@field threads table ----@field variables table ----@field writeMemory table +---@field event_breakpoint table> +---@field event_capabilities table> +---@field event_continued table> +---@field event_exited table> +---@field event_initialized table> +---@field event_invalidated table> +---@field event_loadedSource table> +---@field event_memory table> +---@field event_module table> +---@field event_output table> +---@field event_process table> +---@field event_progressEnd table> +---@field event_progressStart table> +---@field event_progressUpdate table> +---@field event_stopped table> +---@field event_terminated table> +---@field event_thread table> +---@field attach table +---@field breakpointLocations table +---@field completions table> +---@field configurationDone table +---@field continue table +---@field dataBreakpointInfo table +---@field disassemble table +---@field disconnect table> +---@field evaluate table> +---@field exceptionInfo table +---@field goto table +---@field gotoTargets table +---@field initialize table +---@field launch table +---@field loadedSources table +---@field modules table +---@field next table +---@field pause table +---@field readMemory table +---@field restart table +---@field restartFrame table +---@field reverseContinue table +---@field scopes table +---@field setBreakpoints table +---@field setDataBreakpoints table +---@field setExceptionBreakpoints table +---@field setExpression table +---@field setFunctionBreakpoints table +---@field setInstructionBreakpoints table +---@field setVariable table +---@field source table +---@field stackTrace table +---@field stepBack table +---@field stepIn table +---@field stepInTargets table +---@field stepOut table +---@field terminate table +---@field terminateThreads table +---@field threads table +---@field variables table> +---@field writeMemory table M.listeners = { @@ -708,7 +711,7 @@ function M.step_into(opts) session:request('stepInTargets', { frameId = session.current_frame.id }, function(err, response) if err then notify( - 'Error on step_into: ' .. lazy.utils.fmt_error(err) .. ' (while requesting stepInTargets)', + 'Error on step_into: ' .. tostring(err) .. ' (while requesting stepInTargets)', vim.log.levels.ERROR ) return @@ -798,7 +801,7 @@ local function terminate(lsession, opts) local timeout_ms = timeout_sec * 1000 lsession:request_with_timeout('terminate', args, timeout_ms, function(err) if err then - log():warn(lazy.utils.fmt_error(err)) + log():warn(tostring(err)) end if not lsession.closed then lsession:close() @@ -928,7 +931,7 @@ function M.restart(config, opts) config = prepare_config(config) lsession:request('restart', config, function(err0, _) if err0 then - notify('Error restarting debug adapter: ' .. lazy.utils.fmt_error(err0), vim.log.levels.ERROR) + notify('Error restarting debug adapter: ' .. tostring(err0), vim.log.levels.ERROR) else notify('Restarted debug adapter', vim.log.levels.INFO) end diff --git a/lua/dap/entity.lua b/lua/dap/entity.lua index 130e011c..ff5a6af6 100644 --- a/lua/dap/entity.lua +++ b/lua/dap/entity.lua @@ -218,7 +218,7 @@ local function set_expression(_, item, _, context) } session:request('setExpression', params, function(err) if err then - utils.notify('Error on setExpression: ' .. err.message, vim.log.levels.WARN) + utils.notify('Error on setExpression: ' .. tostring(err), vim.log.levels.WARN) else session:_request_scopes(session.current_frame) end @@ -354,14 +354,14 @@ function threads_spec.fetch_children(thread, cb) local params = { threadId = thread.id } local err, resp = session:request('stackTrace', params) if err then - utils.notify('Error fetching stackTrace: ' .. utils.fmt_error(err), vim.log.levels.WARN) + utils.notify('Error fetching stackTrace: ' .. tostring(err), vim.log.levels.WARN) else thread.frames = resp.stackFrames end if not is_stopped then local err0 = session:request('continue', params) if err0 then - utils.notify('Error on continue: ' .. utils.fmt_error(err), vim.log.levels.WARN) + utils.notify('Error on continue: ' .. tostring(err0), vim.log.levels.WARN) else thread.stopped = false local progress = require('dap.progress') @@ -409,7 +409,7 @@ function threads_spec.compute_actions(info) thread.stopped = false session:request('continue', { threadId = thread.id }, function(err) if err then - utils.notify('Error on continue: ' .. err.message, vim.log.levels.WARN) + utils.notify('Error on continue: ' .. tostring(err), vim.log.levels.WARN) end context.refresh() end) diff --git a/lua/dap/protocol.lua b/lua/dap/protocol.lua index fa2b2724..59ccac1e 100644 --- a/lua/dap/protocol.lua +++ b/lua/dap/protocol.lua @@ -1,11 +1,9 @@ ---@meta - ---@class dap.ProtocolMessage ---@field seq number ---@field type "request"|"response"|"event"|string - ---@class dap.Request: dap.ProtocolMessage ---@field type "request" ---@field command string @@ -297,6 +295,7 @@ ---@field instructionReference? string ---@field offset? number +---@class dap.InitializedEvent ---@class dap.StoppedEvent ---@field reason "step"|"breakpoint"|"exception"|"pause"|"entry"|"goto"|"function breakpoint"|"data breakpoint"|"instruction breakpoint"|string; diff --git a/lua/dap/repl.lua b/lua/dap/repl.lua index 9c9de1eb..68004c51 100644 --- a/lua/dap/repl.lua +++ b/lua/dap/repl.lua @@ -166,10 +166,7 @@ end local function evaluate_handler(err, resp) if err then - local message = utils.fmt_error(err) - if message then - M.append(message, nil, { newline = false }) - end + M.append(tostring(err), nil, { newline = false }) return end local layer = ui.layer(repl.buf) diff --git a/lua/dap/session.lua b/lua/dap/session.lua index 98f4a8b5..580a080e 100644 --- a/lua/dap/session.lua +++ b/lua/dap/session.lua @@ -14,6 +14,12 @@ local mime_to_filetype = { ['text/javascript'] = 'javascript' } +local err_mt = { + __tostring = function(e) + return utils.fmt_error(e) + end, +} + local ns_pool = {} do @@ -318,7 +324,7 @@ function Session:event_initialized() if self.capabilities.supportsConfigurationDoneRequest then self:request('configurationDone', nil, function(err1, _) if err1 then - utils.notify(utils.fmt_error(err1), vim.log.levels.ERROR) + utils.notify(tostring(err1), vim.log.levels.ERROR) end self.initialized = true end) @@ -347,7 +353,7 @@ function Session:_show_exception_info(thread_id, bufnr, frame) end local err, response = self:request('exceptionInfo', {threadId = thread_id}) if err then - utils.notify('Error getting exception info: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error getting exception info: ' .. tostring(err), vim.log.levels.ERROR) end if not response then return @@ -704,7 +710,7 @@ function Session:event_stopped(stopped) self:update_threads(coresume(co)) local err = coroutine.yield() if err then - utils.notify('Error retrieving threads: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error retrieving threads: ' .. tostring(err), vim.log.levels.ERROR) return end if thread.stopped == false then @@ -767,7 +773,7 @@ function Session:event_stopped(stopped) return end if err then - utils.notify('Error retrieving stack traces: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error retrieving stack traces: ' .. tostring(err), vim.log.levels.ERROR) return end local frames = response.stackFrames --[=[@as dap.StackFrame[]]=] @@ -879,7 +885,7 @@ function Session:_goto(line, source, col) coroutine.wrap(function() local err, response = self:request('gotoTargets', {source = source or frame.source, line = line, col = col}) if err then - utils.notify('Error getting gotoTargets: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error getting gotoTargets: ' .. tostring(err), vim.log.levels.ERROR) return end if not response or not response.targets then @@ -904,7 +910,7 @@ function Session:_goto(line, source, col) if thread then thread.stopped = true end - utils.notify('Error executing goto: ' .. utils.fmt_error(goto_err), vim.log.levels.ERROR) + utils.notify('Error executing goto: ' .. tostring(goto_err), vim.log.levels.ERROR) end end)() end @@ -976,7 +982,7 @@ do ---@param resp dap.SetBreakpointsResponse local function on_response(err1, resp) if err1 then - utils.notify('Error setting breakpoints: ' .. utils.fmt_error(err1), vim.log.levels.ERROR) + utils.notify('Error setting breakpoints: ' .. tostring(err1), vim.log.levels.ERROR) elseif resp then for _, bp in pairs(resp.breakpoints) do breakpoints.set_state(bufnr, bp) @@ -1033,7 +1039,7 @@ function Session:set_exception_breakpoints(filters, exceptionOptions, on_done) { filters = filters, exceptionOptions = exceptionOptions }, function(err, _) if err then - utils.notify('Error setting exception breakpoints: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error setting exception breakpoints: ' .. tostring(err), vim.log.levels.ERROR) end if on_done then on_done() @@ -1042,6 +1048,17 @@ function Session:set_exception_breakpoints(filters, exceptionOptions, on_done) end +---@param listeners table|dap.EventListener> +local function call_listener(listeners, ...) + for key, listener in pairs(listeners) do + local remove = listener(...) + if remove then + listeners[key] = nil + end + end +end + + function Session:handle_body(body) local decoded = assert(json_decode(body), "Debug adapter must send JSON objects") log:debug(self.id, decoded) @@ -1055,45 +1072,36 @@ function Session:handle_body(body) log:error('No callback found. Did the debug adapter send duplicate responses?', decoded) return end + local err = nil + local response = nil if decoded.success then - vim.schedule(function() - for _, c in pairs(listeners.before[decoded.command]) do - c(self, nil, decoded.body, request, decoded.request_seq) - end - callback(nil, decoded.body, decoded.request_seq) - for _, c in pairs(listeners.after[decoded.command]) do - c(self, nil, decoded.body, request, decoded.request_seq) - end - end) + response = decoded.body else - vim.schedule(function() - local err = { message = decoded.message; body = decoded.body; } - for _, c in pairs(listeners.before[decoded.command]) do - c(self, err, nil, request, decoded.request_seq) - end - callback(err, nil, decoded.request_seq) - for _, c in pairs(listeners.after[decoded.command]) do - c(self, err, nil, request, decoded.request_seq) - end - end) + err = { + message = decoded.message, + body = decoded.body, + } + setmetatable(err, err_mt) end + vim.schedule(function() + local before = listeners.before[decoded.command] + call_listener(before, self, err, response, request, decoded.request_seq) + callback(err, decoded.body, decoded.request_seq) + local after = listeners.after[decoded.command] + call_listener(after, self, err, response, request, decoded.request_seq) + end) elseif decoded.event then - local callback = self['event_' .. decoded.event] + local callback_name = "event_" .. decoded.event + local callback = self[callback_name] vim.schedule(function() - local event_handled = false - for _, c in pairs(listeners.before['event_' .. decoded.event]) do - event_handled = true - c(self, decoded.body) - end + local before = listeners.before[callback_name] + call_listener(before, self, decoded.body) if callback then - event_handled = true callback(self, decoded.body) end - for _, c in pairs(listeners.after['event_' .. decoded.event]) do - event_handled = true - c(self, decoded.body) - end - if not event_handled then + local after = listeners.after[callback_name] + call_listener(after, self, decoded.body) + if not callback and not next(before) and not next(after) then log:warn('No event handler for ', decoded) end end) @@ -1628,7 +1636,7 @@ local function pause_thread(session, thread_id, cb) session:request('pause', { threadId = thread_id; }, function(err) if err then - utils.notify('Error pausing: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error pausing: ' .. tostring(err), vim.log.levels.ERROR) else utils.notify('Thread paused ' .. thread_id, vim.log.levels.INFO) local thread = session.threads[thread_id] @@ -1651,7 +1659,7 @@ function Session:_pause(thread_id, cb) if self.dirty.threads then self:update_threads(function(err) if err then - utils.notify('Error requesting threads: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error requesting threads: ' .. tostring(err), vim.log.levels.ERROR) return end self:_pause(nil, cb) @@ -1707,7 +1715,7 @@ function Session:restart_frame() clear_running(self) local err = self:request('restartFrame', { frameId = frame.id }) if err then - utils.notify('Error on restart_frame: ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error on restart_frame: ' .. tostring(err), vim.log.levels.ERROR) end end)() end @@ -1744,7 +1752,7 @@ function Session:_step(step, params) clear_running(self, thread_id) self:request(step, params, function(err) if err then - utils.notify('Error on '.. step .. ': ' .. utils.fmt_error(err), vim.log.levels.ERROR) + utils.notify('Error on '.. step .. ': ' .. tostring(err), vim.log.levels.ERROR) end progress.report('Running') end) @@ -1841,33 +1849,36 @@ end --- Send a request to the debug adapter ----@param command string command to execute ----@param arguments any|nil object containing arguments for the command ----@param callback fun(err: table, result: any)|nil called with the response result. ---- If nil and running within a coroutine the function will yield the result -function Session:request(command, arguments, callback) +--- +---@param command string command name +---@param arguments any? command arguments +---@param on_result fun(err: dap.ErrorResponse?, result: any)? response callback +---@return dap.ErrorResponse? err, any response # (if running in coroutine and on_response is empty) +---@overload fun(self: dap.Session, command: "evaluate", arguments: dap.EvaluateArguments, on_result: fun(err: dap.ErrorResponse?, result: dap.EvaluateResponse?)?):(dap.ErrorResponse?, dap.EvaluateResponse?) +---@overload fun(self: dap.Session, command: "variables", arguments: dap.VariablesArguments, on_result: fun(err: dap.ErrorResponse?, result: dap.VariableResponse?)?):(dap.ErrorResponse?, dap.VariableResponse?) +function Session:request(command, arguments, on_result) local payload = { - seq = self.seq; - type = 'request'; - command = command; - arguments = arguments + seq = self.seq, + type = 'request', + command = command, + arguments = arguments, } log:debug('request', payload) local current_seq = self.seq self.seq = self.seq + 1 - local co - if not callback then - co = coroutine.running() - if co then - callback = coresume(co) + local co, is_main + if not on_result then + co, is_main = coroutine.running() + if co and not is_main then + on_result = coresume(co) else -- Assume missing callback is intentional. -- Prevent error logging in Session:handle_body - callback = function(_, _) + on_result = function(_, _) end end end - self.message_callbacks[current_seq] = callback + self.message_callbacks[current_seq] = on_result self.message_requests[current_seq] = arguments send_payload(self.client, payload) if co then @@ -1893,10 +1904,11 @@ function Session:initialize(config) vim.schedule(repl.clear) local adapter_responded = false + ---@param err0 dap.ErrorResponse? ---@param result dap.Capabilities? local function on_initialize(err0, result) if err0 then - utils.notify('Could not initialize debug adapter: ' .. utils.fmt_error(err0), vim.log.levels.ERROR) + utils.notify('Could not initialize debug adapter: ' .. tostring(err0), vim.log.levels.ERROR) adapter_responded = true return end @@ -1904,7 +1916,7 @@ function Session:initialize(config) self:request(config.request, config, function(err) adapter_responded = true if err then - utils.notify(string.format('Error on %s: %s', config.request, utils.fmt_error(err)), vim.log.levels.ERROR) + utils.notify(string.format('Error on %s: %s', config.request, err), vim.log.levels.ERROR) self:close() end end) @@ -1955,7 +1967,7 @@ function Session:evaluate(args, fn) } end args.frameId = args.frameId or (self.current_frame or {}).id - return self:request('evaluate', args, fn) + return self:request("evaluate", args, fn) end