diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/Makefile b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/Makefile new file mode 100644 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/Makefile @@ -0,0 +1,3 @@ +CXX_SOURCES := main.cpp + +include Makefile.rules diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/TestVSCode_completions.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/TestVSCode_completions.py new file mode 100644 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/TestVSCode_completions.py @@ -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, allocator >, allocator, allocator > > > &", + } + ], + [{"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, allocator >, allocator, allocator > > > &", + } + ], + ) + + 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.", + } + ], + ) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/main.cpp b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/main.cpp new file mode 100644 --- /dev/null +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/completions/main.cpp @@ -0,0 +1,16 @@ +#include +#include + +int fun(std::vector 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 vec; + fun(vec); + return 0; // breakpoint 2 +} diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/lldbvscode_testcase.py @@ -254,7 +254,7 @@ '''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) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/variables/TestVSCode_variables.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/variables/TestVSCode_variables.py --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/variables/TestVSCode_variables.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/variables/TestVSCode_variables.py @@ -85,7 +85,7 @@ 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") diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py @@ -348,6 +348,10 @@ 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 @@ } 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: diff --git a/lldb/tools/lldb-vscode/lldb-vscode.cpp b/lldb/tools/lldb-vscode/lldb-vscode.cpp --- a/lldb/tools/lldb-vscode/lldb-vscode.cpp +++ b/lldb/tools/lldb-vscode/lldb-vscode.cpp @@ -821,6 +821,152 @@ 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 @@ // 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 @@ static std::map g_request_handlers = { // VSCode Debug Adaptor requests REQUEST_CALLBACK(attach), + REQUEST_CALLBACK(completions), REQUEST_CALLBACK(continue), REQUEST_CALLBACK(configurationDone), REQUEST_CALLBACK(disconnect),