Skip to content

Commit a5876be

Browse files
authored
[lldb-dap] Correct auto-completion based on ReplMode and escape char (#110784)
This commit improves the auto-completion in the Debug Console provided by VS-Code. So far, we were always suggesting completions for both LLDB commands and for variables / expressions, even if the heuristic already determined how the given string will be executed, e.g., because the user explicitly typed the escape prefix. Furthermore, auto-completion after the escape character was broken, since the offsets were not adjusted correctly. With this commit we now correctly take this into account. Even with this commit, auto-completion does not always work reliably: * VS Code only requests auto-completion after typing the first alphabetic character, but not after punctuation characters. This means that no completions are provided after typing "`" * LLDB does not provide autocompletions if a string is an exact match. This means if a user types `l` (which is a valid command), LLDB will not provide "language" and "log" as potential completions. Even worse, VS Code caches the completion and does client-side filtering. Hence, even after typing `la`, no auto-completion for "language" is shown in the UI. Those issues might be fixed in follow-up commits. Also with those known issues, the experience is already much better with this commit. Furthermore, I updated the README since I noticed that it was slightly inaccurate.
1 parent 6a34684 commit a5876be

File tree

6 files changed

+226
-83
lines changed

6 files changed

+226
-83
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ def request_compileUnits(self, moduleId):
10061006
return response
10071007

10081008
def request_completions(self, text, frameId=None):
1009-
args_dict = {"text": text, "column": len(text)}
1009+
args_dict = {"text": text, "column": len(text) + 1}
10101010
if frameId:
10111011
args_dict["frameId"] = frameId
10121012
command_dict = {

lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py

Lines changed: 166 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@
99
from lldbsuite.test.decorators import *
1010
from lldbsuite.test.lldbtest import *
1111

12+
session_completion = {
13+
"text": "session",
14+
"label": "session -- Commands controlling LLDB session.",
15+
}
16+
settings_completion = {
17+
"text": "settings",
18+
"label": "settings -- Commands for managing LLDB settings.",
19+
}
20+
memory_completion = {
21+
"text": "memory",
22+
"label": "memory -- Commands for operating on memory in the current target process.",
23+
}
24+
command_var_completion = {
25+
"text": "var",
26+
"label": "var -- Show variables for the current stack frame. Defaults to all arguments and local variables in scope. Names of argument, local, file static and file global variables can be specified.",
27+
}
28+
variable_var_completion = {
29+
"text": "var",
30+
"label": "var -- vector<baz> &",
31+
}
32+
variable_var1_completion = {"text": "var1", "label": "var1 -- int &"}
33+
variable_var2_completion = {"text": "var2", "label": "var2 -- int &"}
1234

1335
class TestDAP_completions(lldbdap_testcase.DAPTestCaseBase):
1436
def verify_completions(self, actual_list, expected_list, not_expected_list=[]):
@@ -18,12 +40,8 @@ def verify_completions(self, actual_list, expected_list, not_expected_list=[]):
1840
for not_expected_item in not_expected_list:
1941
self.assertNotIn(not_expected_item, actual_list)
2042

21-
@skipIfWindows
22-
@skipIf(compiler="clang", compiler_version=["<", "17.0"])
23-
def test_completions(self):
24-
"""
25-
Tests the completion request at different breakpoints
26-
"""
43+
44+
def setup_debugee(self):
2745
program = self.getBuildArtifact("a.out")
2846
self.build_and_launch(program)
2947

@@ -32,90 +50,146 @@ def test_completions(self):
3250
breakpoint2_line = line_number(source, "// breakpoint 2")
3351

3452
self.set_source_breakpoints(source, [breakpoint1_line, breakpoint2_line])
53+
54+
def test_command_completions(self):
55+
"""
56+
Tests completion requests for lldb commands, within "repl-mode=command"
57+
"""
58+
self.setup_debugee()
3559
self.continue_to_next_stop()
3660

37-
# shouldn't see variables inside main
61+
res = self.dap_server.request_evaluate(
62+
"`lldb-dap repl-mode command", context="repl"
63+
)
64+
self.assertTrue(res["success"])
65+
66+
# Provides completion for top-level commands
3867
self.verify_completions(
39-
self.dap_server.get_completions("var"),
68+
self.dap_server.get_completions("se"),
69+
[session_completion, settings_completion],
70+
)
71+
72+
# Provides completions for sub-commands
73+
self.verify_completions(
74+
self.dap_server.get_completions("memory "),
4075
[
4176
{
42-
"text": "var",
43-
"label": "var -- vector<baz> &",
77+
"text": "read",
78+
"label": "read -- Read from the memory of the current target process.",
4479
},
4580
{
46-
"text": "var",
47-
"label": "var -- Show variables for the current stack frame. Defaults to all arguments and local variables in scope. Names of argument, local, file static and file global variables can be specified.",
81+
"text": "region",
82+
"label": "region -- Get information on the memory region containing an address in the current target process.",
4883
},
4984
],
50-
[
51-
{"text": "var1", "label": "var1 -- int &"},
52-
],
5385
)
5486

55-
# should see global keywords but not variables inside main
87+
# Provides completions for parameter values of commands
5688
self.verify_completions(
57-
self.dap_server.get_completions("str"),
58-
[{"text": "struct", "label": "struct"}],
59-
[{"text": "str1", "label": "str1 -- std::string &"}],
89+
self.dap_server.get_completions("`log enable "),
90+
[{"text": "gdb-remote", "label": "gdb-remote"}],
6091
)
6192

62-
self.continue_to_next_stop()
93+
# Also works if the escape prefix is used
94+
self.verify_completions(
95+
self.dap_server.get_completions("`mem"), [memory_completion]
96+
)
6397

64-
# should see variables from main but not from the other function
6598
self.verify_completions(
66-
self.dap_server.get_completions("var"),
67-
[
68-
{"text": "var1", "label": "var1 -- int &"},
69-
{"text": "var2", "label": "var2 -- int &"},
70-
],
99+
self.dap_server.get_completions("`"),
100+
[session_completion, settings_completion, memory_completion],
101+
)
102+
103+
# Completes an incomplete quoted token
104+
self.verify_completions(
105+
self.dap_server.get_completions('setting "se'),
71106
[
72107
{
73-
"text": "var",
74-
"label": "var -- vector<baz> &",
108+
"text": "set",
109+
"label": "set -- Set the value of the specified debugger setting.",
75110
}
76111
],
77112
)
78113

114+
# Completes an incomplete quoted token
79115
self.verify_completions(
80-
self.dap_server.get_completions("str"),
81-
[
82-
{"text": "struct", "label": "struct"},
83-
{"text": "str1", "label": "str1 -- string &"},
84-
],
116+
self.dap_server.get_completions("'mem"),
117+
[memory_completion],
85118
)
86119

87-
# should complete arbitrary commands including word starts
120+
# Completes expressions with quotes inside
88121
self.verify_completions(
89-
self.dap_server.get_completions("`log enable "),
90-
[{"text": "gdb-remote", "label": "gdb-remote"}],
122+
self.dap_server.get_completions('expr " "; typed'),
123+
[{"text": "typedef", "label": "typedef"}],
91124
)
92125

93-
# should complete expressions with quotes inside
126+
# Provides completions for commands, but not variables
94127
self.verify_completions(
95-
self.dap_server.get_completions('`expr " "; typed'),
96-
[{"text": "typedef", "label": "typedef"}],
128+
self.dap_server.get_completions("var"),
129+
[command_var_completion],
130+
[variable_var_completion],
131+
)
132+
133+
def test_variable_completions(self):
134+
"""
135+
Tests completion requests in "repl-mode=variable"
136+
"""
137+
self.setup_debugee()
138+
self.continue_to_next_stop()
139+
140+
res = self.dap_server.request_evaluate(
141+
"`lldb-dap repl-mode variable", context="repl"
142+
)
143+
self.assertTrue(res["success"])
144+
145+
# Provides completions for varibles, but not command
146+
self.verify_completions(
147+
self.dap_server.get_completions("var"),
148+
[variable_var_completion],
149+
[command_var_completion],
97150
)
98151

99-
# should complete an incomplete quoted token
152+
# We stopped inside `fun`, so we shouldn't see variables from main
100153
self.verify_completions(
101-
self.dap_server.get_completions('`setting "se'),
154+
self.dap_server.get_completions("var"),
155+
[variable_var_completion],
102156
[
103-
{
104-
"text": "set",
105-
"label": "set -- Set the value of the specified debugger setting.",
106-
}
157+
variable_var1_completion,
158+
variable_var2_completion,
107159
],
108160
)
161+
162+
# We should see global keywords but not variables inside main
109163
self.verify_completions(
110-
self.dap_server.get_completions("`'comm"),
164+
self.dap_server.get_completions("str"),
165+
[{"text": "struct", "label": "struct"}],
166+
[{"text": "str1", "label": "str1 -- std::string &"}],
167+
)
168+
169+
self.continue_to_next_stop()
170+
171+
# We stopped in `main`, so we should see variables from main but
172+
# not from the other function
173+
self.verify_completions(
174+
self.dap_server.get_completions("var"),
111175
[
112-
{
113-
"text": "command",
114-
"label": "command -- Commands for managing custom LLDB commands.",
115-
}
176+
variable_var1_completion,
177+
variable_var2_completion,
178+
],
179+
[
180+
variable_var_completion,
181+
],
182+
)
183+
184+
self.verify_completions(
185+
self.dap_server.get_completions("str"),
186+
[
187+
{"text": "struct", "label": "struct"},
188+
{"text": "str1", "label": "str1 -- string &"},
116189
],
117190
)
118191

192+
# Completion also works for more complex expressions
119193
self.verify_completions(
120194
self.dap_server.get_completions("foo1.v"),
121195
[{"text": "var1", "label": "foo1.var1 -- int"}],
@@ -148,3 +222,45 @@ def test_completions(self):
148222
[{"text": "var1", "label": "var1 -- int"}],
149223
[{"text": "var2", "label": "var2 -- int"}],
150224
)
225+
226+
# Even in variable mode, we can still use the escape prefix
227+
self.verify_completions(
228+
self.dap_server.get_completions("`mem"), [memory_completion]
229+
)
230+
231+
def test_auto_completions(self):
232+
"""
233+
Tests completion requests in "repl-mode=auto"
234+
"""
235+
self.setup_debugee()
236+
237+
res = self.dap_server.request_evaluate(
238+
"`lldb-dap repl-mode auto", context="repl"
239+
)
240+
self.assertTrue(res["success"])
241+
242+
self.continue_to_next_stop()
243+
self.continue_to_next_stop()
244+
245+
# We are stopped inside `main`. Variables `var1` and `var2` are in scope.
246+
# Make sure, we offer all completions
247+
self.verify_completions(
248+
self.dap_server.get_completions("va"),
249+
[
250+
command_var_completion,
251+
variable_var1_completion,
252+
variable_var2_completion,
253+
],
254+
)
255+
256+
# If we are using the escape prefix, only commands are suggested, but no variables
257+
self.verify_completions(
258+
self.dap_server.get_completions("`va"),
259+
[
260+
command_var_completion,
261+
],
262+
[
263+
variable_var1_completion,
264+
variable_var2_completion,
265+
],
266+
)

lldb/tools/lldb-dap/DAP.cpp

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -483,20 +483,20 @@ llvm::json::Value DAP::CreateTopLevelScopes() {
483483
return llvm::json::Value(std::move(scopes));
484484
}
485485

486-
ExpressionContext DAP::DetectExpressionContext(lldb::SBFrame frame,
487-
std::string &expression) {
488-
// Include the escape hatch prefix.
486+
ReplMode DAP::DetectReplMode(lldb::SBFrame frame, std::string &expression,
487+
bool partial_expression) {
488+
// Check for the escape hatch prefix.
489489
if (!expression.empty() &&
490490
llvm::StringRef(expression).starts_with(g_dap.command_escape_prefix)) {
491491
expression = expression.substr(g_dap.command_escape_prefix.size());
492-
return ExpressionContext::Command;
492+
return ReplMode::Command;
493493
}
494494

495495
switch (repl_mode) {
496496
case ReplMode::Variable:
497-
return ExpressionContext::Variable;
497+
return ReplMode::Variable;
498498
case ReplMode::Command:
499-
return ExpressionContext::Command;
499+
return ReplMode::Command;
500500
case ReplMode::Auto:
501501
// To determine if the expression is a command or not, check if the first
502502
// term is a variable or command. If it's a variable in scope we will prefer
@@ -509,6 +509,12 @@ ExpressionContext DAP::DetectExpressionContext(lldb::SBFrame frame,
509509
// int var and expression "va" > command
510510
std::pair<llvm::StringRef, llvm::StringRef> token =
511511
llvm::getToken(expression);
512+
513+
// If the first token is not fully finished yet, we can't
514+
// determine whether this will be a variable or a lldb command.
515+
if (partial_expression && token.second.empty())
516+
return ReplMode::Auto;
517+
512518
std::string term = token.first.str();
513519
lldb::SBCommandInterpreter interpreter = debugger.GetCommandInterpreter();
514520
bool term_is_command = interpreter.CommandExists(term.c_str()) ||
@@ -527,9 +533,9 @@ ExpressionContext DAP::DetectExpressionContext(lldb::SBFrame frame,
527533

528534
// Variables take preference to commands in auto, since commands can always
529535
// be called using the command_escape_prefix
530-
return term_is_variable ? ExpressionContext::Variable
531-
: term_is_command ? ExpressionContext::Command
532-
: ExpressionContext::Variable;
536+
return term_is_variable ? ReplMode::Variable
537+
: term_is_command ? ReplMode::Command
538+
: ReplMode::Variable;
533539
}
534540

535541
llvm_unreachable("enum cases exhausted.");

lldb/tools/lldb-dap/DAP.h

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,6 @@ enum class PacketStatus {
9494

9595
enum class ReplMode { Variable = 0, Command, Auto };
9696

97-
/// The detected context of an expression based off the current repl mode.
98-
enum class ExpressionContext {
99-
Variable = 0,
100-
Command,
101-
};
102-
10397
struct Variables {
10498
/// Variable_reference start index of permanent expandable variable.
10599
static constexpr int64_t PermanentVariableStartIndex = (1ll << 32);
@@ -245,12 +239,24 @@ struct DAP {
245239

246240
void PopulateExceptionBreakpoints();
247241

248-
/// \return
249-
/// Attempt to determine if an expression is a variable expression or
250-
/// lldb command using a hueristic based on the first term of the
251-
/// expression.
252-
ExpressionContext DetectExpressionContext(lldb::SBFrame frame,
253-
std::string &expression);
242+
/// Attempt to determine if an expression is a variable expression or
243+
/// lldb command using a heuristic based on the first term of the
244+
/// expression.
245+
///
246+
/// \param[in] frame
247+
/// The frame, used as context to detect local variable names
248+
/// \param[inout] expression
249+
/// The expression string. Might be modified by this function to
250+
/// remove the leading escape character.
251+
/// \param[in] partial_expression
252+
/// Whether the provided `expression` is only a prefix of the
253+
/// final expression. If `true`, this function might return
254+
/// `ReplMode::Auto` to indicate that the expression could be
255+
/// either an expression or a statement, depending on the rest of
256+
/// the expression.
257+
/// \return the expression mode
258+
ReplMode DetectReplMode(lldb::SBFrame frame, std::string &expression,
259+
bool partial_expression);
254260

255261
/// \return
256262
/// \b false if a fatal error was found while executing these commands,

0 commit comments

Comments
 (0)