[lldb-vscode] support the completion request

Summary:
The DAP has a completion request that has been unimplemented. It allows showing autocompletion tokens inside the Debug Console.
I implemented it in a very simple fashion mimicking what the user would see when autocompleting an expression inside the CLI.
There are two cases: normal variables and commands. The latter occurs when a text is prepepended with ` in the Debug Console.
These two cases work well and have tests.

Reviewers: clayborg, aadsm

Subscribers: lldb-commits

Tags: #lldb

Differential Revision: https://reviews.llvm.org/D69873
This commit is contained in:
Walter Erquinigo 2019-10-30 16:46:06 -07:00
parent 979da9a4c3
commit 2c7c528d7a
7 changed files with 302 additions and 3 deletions

View File

@ -0,0 +1,3 @@
CXX_SOURCES := main.cpp
include Makefile.rules

View File

@ -0,0 +1,117 @@
"""
Test lldb-vscode completions request
"""
from __future__ import print_function
import lldbvscode_testcase
import unittest2
import vscode
from lldbsuite.test import lldbutil
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
class TestVSCode_variables(lldbvscode_testcase.VSCodeTestCaseBase):
mydir = TestBase.compute_mydir(__file__)
def verify_completions(self, actual_list, expected_list, not_expected_list=[]):
for expected_item in expected_list:
self.assertTrue(expected_item in actual_list)
for not_expected_item in not_expected_list:
self.assertFalse(not_expected_item in actual_list)
@skipIfWindows
@skipIfDarwin # Skip this test for now until we can figure out why tings aren't working on build bots
@no_debug_info_test
def test_completions(self):
"""
Tests the completion request at different breakpoints
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
source = "main.cpp"
breakpoint1_line = line_number(source, "// breakpoint 1")
breakpoint2_line = line_number(source, "// breakpoint 2")
breakpoint_ids = self.set_source_breakpoints(
source, [breakpoint1_line, breakpoint2_line]
)
self.continue_to_next_stop()
# shouldn't see variables inside main
self.verify_completions(
self.vscode.get_completions("var"),
[
{
"text": "var",
"label": "var -- vector<basic_string<char, char_traits<char>, allocator<char> >, allocator<basic_string<char, char_traits<char>, allocator<char> > > > &",
}
],
[{"text": "var1", "label": "var1 -- int &"}],
)
# should see global keywords but not variables inside main
self.verify_completions(
self.vscode.get_completions("str"),
[{"text": "struct", "label": "struct"}],
[{"text": "str1", "label": "str1 -- std::string &"}],
)
self.continue_to_next_stop()
# should see variables from main but not from the other function
self.verify_completions(
self.vscode.get_completions("var"),
[
{"text": "var1", "label": "var1 -- int &"},
{"text": "var2", "label": "var2 -- int &"},
],
[
{
"text": "var",
"label": "var -- vector<basic_string<char, char_traits<char>, allocator<char> >, allocator<basic_string<char, char_traits<char>, allocator<char> > > > &",
}
],
)
self.verify_completions(
self.vscode.get_completions("str"),
[
{"text": "struct", "label": "struct"},
{"text": "str1", "label": "str1 -- string &"},
],
)
# should complete arbitrary commands including word starts
self.verify_completions(
self.vscode.get_completions("`log enable "),
[{"text": "gdb-remote", "label": "gdb-remote"}],
)
# should complete expressions with quotes inside
self.verify_completions(
self.vscode.get_completions('`expr " "; typed'),
[{"text": "typedef", "label": "typedef"}],
)
# should complete an incomplete quoted token
self.verify_completions(
self.vscode.get_completions('`setting "se'),
[
{
"text": "set",
"label": "set -- Set the value of the specified debugger setting.",
}
],
)
self.verify_completions(
self.vscode.get_completions("`'comm"),
[
{
"text": "command",
"label": "command -- Commands for managing custom LLDB commands.",
}
],
)

View File

@ -0,0 +1,16 @@
#include <string>
#include <vector>
int fun(std::vector<std::string> var) {
return var.size(); // breakpoint 1
}
int main(int argc, char const *argv[]) {
int var1 = 0;
int var2 = 1;
std::string str1 = "a";
std::string str2 = "b";
std::vector<std::string> vec;
fun(vec);
return 0; // breakpoint 2
}

View File

@ -254,7 +254,7 @@ class VSCodeTestCaseBase(TestBase):
'''Sending launch request to vscode
'''
# Make sure we disconnet and terminate the VSCode debug adaptor,
# Make sure we disconnect and terminate the VSCode debug adapter,
# if we throw an exception during the test case
def cleanup():
self.vscode.request_disconnect(terminateDebuggee=True)

View File

@ -85,7 +85,7 @@ class TestVSCode_variables(lldbvscode_testcase.VSCodeTestCaseBase):
source = 'main.cpp'
breakpoint1_line = line_number(source, '// breakpoint 1')
lines = [breakpoint1_line]
# Set breakoint in the thread function so we can step the threads
# Set breakpoint in the thread function so we can step the threads
breakpoint_ids = self.set_source_breakpoints(source, lines)
self.assertTrue(len(breakpoint_ids) == len(lines),
"expect correct number of breakpoints")

View File

@ -348,6 +348,10 @@ class DebugCommunication(object):
print('invalid response')
return None
def get_completions(self, text):
response = self.request_completions(text)
return response['body']['targets']
def get_scope_variables(self, scope_name, frameIndex=0, threadId=None):
stackFrame = self.get_stackFrame(frameIndex=frameIndex,
threadId=threadId)
@ -715,6 +719,18 @@ class DebugCommunication(object):
}
return self.send_recv(command_dict)
def request_completions(self, text):
args_dict = {
'text': text,
'column': len(text)
}
command_dict = {
'command': 'completions',
'type': 'request',
'arguments': args_dict
}
return self.send_recv(command_dict)
def request_stackTrace(self, threadId=None, startFrame=None, levels=None,
dump=False):
if threadId is None:

View File

@ -821,6 +821,152 @@ void request_exceptionInfo(const llvm::json::Object &request) {
g_vsc.SendJSON(llvm::json::Value(std::move(response)));
}
// "CompletionsRequest": {
// "allOf": [ { "$ref": "#/definitions/Request" }, {
// "type": "object",
// "description": "Returns a list of possible completions for a given caret position and text.\nThe CompletionsRequest may only be called if the 'supportsCompletionsRequest' capability exists and is true.",
// "properties": {
// "command": {
// "type": "string",
// "enum": [ "completions" ]
// },
// "arguments": {
// "$ref": "#/definitions/CompletionsArguments"
// }
// },
// "required": [ "command", "arguments" ]
// }]
// },
// "CompletionsArguments": {
// "type": "object",
// "description": "Arguments for 'completions' request.",
// "properties": {
// "frameId": {
// "type": "integer",
// "description": "Returns completions in the scope of this stack frame. If not specified, the completions are returned for the global scope."
// },
// "text": {
// "type": "string",
// "description": "One or more source lines. Typically this is the text a user has typed into the debug console before he asked for completion."
// },
// "column": {
// "type": "integer",
// "description": "The character position for which to determine the completion proposals."
// },
// "line": {
// "type": "integer",
// "description": "An optional line for which to determine the completion proposals. If missing the first line of the text is assumed."
// }
// },
// "required": [ "text", "column" ]
// },
// "CompletionsResponse": {
// "allOf": [ { "$ref": "#/definitions/Response" }, {
// "type": "object",
// "description": "Response to 'completions' request.",
// "properties": {
// "body": {
// "type": "object",
// "properties": {
// "targets": {
// "type": "array",
// "items": {
// "$ref": "#/definitions/CompletionItem"
// },
// "description": "The possible completions for ."
// }
// },
// "required": [ "targets" ]
// }
// },
// "required": [ "body" ]
// }]
// },
// "CompletionItem": {
// "type": "object",
// "description": "CompletionItems are the suggestions returned from the CompletionsRequest.",
// "properties": {
// "label": {
// "type": "string",
// "description": "The label of this completion item. By default this is also the text that is inserted when selecting this completion."
// },
// "text": {
// "type": "string",
// "description": "If text is not falsy then it is inserted instead of the label."
// },
// "sortText": {
// "type": "string",
// "description": "A string that should be used when comparing this item with other items. When `falsy` the label is used."
// },
// "type": {
// "$ref": "#/definitions/CompletionItemType",
// "description": "The item's type. Typically the client uses this information to render the item in the UI with an icon."
// },
// "start": {
// "type": "integer",
// "description": "This value determines the location (in the CompletionsRequest's 'text' attribute) where the completion text is added.\nIf missing the text is added at the location specified by the CompletionsRequest's 'column' attribute."
// },
// "length": {
// "type": "integer",
// "description": "This value determines how many characters are overwritten by the completion text.\nIf missing the value 0 is assumed which results in the completion text being inserted."
// }
// },
// "required": [ "label" ]
// },
// "CompletionItemType": {
// "type": "string",
// "description": "Some predefined types for the CompletionItem. Please note that not all clients have specific icons for all of them.",
// "enum": [ "method", "function", "constructor", "field", "variable", "class", "interface", "module", "property", "unit", "value", "enum", "keyword", "snippet", "text", "color", "file", "reference", "customcolor" ]
// }
void request_completions(const llvm::json::Object &request) {
llvm::json::Object response;
FillResponse(request, response);
llvm::json::Object body;
auto arguments = request.getObject("arguments");
std::string text = GetString(arguments, "text");
auto original_column = GetSigned(arguments, "column", text.size());
auto actual_column = original_column - 1;
llvm::json::Array targets;
// NOTE: the 'line' argument is not needed, as multiline expressions
// work well already
// TODO: support frameID. Currently
// g_vsc.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions
// is frame-unaware.
if (!text.empty() && text[0] == '`') {
text = text.substr(1);
actual_column--;
} else {
text = "p " + text;
actual_column += 2;
}
lldb::SBStringList matches;
lldb::SBStringList descriptions;
g_vsc.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions(
text.c_str(),
actual_column,
0, -1, matches, descriptions);
size_t count = std::min((uint32_t)50, matches.GetSize());
targets.reserve(count);
for (size_t i = 0; i < count; i++) {
std::string match = matches.GetStringAtIndex(i);
std::string description = descriptions.GetStringAtIndex(i);
llvm::json::Object item;
EmplaceSafeString(item, "text", match);
if (description.empty())
EmplaceSafeString(item, "label", match);
else
EmplaceSafeString(item, "label", match + " -- " + description);
targets.emplace_back(std::move(item));
}
body.try_emplace("targets", std::move(targets));
response.try_emplace("body", std::move(body));
g_vsc.SendJSON(llvm::json::Value(std::move(response)));
}
// "EvaluateRequest": {
// "allOf": [ { "$ref": "#/definitions/Request" }, {
// "type": "object",
@ -1107,7 +1253,7 @@ void request_initialize(const llvm::json::Object &request) {
// The debug adapter supports the stepInTargetsRequest.
body.try_emplace("supportsStepInTargetsRequest", false);
// The debug adapter supports the completionsRequest.
body.try_emplace("supportsCompletionsRequest", false);
body.try_emplace("supportsCompletionsRequest", true);
// The debug adapter supports the modules request.
body.try_emplace("supportsModulesRequest", false);
// The set of additional module information exposed by the debug adapter.
@ -2556,6 +2702,7 @@ const std::map<std::string, RequestCallback> &GetRequestHandlers() {
static std::map<std::string, RequestCallback> g_request_handlers = {
// VSCode Debug Adaptor requests
REQUEST_CALLBACK(attach),
REQUEST_CALLBACK(completions),
REQUEST_CALLBACK(continue),
REQUEST_CALLBACK(configurationDone),
REQUEST_CALLBACK(disconnect),