Skip to content

Commit fe49f81

Browse files
authored
feat(watches): setExpression and setVariable requests (#48)
* feat(watches): `setExpression` request * fix(watches): no need to worry about duplicates In fact, allowing duplicates is nice to reevaluate an expression, in some circumstances * refactor(watches): in fact, inline function * fix(watches): set default value properly * fix: type annotation * fix(watches): early return and check type to avoid err * refactor(watches): it's big brain time (expensively?) reevaluate all expressions after setting the value of any expression, to guarantee up to date values * refactor: minor cleanup * refactor: I didn't even realize this function became useless * refactor: earlier refactor broke code * refactor: no longer using alternative types * feat(watches): `setVariable` request * bump docs * feat: `o` to open menu in scopes view
1 parent eea1ec3 commit fe49f81

File tree

8 files changed

+171
-26
lines changed

8 files changed

+171
-26
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ The plugin provides 6 "views" that share the same window (so there's clutter)
8181
- Shows a list of user defined expressions, that are evaluated by the debug adapter
8282
- Add, edit and delete expressions from the watch list
8383
- Add variable under the cursor using a command
84+
- Set the value of an expression or variable (if supported by debug adapter)
8485
- Copy the value of an expression or variable
8586

8687
![watches view](https://github.com/user-attachments/assets/381a5c9c-7eea-4cdc-8358-a2afe9f247b2)
@@ -209,11 +210,12 @@ in the `'winbar'` (e.g., `B` for the breakpoints view).
209210

210211
The breakpoints view, the exceptions view and the scopes view only have 1
211212
mapping: `<CR>`. It jumps to a breakpoint, toggles an exception filter, and
212-
expands a variable, respectively. The watches view comes with 4 mappings:
213+
expands a variable, respectively. The watches view comes with 5 mappings:
213214

214215
- `i` to insert a new expression
215216
- `e` to edit an expression
216217
- `c` to copy an expression or variable
218+
- `s` to change the value of an expression or variable
217219
- `d` to delete an expression
218220

219221
Though, the preferred way of adding a new expression is using the

lua/dap-view/events.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ dap.listeners.after.event_stopped[SUBSCRIPTION_ID] = function()
8888
winbar.redraw_controls()
8989
end
9090

91+
dap.listeners.after.setExpression[SUBSCRIPTION_ID] = function()
92+
eval.reeval()
93+
end
94+
95+
dap.listeners.after.setVariable[SUBSCRIPTION_ID] = function()
96+
eval.reeval()
97+
end
98+
9199
dap.listeners.after.initialize[SUBSCRIPTION_ID] = function(session, _)
92100
state.exceptions_options = vim.iter(session.capabilities.exceptionBreakpointFilters or {})
93101
:map(function(filter)

lua/dap-view/state.lua

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@
1111
---@field term_bufnr? integer
1212
---@field term_winnr? integer
1313
---@field last_active_adapter? string
14+
---@field subtle_frames boolean
1415
---@field current_section? SectionType
1516
---@field exceptions_options ExceptionsOption[]
1617
---@field threads ThreadWithErr[]
1718
---@field threads_err? string
1819
---@field frames_by_line {[number]: dap.StackFrame[]}
19-
---@field expressions_by_line {[integer]: string}
20+
---@field expressions_by_line {[integer]: {name: string, response: dap.EvaluateResponse | string}}
2021
---@field variables_by_reference table<integer, {variable: dap.Variable, updated: boolean}[] | string>
21-
---@field variables_by_line {[integer]: dap.Variable}
22-
---@field subtle_frames boolean
23-
---@field watched_expressions table<string,{response?: (dap.EvaluateResponse | string), updated?: boolean}>
22+
---@field variables_by_line table<integer, {response: dap.Variable, reference: number}>
23+
---@field watched_expressions table<string,{response?: dap.EvaluateResponse | string, updated?: boolean}>
2424
local M = {
2525
exceptions_options = {},
2626
threads = {},

lua/dap-view/views/keymaps.lua

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ M.set_keymaps = function()
1818
end
1919
end, { buffer = state.bufnr })
2020

21+
vim.keymap.set("n", "o", function()
22+
if state.current_section == "scopes" then
23+
require("dap.ui").trigger_actions()
24+
end
25+
end)
26+
2127
vim.keymap.set("n", "i", function()
2228
if state.current_section == "watches" then
2329
vim.ui.input({ prompt = "Expression: " }, function(input)
@@ -42,14 +48,14 @@ M.set_keymaps = function()
4248
if state.current_section == "watches" then
4349
local cursor_line = vim.api.nvim_win_get_cursor(state.winnr)[1]
4450

45-
vim.ui.input(
46-
{ prompt = "Expression: ", default = state.expressions_by_line[cursor_line] },
47-
function(input)
51+
local expression = state.expressions_by_line[cursor_line]
52+
if expression then
53+
vim.ui.input({ prompt = "Expression: ", default = expression.name }, function(input)
4854
if input then
4955
watches_actions.edit_watch_expr(input, cursor_line)
5056
end
51-
end
52-
)
57+
end)
58+
end
5359
end
5460
end, { buffer = state.bufnr })
5561

@@ -61,6 +67,32 @@ M.set_keymaps = function()
6167
end
6268
end, { buffer = state.bufnr, nowait = true })
6369

70+
vim.keymap.set("n", "s", function()
71+
if state.current_section == "watches" then
72+
local cursor_line = vim.api.nvim_win_get_cursor(state.winnr)[1]
73+
74+
local get_default = function()
75+
local expr = state.expressions_by_line[cursor_line]
76+
if expr and type(expr.response) ~= "string" then
77+
return expr.response.result
78+
end
79+
80+
local var = state.variables_by_line[cursor_line]
81+
if var then
82+
return var.response.value
83+
end
84+
85+
return ""
86+
end
87+
88+
vim.ui.input({ prompt = "New value: ", default = get_default() }, function(input)
89+
if input then
90+
watches_actions.set_watch_expr(input, cursor_line)
91+
end
92+
end)
93+
end
94+
end, { buffer = state.bufnr })
95+
6496
vim.keymap.set("n", "t", function()
6597
if state.current_section == "threads" then
6698
state.subtle_frames = not state.subtle_frames

lua/dap-view/watches/actions.lua

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
local state = require("dap-view.state")
22
local guard = require("dap-view.guard")
33
local eval = require("dap-view.watches.eval")
4+
local set = require("dap-view.watches.set")
45

56
local M = {}
67

7-
---@param expr string
8-
local is_expr_valid = function(expr)
9-
-- Avoid duplicate expressions
10-
return #expr > 0 and state.watched_expressions[expr] == nil
11-
end
12-
138
---@param expr string
149
---@return boolean
1510
M.add_watch_expr = function(expr)
16-
if not is_expr_valid(expr) or not guard.expect_session() then
11+
if #expr == 0 or not guard.expect_session() then
1712
return false
1813
end
1914

@@ -26,9 +21,9 @@ end
2621
M.remove_watch_expr = function(line)
2722
local expr = state.expressions_by_line[line]
2823
if expr then
29-
local eval_result = state.watched_expressions[expr].response
24+
local eval_result = state.watched_expressions[expr.name].response
3025

31-
state.watched_expressions[expr] = nil
26+
state.watched_expressions[expr.name] = nil
3227
state.expressions_by_line[line] = nil
3328

3429
-- If the result is a string, it's the error
@@ -47,12 +42,12 @@ end
4742
M.copy_watch_expr = function(line)
4843
local expr = state.expressions_by_line[line]
4944
if expr then
50-
eval.copy_expr(expr)
45+
eval.copy_expr(expr.name)
5146
else
5247
local var = state.variables_by_line[line]
5348
if var then
54-
if var.evaluateName then
55-
eval.copy_expr(var.evaluateName)
49+
if var.response.evaluateName then
50+
eval.copy_expr(var.response.evaluateName)
5651
else
5752
vim.notify("Missing `evaluateName`, can't copy variable")
5853
end
@@ -62,10 +57,59 @@ M.copy_watch_expr = function(line)
6257
end
6358
end
6459

60+
---@param value string
61+
---@param line number
62+
M.set_watch_expr = function(value, line)
63+
if not guard.expect_session() then
64+
return
65+
end
66+
67+
local expr = state.expressions_by_line[line]
68+
if expr then
69+
-- Top level expressions are responses for the `evaluate` request, they have no `evaluateName`
70+
-- Therefore, we can always use `setExpression` if the adapter supports it
71+
set.set_expr(expr.name, value)
72+
else
73+
local var = state.variables_by_line[line]
74+
75+
if var then
76+
-- From the protocol:
77+
--
78+
-- "If a debug adapter implements both `setExpression` and `setVariable`,
79+
-- a client uses `setExpression` if the variable has an evaluateName property."
80+
local session = assert(require("dap").session(), "has active session")
81+
local hasExpression = session.capabilities.supportsSetExpression
82+
local hasVariable = session.capabilities.supportsSetVariable
83+
84+
if hasExpression and hasVariable then
85+
if var.response.evaluateName then
86+
set.set_expr(var.response.evaluateName, value)
87+
else
88+
set.set_var(var.response.name, value, var.reference)
89+
end
90+
elseif hasExpression then
91+
if var.response.evaluateName then
92+
set.set_expr(var.response.evaluateName, value)
93+
else
94+
vim.notify(
95+
"Can't set value for " .. var.response.name .. " because it lacks an `evaluateName`"
96+
)
97+
end
98+
elseif hasVariable then
99+
set.set_var(var.response.name, value, var.reference)
100+
else
101+
vim.notify("Adapter lacks support for both `setExpression` and `setVariable` requests")
102+
end
103+
else
104+
vim.notify("No expression or variable under the under cursor")
105+
end
106+
end
107+
end
108+
65109
---@param expr string
66110
---@param line number
67111
M.edit_watch_expr = function(expr, line)
68-
if not is_expr_valid(expr) or not guard.expect_session() then
112+
if #expr == 0 or not guard.expect_session() then
69113
return
70114
end
71115

lua/dap-view/watches/eval.lua

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local watches = require("dap-view.watches.view")
12
local state = require("dap-view.state")
23

34
local M = {}
@@ -22,7 +23,7 @@ local eval_variables = function(variables_reference, frame_id)
2223
-- Lua's type checking is a lackluster
2324
if type(variables) ~= "string" and type(response) ~= "string" then
2425
for k, var in pairs(response or {}) do
25-
local updated = type(original) == "table" and original[k].variable.value ~= var.value or false
26+
local updated = type(original) == "table" and original[k].variable.value ~= var.value
2627
table.insert(variables, { variable = var, updated = updated })
2728
end
2829
end
@@ -73,4 +74,15 @@ M.copy_expr = function(expr)
7374
end
7475
end
7576

77+
M.reeval = function()
78+
-- Reevaluate expressions which may depend on the changed value
79+
for expr, _ in pairs(state.watched_expressions) do
80+
M.eval_expr(expr)
81+
end
82+
83+
if state.current_section == "watches" then
84+
watches.show()
85+
end
86+
end
87+
7688
return M

lua/dap-view/watches/set.lua

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
local M = {}
2+
3+
---@param expr string
4+
---@param value string
5+
M.set_expr = function(expr, value)
6+
local session = assert(require("dap").session(), "has active session")
7+
8+
if session.capabilities.supportsSetExpression then
9+
coroutine.wrap(function()
10+
local frame_id = session.current_frame and session.current_frame.id
11+
12+
local err, _ =
13+
session:request("setExpression", { expression = expr, value = value, frameId = frame_id })
14+
15+
if err then
16+
vim.notify("Failed to set expression " .. expr .. " to value " .. value)
17+
end
18+
end)()
19+
else
20+
vim.notify("Adapter doesn't support `setExpression` request")
21+
end
22+
end
23+
24+
---@param name string
25+
---@param value string
26+
---@param variables_reference number
27+
M.set_var = function(name, value, variables_reference)
28+
local session = assert(require("dap").session(), "has active session")
29+
30+
if session.capabilities.supportsSetVariable then
31+
coroutine.wrap(function()
32+
local err, _ = session:request(
33+
"setVariable",
34+
{ name = name, value = value, variablesReference = variables_reference }
35+
)
36+
37+
if err then
38+
vim.notify("Failed to set variable " .. name .. " to value " .. value)
39+
end
40+
end)()
41+
else
42+
vim.notify("Adapter doesn't support `setVariable` request")
43+
end
44+
end
45+
46+
return M

lua/dap-view/watches/view.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ local show_variables = function(line, response)
5454

5555
line = line + 1
5656

57-
state.variables_by_line[line] = variable
57+
state.variables_by_line[line] =
58+
{ response = variable, reference = response.variablesReference }
5859
end
5960
end
6061
end
@@ -107,7 +108,7 @@ M.show = function()
107108

108109
line = line + 1
109110

110-
state.expressions_by_line[line] = expr
111+
state.expressions_by_line[line] = { name = expr, response = response }
111112

112113
line = show_variables(line, response)
113114
end

0 commit comments

Comments
 (0)