https://github.com/da-viper updated https://github.com/llvm/llvm-project/pull/177151
>From 7479028caadbe5905883b13dc1bcefdf74aed22b Mon Sep 17 00:00:00 2001 From: Ebuka Ezike <[email protected]> Date: Tue, 20 Jan 2026 16:05:40 +0000 Subject: [PATCH 1/5] [lldb-dap] Fix the completion provided to the DAP client. Previously, completion behavior was inconsistent, sometimes including the partial token or removing existing user text. Since LLDB completions includes the partial token by default, we now strip it before sending to the client. The completion heuristic: 1. Strip the commandEscapePrefix 2. Request completions from the debugger 3. Get the line at cursor position 4. Calculate the length of any partial token 5. Offset each completion by the partial token length In all cases it completion starts from the cursor position. then offsets by `Length` and inserts the completion. Examples (single quotes show whitespace and are not part of the input): ```md | Input | Partial Token | Length | Completion | |------------|---------------|--------|---------------| | m | m | 1 | memory | | `m | m | 1 | memory | | myfoo. | myfoo. | 6 | myfoo.method( | | myfoo.fi | myfoo.fi | 7 | myfoo.field | | myfoo. b | b | 1 | myfoo. bar | | 'memory ' | | 0 | memory read | | memory | memory | 6 | memory | | settings s | s | 1 | settings show | ``` --- .../completions/TestDAP_completions.py | 339 +++++++++++------- .../lldb-dap/Handler/CompletionsHandler.cpp | 21 +- lldb/tools/lldb-dap/Protocol/ProtocolTypes.h | 8 +- 3 files changed, 235 insertions(+), 133 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py index 1792ff9953efe..937904d682bb0 100644 --- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py +++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py @@ -2,47 +2,91 @@ Test lldb-dap completions request """ +# FIXME: remove when LLDB_MINIMUM_PYTHON_VERSION > 3.8 +from __future__ import annotations + +import json +from typing import Optional import lldbdap_testcase -import dap_server -from lldbsuite.test import lldbutil +from dataclasses import dataclass, replace, asdict from lldbsuite.test.decorators import skipIf from lldbsuite.test.lldbtest import line_number -session_completion = { - "text": "session", - "label": "session", - "detail": "Commands controlling LLDB session.", -} -settings_completion = { - "text": "settings", - "label": "settings", - "detail": "Commands for managing LLDB settings.", -} -memory_completion = { - "text": "memory", - "label": "memory", - "detail": "Commands for operating on memory in the current target process.", -} -command_var_completion = { - "text": "var", - "label": "var", - "detail": "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.", -} -variable_var_completion = {"text": "var", "label": "var", "detail": "vector<baz> &"} -variable_var1_completion = {"text": "var1", "label": "var1", "detail": "int &"} -variable_var2_completion = {"text": "var2", "label": "var2", "detail": "int &"} + +@dataclass(frozen=True) +class CompletionItem: + label: str + text: Optional[str] = None + detail: Optional[str] = None + start: Optional[int] = None + length: int = 0 + + def __repr__(self): + # use json as it easier to see the diff on failure. + return json.dumps(asdict(self), indent=4) + + def clone(self, **kwargs) -> CompletionItem: + """Creates a copy of this CompletionItem with specified fields modified.""" + return replace(self, **kwargs) + + +@dataclass +class TestCase: + input: str + expected: set[CompletionItem] + not_expected: Optional[set[CompletionItem]] = None + + +session_completion = CompletionItem( + label="session", + detail="Commands controlling LLDB session.", +) +settings_completion = CompletionItem( + label="settings", + detail="Commands for managing LLDB settings.", +) +memory_completion = CompletionItem( + label="memory", + detail="Commands for operating on memory in the current target process.", +) +command_var_completion = CompletionItem( + label="var", + detail="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.", + length=3, +) +variable_var_completion = CompletionItem(label="var", detail="vector<baz> &", length=3) +variable_var1_completion = CompletionItem(label="var1", detail="int &") +variable_var2_completion = CompletionItem(label="var2", detail="int &") + +str1_completion = CompletionItem( + label="str1", + detail="std::string &", +) # Older version of libcxx produce slightly different typename strings for # templates like vector. @skipIf(compiler="clang", compiler_version=["<", "16.0"]) class TestDAP_completions(lldbdap_testcase.DAPTestCaseBase): - def verify_completions(self, actual_list, expected_list, not_expected_list=[]): - for expected_item in expected_list: - self.assertIn(expected_item, actual_list) - - for not_expected_item in not_expected_list: - self.assertNotIn(not_expected_item, actual_list) + def verify_completions(self, case: TestCase): + completions = { + CompletionItem(**comp) + for comp in self.dap_server.get_completions(case.input) + } + + # handle expected completions + expected_completions = case.expected + for exp_comp in expected_completions: + # with self.subTest(f"Expected completion : {exp_comp}"): + self.assertIn( + exp_comp, completions, f"\nCompletion for input: {case.input}" + ) + + # unexpected completions + not_expected_label = case.not_expected or set() + for not_exp_comp in not_expected_label: + with self.subTest(f"Not expected completion : {not_exp_comp}"): + self.assertNotIn(not_exp_comp, completions) def setup_debuggee(self): program = self.getBuildArtifact("a.out") @@ -70,72 +114,96 @@ def test_command_completions(self): # Provides completion for top-level commands self.verify_completions( - self.dap_server.get_completions("se"), - [session_completion, settings_completion], + TestCase( + input="se", + expected={ + session_completion.clone(length=2), + settings_completion.clone(length=2), + }, + ) ) - # Provides completions for sub-commands self.verify_completions( - self.dap_server.get_completions("memory "), - [ - { - "text": "read", - "label": "read", - "detail": "Read from the memory of the current target process.", - }, - { - "text": "region", - "label": "region", - "detail": "Get information on the memory region containing an address in the current target process.", + TestCase( + input="memory ", + expected={ + CompletionItem( + label="read", + detail="Read from the memory of the current target process.", + ), + CompletionItem( + label="region", + detail="Get information on the memory region containing an address in the current target process.", + ), }, - ], + ) ) # Provides completions for parameter values of commands self.verify_completions( - self.dap_server.get_completions("`log enable "), - [{"text": "gdb-remote", "label": "gdb-remote"}], + TestCase( + input="`log enable ", expected={CompletionItem(label="gdb-remote")} + ) ) # Also works if the escape prefix is used self.verify_completions( - self.dap_server.get_completions("`mem"), [memory_completion] + TestCase(input="`mem", expected={memory_completion.clone(length=3)}) ) self.verify_completions( - self.dap_server.get_completions("`"), - [session_completion, settings_completion, memory_completion], + TestCase( + input="`", + expected={ + session_completion.clone(), + settings_completion.clone(), + memory_completion.clone(), + }, + ) ) # Completes an incomplete quoted token self.verify_completions( - self.dap_server.get_completions('setting "se'), - [ - { - "text": "set", - "label": "set", - "detail": "Set the value of the specified debugger setting.", - } - ], + TestCase( + input='setting "se', + expected={ + CompletionItem( + label="set", + detail="Set the value of the specified debugger setting.", + length=3, + ) + }, + ) ) # Completes an incomplete quoted token self.verify_completions( - self.dap_server.get_completions("'mem"), - [memory_completion], + TestCase(input="'mem", expected={memory_completion.clone(length=4)}) ) # Completes expressions with quotes inside self.verify_completions( - self.dap_server.get_completions('expr " "; typed'), - [{"text": "typedef", "label": "typedef"}], + TestCase( + input='expr " "; typed', + expected={CompletionItem(label="typedef", length=5)}, + ) ) # Provides completions for commands, but not variables self.verify_completions( - self.dap_server.get_completions("var"), - [command_var_completion], - [variable_var_completion], + TestCase( + input="var", + expected={command_var_completion.clone()}, + not_expected={variable_var_completion.clone()}, + ) + ) + + # Completes partial completion + self.verify_completions( + TestCase( + input="plugin list ar", + expected={CompletionItem(label="architecture", length=2)}, + ) ) def test_variable_completions(self): @@ -152,26 +220,32 @@ def test_variable_completions(self): # Provides completions for varibles, but not command self.verify_completions( - self.dap_server.get_completions("var"), - [variable_var_completion], - [command_var_completion], + TestCase( + input="var", + expected={variable_var_completion.clone()}, + not_expected={command_var_completion.clone()}, + ) ) # We stopped inside `fun`, so we shouldn't see variables from main self.verify_completions( - self.dap_server.get_completions("var"), - [variable_var_completion], - [ - variable_var1_completion, - variable_var2_completion, - ], + TestCase( + input="var", + expected={variable_var_completion}, + not_expected={ + variable_var1_completion.clone(length=3), + variable_var2_completion.clone(length=3), + }, + ) ) # We should see global keywords but not variables inside main self.verify_completions( - self.dap_server.get_completions("str"), - [{"text": "struct", "label": "struct"}], - [{"text": "str1", "label": "str1", "detail": "std::string &"}], + TestCase( + input="str", + expected={CompletionItem(label="struct", length=3)}, + not_expected={str1_completion.clone(length=3)}, + ) ) self.continue_to_next_stop() @@ -179,65 +253,86 @@ def test_variable_completions(self): # We stopped in `main`, so we should see variables from main but # not from the other function self.verify_completions( - self.dap_server.get_completions("var"), - [ - variable_var1_completion, - variable_var2_completion, - ], - [ - variable_var_completion, - ], + TestCase( + input="var", + expected={ + variable_var1_completion.clone(length=3), + variable_var2_completion.clone(length=3), + }, + not_expected={ + variable_var_completion.clone(length=3), + }, + ) ) self.verify_completions( - self.dap_server.get_completions("str"), - [ - {"text": "struct", "label": "struct"}, - {"text": "str1", "label": "str1", "detail": "std::string &"}, - ], + TestCase( + input="str", + expected={ + CompletionItem(label="struct", length=3), + str1_completion.clone(length=3), + }, + ) ) self.assertIsNotNone(self.dap_server.get_completions("ƒ")) # Test utf8 after ascii. + # TODO self.dap_server.get_completions("mƒ") # Completion also works for more complex expressions self.verify_completions( - self.dap_server.get_completions("foo1.v"), - [{"text": "var1", "label": "foo1.var1", "detail": "int"}], + TestCase( + input="foo1.v", + expected={CompletionItem(label="foo1.var1", detail="int", length=6)}, + ) ) self.verify_completions( - self.dap_server.get_completions("foo1.my_bar_object.v"), - [{"text": "var1", "label": "foo1.my_bar_object.var1", "detail": "int"}], + TestCase( + input="foo1.my_bar_object.v", + expected={ + CompletionItem( + label="foo1.my_bar_object.var1", detail="int", length=20 + ) + }, + ) ) self.verify_completions( - self.dap_server.get_completions("foo1.var1 + foo1.v"), - [{"text": "var1", "label": "foo1.var1", "detail": "int"}], + TestCase( + input="foo1.var1 + foo1.v", + expected={CompletionItem(label="foo1.var1", detail="int", length=6)}, + ) ) self.verify_completions( - self.dap_server.get_completions("foo1.var1 + v"), - [{"text": "var1", "label": "var1", "detail": "int &"}], + TestCase( + input="foo1.var1 + v", + expected={CompletionItem(label="var1", detail="int &", length=1)}, + ) ) # should correctly handle spaces between objects and member operators self.verify_completions( - self.dap_server.get_completions("foo1 .v"), - [{"text": "var1", "label": ".var1", "detail": "int"}], - [{"text": "var2", "label": ".var2", "detail": "int"}], + TestCase( + input="foo1 .v", + expected={CompletionItem(label=".var1", detail="int", length=2)}, + not_expected={CompletionItem(label=".var2", detail="int", length=2)}, + ) ) self.verify_completions( - self.dap_server.get_completions("foo1 . v"), - [{"text": "var1", "label": "var1", "detail": "int"}], - [{"text": "var2", "label": "var2", "detail": "int"}], + TestCase( + input="foo1 . v", + expected={CompletionItem(label="var1", detail="int", length=1)}, + not_expected={CompletionItem(label="var2", detail="int", length=1)}, + ) ) # Even in variable mode, we can still use the escape prefix self.verify_completions( - self.dap_server.get_completions("`mem"), [memory_completion] + TestCase(input="`mem", expected={memory_completion.clone(length=3)}) ) def test_auto_completions(self): @@ -261,24 +356,28 @@ def test_auto_completions(self): # We are stopped inside `main`. Variables `var1` and `var2` are in scope. # Make sure, we offer all completions self.verify_completions( - self.dap_server.get_completions("va"), - [ - command_var_completion, - variable_var1_completion, - variable_var2_completion, - ], + TestCase( + input="va", + expected={ + command_var_completion.clone(length=2), + variable_var1_completion.clone(length=2), + variable_var2_completion.clone(length=2), + }, + ) ) # If we are using the escape prefix, only commands are suggested, but no variables self.verify_completions( - self.dap_server.get_completions("`va"), - [ - command_var_completion, - ], - [ - variable_var1_completion, - variable_var2_completion, - ], + TestCase( + input="`va", + expected={ + command_var_completion.clone(length=2), + }, + not_expected={ + variable_var1_completion.clone(length=2), + variable_var2_completion.clone(length=2), + }, + ) ) # TODO: Note we are not checking the result because the `expression --` command adds an extra character diff --git a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp index 9a3973190f7e7..671d894611db8 100644 --- a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp @@ -72,6 +72,14 @@ static std::optional<size_t> GetCursorPos(StringRef text, uint32_t line, return cursor_pos; } +static size_t GetTokenLengthAtCursor(StringRef line, size_t cursor_pos) { + line = line.substr(0, cursor_pos); + const size_t idx = line.rfind(' '); + if (idx == StringRef::npos) + return line.size(); + return line.size() - (idx + 1); +} + /// Returns a list of possible completions for a given caret position and text. /// /// Clients should only call this request if the corresponding capability @@ -109,6 +117,7 @@ CompletionsRequestHandler::Run(const CompletionsArguments &args) const { cursor_pos -= escape_prefix.size(); } + const size_t partial_token_len = GetTokenLengthAtCursor(text, cursor_pos); // While the user is typing then we likely have an incomplete input and cannot // reliably determine the precise intent (command vs variable), try completing // the text as both a command and variable expression, if applicable. @@ -138,19 +147,13 @@ CompletionsRequestHandler::Run(const CompletionsArguments &args) const { const StringRef match = matches.GetStringAtIndex(i); const StringRef description = descriptions.GetStringAtIndex(i); - StringRef match_ref = match; - for (const StringRef commit_point : {".", "->"}) { - if (const size_t pos = match_ref.rfind(commit_point); - pos != StringRef::npos) { - match_ref = match_ref.substr(pos + commit_point.size()); - } - } - CompletionItem item; - item.text = match_ref; item.label = match; if (!description.empty()) item.detail = description; + // lldb returned completions includes the partial typed token + // overwrite it. + item.length = partial_token_len; targets.emplace_back(std::move(item)); } diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h b/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h index 3433dc74d5b31..71046d24c9787 100644 --- a/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h +++ b/lldb/tools/lldb-dap/Protocol/ProtocolTypes.h @@ -168,24 +168,24 @@ struct CompletionItem { /// whether it is 0- or 1-based. If the start position is omitted the text /// is added at the location specified by the `column` attribute of the /// `completions` request. - int64_t start = 0; + uint64_t start = 0; /// Length determines how many characters are overwritten by the completion /// text and it is measured in UTF-16 code units. If missing the value 0 is /// assumed which results in the completion text being inserted. - int64_t length = 0; + uint64_t length = 0; /// Determines the start of the new selection after the text has been /// inserted (or replaced). `selectionStart` is measured in UTF-16 code /// units and must be in the range 0 and length of the completion text. If /// omitted the selection starts at the end of the completion text. - int64_t selectionStart = 0; + uint64_t selectionStart = 0; /// Determines the length of the new selection after the text has been /// inserted (or replaced) and it is measured in UTF-16 code units. The /// selection can not extend beyond the bounds of the completion text. If /// omitted the length is assumed to be 0. - int64_t selectionLength = 0; + uint64_t selectionLength = 0; }; bool fromJSON(const llvm::json::Value &, CompletionItem &, llvm::json::Path); llvm::json::Value toJSON(const CompletionItem &); >From 3a9ab9400af8363727412e9b35eb0118f858119d Mon Sep 17 00:00:00 2001 From: Ebuka Ezike <[email protected]> Date: Fri, 23 Jan 2026 15:39:42 +0000 Subject: [PATCH 2/5] [lldb-dap] add review changes --- .../completions/TestDAP_completions.py | 39 +++++++++++++++++++ .../lldb-dap/Handler/CompletionsHandler.cpp | 18 ++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py index 937904d682bb0..8f6da7ca836ba 100644 --- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py +++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py @@ -100,6 +100,38 @@ def setup_debuggee(self): ], ) + def check_non_ascii_completion(self, alias_cmd: str): + """Creates an command alias for the `next` command and + verify if it has completion for the command and its help. + + It assumes we are in command mode in the repl. + """ + res = self.dap_server.request_evaluate( + f"command alias {alias_cmd} next", context="repl" + ) + self.assertTrue(res["success"]) + + part = alias_cmd[:2] # first two characters + part_codeunits = len(part.encode("utf-16-le")) // 2 + + next_detail = "Source level single step, stepping over calls. Defaults to current thread unless specified." + expected_item = CompletionItem( + label=alias_cmd, detail=next_detail, length=part_codeunits + ) + + # complete the command + self.verify_completions(TestCase(input=part, expected={expected_item.clone()})) + # complete the help + self.verify_completions( + TestCase(input=f"help {part}", expected={expected_item.clone()}) + ) + + # remove the alias + res = self.dap_server.request_evaluate( + f"command unalias {alias_cmd}", context="repl" + ) + self.assertTrue(res["success"]) + def test_command_completions(self): """ Tests completion requests for lldb commands, within "repl-mode=command" @@ -206,6 +238,13 @@ def test_command_completions(self): ) ) + # Complete custom command with non ascii character. + self.check_non_ascii_completion("n€xt") # 2 bytes £ + self.check_non_ascii_completion("n£xt") # 3 bytes € + self.check_non_ascii_completion("n💩xt") # 4 bytes 💩 + self.check_non_ascii_completion("√∂xt") # start with non ascii + self.check_non_ascii_completion("one_seç") # ends with non ascii + def test_variable_completions(self): """ Tests completion requests in "repl-mode=variable" diff --git a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp index 671d894611db8..4704abd4b60c8 100644 --- a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp @@ -12,6 +12,7 @@ #include "Protocol/ProtocolTypes.h" #include "RequestHandler.h" #include "lldb/API/SBStringList.h" +#include "llvm/ADT/SmallVector.h" #include "llvm/Support/ConvertUTF.h" using namespace llvm; @@ -72,12 +73,17 @@ static std::optional<size_t> GetCursorPos(StringRef text, uint32_t line, return cursor_pos; } -static size_t GetTokenLengthAtCursor(StringRef line, size_t cursor_pos) { +static size_t GetPartialTokenCodeUnits(StringRef line, size_t cursor_pos) { line = line.substr(0, cursor_pos); const size_t idx = line.rfind(' '); - if (idx == StringRef::npos) - return line.size(); - return line.size() - (idx + 1); + + const size_t byte_offset = (idx == StringRef::npos) ? 0 : idx + 1; + const StringRef byte_token = line.substr(byte_offset); + llvm::SmallVector<UTF16, 20> utf16_token; + + if (llvm::convertUTF8ToUTF16String(byte_token, utf16_token)) + return utf16_token.size(); + return byte_token.size(); // fallback to back to byte offset. } /// Returns a list of possible completions for a given caret position and text. @@ -117,7 +123,7 @@ CompletionsRequestHandler::Run(const CompletionsArguments &args) const { cursor_pos -= escape_prefix.size(); } - const size_t partial_token_len = GetTokenLengthAtCursor(text, cursor_pos); + const size_t partial_token_cu = GetPartialTokenCodeUnits(text, cursor_pos); // While the user is typing then we likely have an incomplete input and cannot // reliably determine the precise intent (command vs variable), try completing // the text as both a command and variable expression, if applicable. @@ -153,7 +159,7 @@ CompletionsRequestHandler::Run(const CompletionsArguments &args) const { item.detail = description; // lldb returned completions includes the partial typed token // overwrite it. - item.length = partial_token_len; + item.length = partial_token_cu; targets.emplace_back(std::move(item)); } >From be9ea448ada80e01af69bd4f9ecf022d48f8b26c Mon Sep 17 00:00:00 2001 From: Ebuka Ezike <[email protected]> Date: Fri, 23 Jan 2026 15:42:39 +0000 Subject: [PATCH 3/5] add review changes --- lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py index 8f6da7ca836ba..099c2271383d3 100644 --- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py +++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py @@ -30,7 +30,7 @@ def clone(self, **kwargs) -> CompletionItem: return replace(self, **kwargs) -@dataclass +@dataclass(frozen=True) class TestCase: input: str expected: set[CompletionItem] >From 28e8cebcedb082cfef5b5d639782de74a20e3c46 Mon Sep 17 00:00:00 2001 From: Ebuka Ezike <[email protected]> Date: Fri, 23 Jan 2026 15:48:04 +0000 Subject: [PATCH 4/5] remove unnecessary clones --- .../completions/TestDAP_completions.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py index 099c2271383d3..517f9d43ed8da 100644 --- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py +++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py @@ -120,10 +120,10 @@ def check_non_ascii_completion(self, alias_cmd: str): ) # complete the command - self.verify_completions(TestCase(input=part, expected={expected_item.clone()})) + self.verify_completions(TestCase(input=part, expected={expected_item})) # complete the help self.verify_completions( - TestCase(input=f"help {part}", expected={expected_item.clone()}) + TestCase(input=f"help {part}", expected={expected_item}) ) # remove the alias @@ -186,11 +186,7 @@ def test_command_completions(self): self.verify_completions( TestCase( input="`", - expected={ - session_completion.clone(), - settings_completion.clone(), - memory_completion.clone(), - }, + expected={session_completion, settings_completion, memory_completion}, ) ) @@ -225,8 +221,8 @@ def test_command_completions(self): self.verify_completions( TestCase( input="var", - expected={command_var_completion.clone()}, - not_expected={variable_var_completion.clone()}, + expected={command_var_completion}, + not_expected={variable_var_completion}, ) ) @@ -261,8 +257,8 @@ def test_variable_completions(self): self.verify_completions( TestCase( input="var", - expected={variable_var_completion.clone()}, - not_expected={command_var_completion.clone()}, + expected={variable_var_completion}, + not_expected={command_var_completion}, ) ) >From 0435cc35e8b0dd5f9fd6efe96b92258e98a02616 Mon Sep 17 00:00:00 2001 From: Ebuka Ezike <[email protected]> Date: Fri, 23 Jan 2026 18:37:05 +0000 Subject: [PATCH 5/5] add review changes --- .../completions/TestDAP_completions.py | 75 +++++++++---------- .../lldb-dap/Handler/CompletionsHandler.cpp | 4 +- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py index 517f9d43ed8da..3996bdf77242d 100644 --- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py +++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py @@ -31,7 +31,7 @@ def clone(self, **kwargs) -> CompletionItem: @dataclass(frozen=True) -class TestCase: +class Scenario: input: str expected: set[CompletionItem] not_expected: Optional[set[CompletionItem]] = None @@ -68,23 +68,20 @@ class TestCase: # templates like vector. @skipIf(compiler="clang", compiler_version=["<", "16.0"]) class TestDAP_completions(lldbdap_testcase.DAPTestCaseBase): - def verify_completions(self, case: TestCase): + def verify_completions(self, case: Scenario): completions = { CompletionItem(**comp) for comp in self.dap_server.get_completions(case.input) } # handle expected completions - expected_completions = case.expected - for exp_comp in expected_completions: - # with self.subTest(f"Expected completion : {exp_comp}"): + for exp_comp in case.expected: self.assertIn( exp_comp, completions, f"\nCompletion for input: {case.input}" ) # unexpected completions - not_expected_label = case.not_expected or set() - for not_exp_comp in not_expected_label: + for not_exp_comp in case.not_expected or set(): with self.subTest(f"Not expected completion : {not_exp_comp}"): self.assertNotIn(not_exp_comp, completions) @@ -100,7 +97,7 @@ def setup_debuggee(self): ], ) - def check_non_ascii_completion(self, alias_cmd: str): + def verify_non_ascii_completion(self, alias_cmd: str): """Creates an command alias for the `next` command and verify if it has completion for the command and its help. @@ -120,10 +117,10 @@ def check_non_ascii_completion(self, alias_cmd: str): ) # complete the command - self.verify_completions(TestCase(input=part, expected={expected_item})) + self.verify_completions(Scenario(input=part, expected={expected_item})) # complete the help self.verify_completions( - TestCase(input=f"help {part}", expected={expected_item}) + Scenario(input=f"help {part}", expected={expected_item}) ) # remove the alias @@ -146,7 +143,7 @@ def test_command_completions(self): # Provides completion for top-level commands self.verify_completions( - TestCase( + Scenario( input="se", expected={ session_completion.clone(length=2), @@ -156,7 +153,7 @@ def test_command_completions(self): ) # Provides completions for sub-commands self.verify_completions( - TestCase( + Scenario( input="memory ", expected={ CompletionItem( @@ -173,18 +170,18 @@ def test_command_completions(self): # Provides completions for parameter values of commands self.verify_completions( - TestCase( + Scenario( input="`log enable ", expected={CompletionItem(label="gdb-remote")} ) ) # Also works if the escape prefix is used self.verify_completions( - TestCase(input="`mem", expected={memory_completion.clone(length=3)}) + Scenario(input="`mem", expected={memory_completion.clone(length=3)}) ) self.verify_completions( - TestCase( + Scenario( input="`", expected={session_completion, settings_completion, memory_completion}, ) @@ -192,7 +189,7 @@ def test_command_completions(self): # Completes an incomplete quoted token self.verify_completions( - TestCase( + Scenario( input='setting "se', expected={ CompletionItem( @@ -206,12 +203,12 @@ def test_command_completions(self): # Completes an incomplete quoted token self.verify_completions( - TestCase(input="'mem", expected={memory_completion.clone(length=4)}) + Scenario(input="'mem", expected={memory_completion.clone(length=4)}) ) # Completes expressions with quotes inside self.verify_completions( - TestCase( + Scenario( input='expr " "; typed', expected={CompletionItem(label="typedef", length=5)}, ) @@ -219,7 +216,7 @@ def test_command_completions(self): # Provides completions for commands, but not variables self.verify_completions( - TestCase( + Scenario( input="var", expected={command_var_completion}, not_expected={variable_var_completion}, @@ -228,18 +225,18 @@ def test_command_completions(self): # Completes partial completion self.verify_completions( - TestCase( + Scenario( input="plugin list ar", expected={CompletionItem(label="architecture", length=2)}, ) ) # Complete custom command with non ascii character. - self.check_non_ascii_completion("n€xt") # 2 bytes £ - self.check_non_ascii_completion("n£xt") # 3 bytes € - self.check_non_ascii_completion("n💩xt") # 4 bytes 💩 - self.check_non_ascii_completion("√∂xt") # start with non ascii - self.check_non_ascii_completion("one_seç") # ends with non ascii + self.verify_non_ascii_completion("n€xt") # 2 bytes £ + self.verify_non_ascii_completion("n£xt") # 3 bytes € + self.verify_non_ascii_completion("n💩xt") # 4 bytes 💩 + self.verify_non_ascii_completion("√∂xt") # start with non ascii + self.verify_non_ascii_completion("one_seç") # ends with non ascii def test_variable_completions(self): """ @@ -255,7 +252,7 @@ def test_variable_completions(self): # Provides completions for varibles, but not command self.verify_completions( - TestCase( + Scenario( input="var", expected={variable_var_completion}, not_expected={command_var_completion}, @@ -264,7 +261,7 @@ def test_variable_completions(self): # We stopped inside `fun`, so we shouldn't see variables from main self.verify_completions( - TestCase( + Scenario( input="var", expected={variable_var_completion}, not_expected={ @@ -276,7 +273,7 @@ def test_variable_completions(self): # We should see global keywords but not variables inside main self.verify_completions( - TestCase( + Scenario( input="str", expected={CompletionItem(label="struct", length=3)}, not_expected={str1_completion.clone(length=3)}, @@ -288,7 +285,7 @@ def test_variable_completions(self): # We stopped in `main`, so we should see variables from main but # not from the other function self.verify_completions( - TestCase( + Scenario( input="var", expected={ variable_var1_completion.clone(length=3), @@ -301,7 +298,7 @@ def test_variable_completions(self): ) self.verify_completions( - TestCase( + Scenario( input="str", expected={ CompletionItem(label="struct", length=3), @@ -317,14 +314,14 @@ def test_variable_completions(self): # Completion also works for more complex expressions self.verify_completions( - TestCase( + Scenario( input="foo1.v", expected={CompletionItem(label="foo1.var1", detail="int", length=6)}, ) ) self.verify_completions( - TestCase( + Scenario( input="foo1.my_bar_object.v", expected={ CompletionItem( @@ -335,14 +332,14 @@ def test_variable_completions(self): ) self.verify_completions( - TestCase( + Scenario( input="foo1.var1 + foo1.v", expected={CompletionItem(label="foo1.var1", detail="int", length=6)}, ) ) self.verify_completions( - TestCase( + Scenario( input="foo1.var1 + v", expected={CompletionItem(label="var1", detail="int &", length=1)}, ) @@ -350,7 +347,7 @@ def test_variable_completions(self): # should correctly handle spaces between objects and member operators self.verify_completions( - TestCase( + Scenario( input="foo1 .v", expected={CompletionItem(label=".var1", detail="int", length=2)}, not_expected={CompletionItem(label=".var2", detail="int", length=2)}, @@ -358,7 +355,7 @@ def test_variable_completions(self): ) self.verify_completions( - TestCase( + Scenario( input="foo1 . v", expected={CompletionItem(label="var1", detail="int", length=1)}, not_expected={CompletionItem(label="var2", detail="int", length=1)}, @@ -367,7 +364,7 @@ def test_variable_completions(self): # Even in variable mode, we can still use the escape prefix self.verify_completions( - TestCase(input="`mem", expected={memory_completion.clone(length=3)}) + Scenario(input="`mem", expected={memory_completion.clone(length=3)}) ) def test_auto_completions(self): @@ -391,7 +388,7 @@ def test_auto_completions(self): # We are stopped inside `main`. Variables `var1` and `var2` are in scope. # Make sure, we offer all completions self.verify_completions( - TestCase( + Scenario( input="va", expected={ command_var_completion.clone(length=2), @@ -403,7 +400,7 @@ def test_auto_completions(self): # If we are using the escape prefix, only commands are suggested, but no variables self.verify_completions( - TestCase( + Scenario( input="`va", expected={ command_var_completion.clone(length=2), diff --git a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp index 4704abd4b60c8..47e0f1a35ebac 100644 --- a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp @@ -79,9 +79,9 @@ static size_t GetPartialTokenCodeUnits(StringRef line, size_t cursor_pos) { const size_t byte_offset = (idx == StringRef::npos) ? 0 : idx + 1; const StringRef byte_token = line.substr(byte_offset); - llvm::SmallVector<UTF16, 20> utf16_token; + SmallVector<UTF16, 20> utf16_token; - if (llvm::convertUTF8ToUTF16String(byte_token, utf16_token)) + if (convertUTF8ToUTF16String(byte_token, utf16_token)) return utf16_token.size(); return byte_token.size(); // fallback to back to byte offset. } _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
