[lldb] Add completions for plugin list/enable/disable (#147775)

This commit adds completion support for the plugin commands. It will try
to complete partial namespaces to the full namespace string. If the
completion input is already a full namespace string then it will add all
the matching plugins in that namespace as completions.

This lets the user complete to the namespace first and then tab-complete
to the next level if desired.

```
(lldb) plugin list a<tab>
Available completions:
        abi
        architecture
(lldb) plugin list ab<tab>
(lldb) plugin list abi<tab>
(lldb) plugin list abi.<tab>
Available completions:
        abi.SysV-arm64
        abi.ABIMacOSX_arm64
        abi.SysV-arm
        ...
```
This commit is contained in:
David Peixotto
2025-07-15 12:44:00 -07:00
committed by GitHub
parent d67d91a990
commit fccae859bc
8 changed files with 130 additions and 5 deletions

View File

@@ -787,6 +787,9 @@ public:
static std::vector<RegisteredPluginInfo> GetUnwindAssemblyPluginInfo();
static bool SetUnwindAssemblyPluginEnabled(llvm::StringRef name, bool enable);
static void AutoCompletePluginName(llvm::StringRef partial_name,
CompletionRequest &request);
};
} // namespace lldb_private

View File

@@ -123,6 +123,10 @@ public:
static void ThreadIDs(CommandInterpreter &interpreter,
CompletionRequest &request, SearchFilter *searcher);
static void ManagedPlugins(CommandInterpreter &interpreter,
CompletionRequest &request,
SearchFilter *searcher);
/// This completer works for commands whose only arguments are a command path.
/// It isn't tied to an argument type because it completes not on a single
/// argument but on the sequence of arguments, so you have to invoke it by

View File

@@ -1321,10 +1321,11 @@ enum CompletionType {
eTypeCategoryNameCompletion = (1ul << 24),
eCustomCompletion = (1ul << 25),
eThreadIDCompletion = (1ul << 26),
eManagedPluginCompletion = (1ul << 27),
// This last enum element is just for input validation.
// Add new completions before this element,
// and then increment eTerminatorCompletion's shift value
eTerminatorCompletion = (1ul << 27)
eTerminatorCompletion = (1ul << 28)
};
/// Specifies if children need to be re-computed

View File

@@ -2268,7 +2268,7 @@ class TestBase(Base, metaclass=LLDBTestCaseFactory):
completions, list(match_strings)[1:], "List of returned completion is wrong"
)
def completions_contain(self, command, completions):
def completions_contain(self, command, completions, match=True):
"""Checks that the completions for the given command contain the given
list of completions."""
interp = self.dbg.GetCommandInterpreter()
@@ -2276,9 +2276,16 @@ class TestBase(Base, metaclass=LLDBTestCaseFactory):
interp.HandleCompletion(command, len(command), 0, -1, match_strings)
for completion in completions:
# match_strings is a 1-indexed list, so we have to slice...
self.assertIn(
completion, list(match_strings)[1:], "Couldn't find expected completion"
)
if match:
self.assertIn(
completion,
list(match_strings)[1:],
"Couldn't find expected completion",
)
else:
self.assertNotIn(
completion, list(match_strings)[1:], "Found unexpected completion"
)
def filecheck(
self, command, check_file, filecheck_options="", expect_cmd_failure=False

View File

@@ -87,6 +87,7 @@ bool CommandCompletions::InvokeCommonCompletionCallbacks(
{lldb::eTypeCategoryNameCompletion,
CommandCompletions::TypeCategoryNames},
{lldb::eThreadIDCompletion, CommandCompletions::ThreadIDs},
{lldb::eManagedPluginCompletion, CommandCompletions::ManagedPlugins},
{lldb::eTerminatorCompletion,
nullptr} // This one has to be last in the list.
};
@@ -850,6 +851,13 @@ void CommandCompletions::ThreadIDs(CommandInterpreter &interpreter,
}
}
void CommandCompletions::ManagedPlugins(CommandInterpreter &interpreter,
CompletionRequest &request,
SearchFilter *searcher) {
PluginManager::AutoCompletePluginName(request.GetCursorArgumentPrefix(),
request);
}
void CommandCompletions::CompleteModifiableCmdPathArgs(
CommandInterpreter &interpreter, CompletionRequest &request,
OptionElementVector &opt_element_vector) {

View File

@@ -194,6 +194,14 @@ List only the plugin 'foo' matching a fully qualified name exactly
Options *GetOptions() override { return &m_options; }
void
HandleArgumentCompletion(CompletionRequest &request,
OptionElementVector &opt_element_vector) override {
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
GetCommandInterpreter(), lldb::eManagedPluginCompletion, request,
nullptr);
}
protected:
void DoExecute(Args &command, CommandReturnObject &result) override {
size_t argc = command.GetArgumentCount();
@@ -293,6 +301,14 @@ public:
AddSimpleArgumentList(eArgTypeManagedPlugin);
}
void
HandleArgumentCompletion(CompletionRequest &request,
OptionElementVector &opt_element_vector) override {
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
GetCommandInterpreter(), lldb::eManagedPluginCompletion, request,
nullptr);
}
~CommandObjectPluginEnable() override = default;
protected:
@@ -309,6 +325,14 @@ public:
AddSimpleArgumentList(eArgTypeManagedPlugin);
}
void
HandleArgumentCompletion(CompletionRequest &request,
OptionElementVector &opt_element_vector) override {
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
GetCommandInterpreter(), lldb::eManagedPluginCompletion, request,
nullptr);
}
~CommandObjectPluginDisable() override = default;
protected:

View File

@@ -18,6 +18,7 @@
#include "lldb/Utility/Status.h"
#include "lldb/Utility/StringList.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/DynamicLibrary.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/raw_ostream.h"
@@ -2473,3 +2474,34 @@ bool PluginManager::SetUnwindAssemblyPluginEnabled(llvm::StringRef name,
bool enable) {
return GetUnwindAssemblyInstances().SetInstanceEnabled(name, enable);
}
void PluginManager::AutoCompletePluginName(llvm::StringRef name,
CompletionRequest &request) {
// Split the name into the namespace and the plugin name.
// If there is no dot then the ns_name will be equal to name and
// plugin_prefix will be empty.
llvm::StringRef ns_name, plugin_prefix;
std::tie(ns_name, plugin_prefix) = name.split('.');
for (const PluginNamespace &plugin_ns : GetPluginNamespaces()) {
// If the plugin namespace matches exactly then
// add all the plugins in this namespace as completions if the
// plugin names starts with the plugin_prefix. If the plugin_prefix
// is empty then it will match all the plugins (empty string is a
// prefix of everything).
if (plugin_ns.name == ns_name) {
for (const RegisteredPluginInfo &plugin : plugin_ns.get_info()) {
llvm::SmallString<128> buf;
if (plugin.name.starts_with(plugin_prefix))
request.AddCompletion(
(plugin_ns.name + "." + plugin.name).toStringRef(buf));
}
} else if (plugin_ns.name.starts_with(name) &&
!plugin_ns.get_info().empty()) {
// Otherwise check if the namespace is a prefix of the full name.
// Use a partial completion here so that we can either operate on the full
// namespace or tab-complete to the next level.
request.AddCompletion(plugin_ns.name, "", CompletionMode::Partial);
}
}
}

View File

@@ -60,3 +60,49 @@ class TestFrameVar(TestBase):
self.expect(
f"plugin enable {plugin_namespace}", substrs=[plugin_namespace, "[+]"]
)
def test_completions(self):
# Make sure completions work for the plugin list, enable, and disable commands.
# We just check a few of the expected plugins to make sure the completion works.
self.completions_contain(
"plugin list ", ["abi", "architecture", "disassembler"]
)
self.completions_contain(
"plugin enable ", ["abi", "architecture", "disassembler"]
)
self.completions_contain(
"plugin disable ", ["abi", "architecture", "disassembler"]
)
# A completion for a partial namespace should be the full namespace.
# This allows the user to run the command on the full namespace.
self.completions_match("plugin list ab", ["abi"])
self.completions_contain(
"plugin list object", ["object-container", "object-file"]
)
# A completion for a full namespace should contain the plugins in that namespace.
self.completions_contain("plugin list abi", ["abi.sysv-x86_64"])
self.completions_contain("plugin list abi.", ["abi.sysv-x86_64"])
self.completions_contain("plugin list abi.s", ["abi.sysv-x86_64"])
self.completions_contain("plugin list abi.sysv-x", ["abi.sysv-x86_64"])
# Check for a completion that is a both a complete namespace and a prefix of
# another namespace. It should return the completions for the plugins in the completed
# namespace as well as the completion for the partial namespace.
self.completions_contain(
"plugin list language", ["language.cplusplus", "language-runtime"]
)
# When the namespace is a prefix of another namespace and the user types a dot, the
# completion should not include the match for the partial namespace.
self.completions_contain(
"plugin list language.", ["language.cplusplus"], match=True
)
self.completions_contain(
"plugin list language.", ["language-runtime"], match=False
)
# Check for an empty completion list when the names is invalid.
# See docs for `complete_from_to` for how this checks for an empty list.
self.complete_from_to("plugin list abi.foo", ["plugin list abi.foo"])