Add support for sessionless expression interpreters

This commit is contained in:
Mathias Fussenegger 2024-06-02 14:11:31 +02:00
parent 6f79b82299
commit 3be7caf8a5
8 changed files with 160 additions and 31 deletions

View file

@ -268,9 +268,20 @@ M.adapters = {}
---@type table<string, dap.Configuration[]>
M.configurations = {}
---@class dap.Interpreter
---@field evaluate fun(expression: string): dap.InterpreterResult
---@class dap.InterpreterResult : dap.VariableContainer
---@field result string
---@field type? string
local providers = {
---@type table<string, fun(bufnr: integer): dap.Configuration[]>
configs = {},
---@type table<string, dap.Interpreter>
interpreters = {},
}
do
local providers_mt = {
@ -279,6 +290,70 @@ do
end,
}
M.providers = setmetatable(providers, providers_mt)
local lua_interpreter = {}
providers.interpreters.lua = lua_interpreter
---@param key any
---@param value any
---@return dap.Variable
local function to_variable(key, value)
local result = {
name = tostring(key),
value = tostring(value),
type = type(value)
}
if type(value) == "table" then
result.value = result.value .. " size=" .. vim.tbl_count(value)
result.variables = {}
for k, v in pairs(value) do
table.insert(result.variables, to_variable(k, v))
end
end
return result
end
---@param expression string
---@result dap.InterpreterResult
function lua_interpreter.evaluate(expression)
local parser = vim.treesitter.get_string_parser(expression, "lua")
local trees = parser:parse()
local root = trees[1]:root() -- root is likely chunk
local child = root:child(root:child_count() - 1)
if child and child:type() ~= "return_statement" then
local slnum, scol, _, _ = child:range()
local lines = vim.split(expression, "\n", { plain = true })
local line = lines[slnum + 1]
lines[slnum + 1] = line:sub(1, scol) .. "return " .. line:sub(scol)
expression = table.concat(lines, "\n")
end
local fn, err = loadstring(expression)
local result
if fn then
local env = getfenv(fn)
local newenv = {}
function newenv.print(...)
local line = table.concat({...})
M.repl.append(line .. "\n", nil, { newline = false })
end
setmetatable(newenv, {__index = env})
setfenv(fn, newenv)
result = fn() or "nil"
if type(result) == "table" then
local tbl = result
result = tostring(result) .. " size=" .. tostring(vim.tbl_count(tbl))
local variables = {}
for k, v in pairs(tbl) do
table.insert(variables, to_variable(k, v))
end
return {result = result, variables = variables}
end
end
return {
result = result and tostring(result) or ("Error: " .. err),
variables = {}
}
end
end

View file

@ -6,12 +6,14 @@ local variable = {}
M.variable = variable
local syntax_mapping = {
boolean = 'Boolean',
String = 'String',
int = 'Number',
long = 'Number',
double = 'Float',
float = 'Float',
boolean = "Boolean",
String = "String",
int = "Number",
long = "Number",
number = "Number",
double = "Float",
float = "Float",
["function"] = "Function",
}
@ -35,7 +37,7 @@ function variable.render_parent(var)
if var.name then
return variable.render_child(var --[[@as dap.Variable]], 0)
end
local syntax_group = var.type and syntax_mapping[var.type]
local syntax_group = var.type and syntax_mapping[var.type:lower()]
if syntax_group then
return var.result, {{syntax_group, 0, -1},}
end
@ -62,7 +64,7 @@ function variable.has_children(var)
return (var.variables and #var.variables > 0) or var.variablesReference ~= 0
end
---@param var dap.Variable|dap.Scope
---@param var dap.VariableContainer
---@result dap.Variable[]
function variable.get_children(var)
return var.variables or {}
@ -82,7 +84,7 @@ local function cmp_vars(a, b)
end
---@param var dap.Variable|dap.Scope
---@param var dap.VariableContainer
---@param cb fun(variables: dap.Variable[])
function variable.fetch_children(var, cb)
local session = require('dap').session()

View file

@ -33,10 +33,10 @@ function M.check()
health.info("Adapter is a function. Can't validate it")
else
if adapter.type == "executable" then
adapter = adapter --[[@as ExecutableAdapter]]
adapter = adapter --[[@as dap.ExecutableAdapter]]
check_executable(adapter.command)
elseif adapter.type == "server" then
adapter = adapter --[[@as ServerAdapter]]
adapter = adapter --[[@as dap.ServerAdapter]]
if not adapter.port then
health.error("Missing required `port` property")
end
@ -44,7 +44,7 @@ function M.check()
check_executable(adapter.executable.command)
end
elseif adapter.type == "pipe" then
adapter = adapter --[[@as PipeAdapter]]
adapter = adapter --[[@as dap.PipeAdapter]]
if not adapter.pipe then
health.error("Missing required `pipe` property")
end

View file

@ -98,18 +98,23 @@
---@class dap.VariableResponse
---@field variables dap.Variable[]
---@class dap.Variable
---@class dap.VariableContainer
---@field variablesReference integer if > 0 the variable is structured
---@field variables? dap.Variable[] resolved variablesReference. Not part of the spec; added by nvim-dap
---@field parent? dap.VariableContainer injected by nvim-dap
---@class dap.Variable : dap.VariableContainer
---@field name string
---@field value string
---@field type? string
---@field presentationHint? dap.VariablePresentationHint
---@field evaluateName? string
---@field variablesReference number if > 0 the variable is structured
---@field namedVariables? number
---@field indexedVariables? number
---@field memoryReference? string
---@field variables? dap.Variable[] resolved variablesReference. Not part of the spec; added by nvim-dap
---@field parent? dap.Variable|dap.Scope injected by nvim-dap
---@class dap.EvaluateArguments
---@field expression string
@ -117,11 +122,11 @@
---@field context? "watch"|"repl"|"hover"|"clipboard"|"variables"|string
---@field format? dap.ValueFormat
---@class dap.EvaluateResponse
---@class dap.EvaluateResponse : dap.VariableContainer
---@field result string
---@field type? string
---@field presentationHint? dap.VariablePresentationHint
---@field variablesReference number
---@field namedVariables? number
---@field indexedVariables? number
---@field memoryReference? string

View file

@ -152,6 +152,8 @@ local function print_commands()
end
---@param err dap.ErrorResponse?
---@param resp dap.EvaluateResponse|dap.InterpreterResult|nil
local function evaluate_handler(err, resp)
if err then
local message = utils.fmt_error(err)
@ -160,9 +162,15 @@ local function evaluate_handler(err, resp)
end
return
end
if not resp then
M.append("No response", nil, { newline = false })
return
end
local layer = ui.layer(repl.buf)
local attributes = (resp.presentationHint or {}).attributes or {}
if resp.variablesReference > 0 or vim.tbl_contains(attributes, 'rawString') then
local var_ref = resp.variablesReference or 0
local vars = resp.variables
if (vars and next(vars)) or var_ref > 0 or vim.tbl_contains(attributes, 'rawString') then
local spec = require('dap.entity').variable.tree_spec
local tree = ui.new_tree(spec)
-- tree.render would "append" twice, once for the top element and once for the children
@ -217,7 +225,9 @@ local function print_threads(threads)
end
function execute(text)
---@param opts dap.repl.execute.opts?
function execute(text, opts)
opts = opts or {}
if text == '' then
if history.last then
text = history.last
@ -252,15 +262,16 @@ function execute(text)
end
return
end
if not session then
M.append('No active debug session')
return
end
if vim.tbl_contains(M.commands.continue, text) then
require('dap').continue()
elseif vim.tbl_contains(M.commands.next_, text) then
require('dap').step_over()
elseif vim.tbl_contains(M.commands.capabilities, text) then
if not session then
M.append('No active debug session')
return
end
M.append(vim.inspect(session.capabilities))
elseif vim.tbl_contains(M.commands.into, text) then
require('dap').step_into()
@ -269,24 +280,48 @@ function execute(text)
elseif vim.tbl_contains(M.commands.out, text) then
require('dap').step_out()
elseif vim.tbl_contains(M.commands.up, text) then
if not session then
M.append('No active debug session')
return
end
session:_frame_delta(1)
M.print_stackframes()
elseif vim.tbl_contains(M.commands.step_back, text) then
require('dap').step_back()
elseif vim.tbl_contains(M.commands.pause, text) then
if not session then
M.append('No active debug session')
return
end
session:_pause()
elseif vim.tbl_contains(M.commands.reverse_continue, text) then
require('dap').reverse_continue()
elseif vim.tbl_contains(M.commands.down, text) then
if not session then
M.append('No active debug session')
return
end
session:_frame_delta(-1)
M.print_stackframes()
elseif vim.tbl_contains(M.commands.goto_, splitted_text[1]) then
if not session then
M.append('No active debug session')
return
end
if splitted_text[2] then
session:_goto(tonumber(splitted_text[2]))
end
elseif vim.tbl_contains(M.commands.scopes, text) then
if not session then
M.append('No active debug session')
return
end
print_scopes(session.current_frame)
elseif vim.tbl_contains(M.commands.threads, text) then
if not session then
M.append('No active debug session')
return
end
print_threads(vim.tbl_values(session.threads))
elseif vim.tbl_contains(M.commands.frames, text) then
M.print_stackframes()
@ -294,14 +329,26 @@ function execute(text)
local command = splitted_text[1]
local args = string.sub(text, string.len(command)+2)
M.commands.custom_commands[command](args)
else
elseif session then
session:evaluate(text, evaluate_handler)
else
local dap = require("dap")
local interpreter = dap.providers.interpreters[opts.filetype or "lua"]
if interpreter then
local result = interpreter.evaluate(text)
evaluate_handler(nil, result)
end
end
end
---@class dap.repl.execute.opts
---@field filetype string?
--- Add and execute text as if entered directly
function M.execute(text)
---@param opts dap.repl.execute.opts?
function M.execute(text, opts)
M.append("dap> " .. text .. "\n", "$", { newline = true })
local numlines = api.nvim_buf_line_count(repl.buf)
if repl.win and api.nvim_win_is_valid(repl.win) then
@ -310,7 +357,7 @@ function M.execute(text)
vim.cmd.normal({"zt", bang = true })
end)
end
execute(text)
execute(text, opts)
end

View file

@ -535,7 +535,7 @@ end
local function jump_to_frame(session, frame, preserve_focus_hint, stopped)
local source = frame.source
if not source then
utils.notify('Source not available, cannot jump to frame', vim.log.levels.INFO)
utils.notify('Source missing, cannot jump to frame: ' .. frame.name, vim.log.levels.INFO)
return
end
vim.fn.sign_unplace(session.sign_group)
@ -544,7 +544,7 @@ local function jump_to_frame(session, frame, preserve_focus_hint, stopped)
end
local bufnr = frame_to_bufnr(session, frame)
if not bufnr then
utils.notify('Source not available, cannot jump to frame', vim.log.levels.INFO)
utils.notify('Source missing, cannot jump to frame: ' .. frame.name, vim.log.levels.INFO)
return
end
vim.fn.bufload(bufnr)

View file

@ -652,7 +652,7 @@ function M.sidebar(widget, winopts, wincmd)
end
---@param session Session
---@param session dap.Session
---@param expr string
---@param max_level integer
local function get_var_lines(session, expr, max_level)

View file

@ -154,7 +154,7 @@ if api.nvim_create_autocmd then
vim.bo[args.buf].modified = false
local repl = require("dap.repl")
local lines = api.nvim_buf_get_lines(args.buf, 0, -1, true)
repl.execute(table.concat(lines, "\n"))
repl.execute(table.concat(lines, "\n"), { filetype = ft })
repl.open()
end,
})