-
Notifications
You must be signed in to change notification settings - Fork 14.3k
Add the ability to define custom completers to the parsed_cmd template. #109062
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
and arguments to the parsed_cmd template.
@llvm/pr-subscribers-lldb Author: None (jimingham) ChangesIf your arguments or option values are of a type that naturally uses one of our common completion mechanisms, you will get completion for free. But if you have your own custom values or if you want to do fancy things like have Patch is 48.25 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/109062.diff 14 Files Affected:
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index 810673aaec5d19..1f5012af99a291 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -752,6 +752,79 @@ lldb_private::python::SWIGBridge::LLDBSwigPythonGetRepeatCommandForScriptedComma
return result.Str().GetString().str();
}
+StructuredData::DictionarySP
+lldb_private::python::SWIGBridge::LLDBSwigPythonHandleArgumentCompletionForScriptedCommand(PyObject *implementor,
+ std::vector<llvm::StringRef> &args_vec, size_t args_pos, size_t pos_in_arg) {
+
+ PyErr_Cleaner py_err_cleaner(true);
+
+ PythonObject self(PyRefType::Borrowed, implementor);
+ auto pfunc = self.ResolveName<PythonCallable>("handle_argument_completion");
+ // If this isn't implemented, return an empty dict to signal falling back to default completion:
+ if (!pfunc.IsAllocated())
+ return {};
+
+ PythonList args_list(PyInitialValue::Empty);
+ for (auto elem : args_vec)
+ args_list.AppendItem(PythonString(elem));
+
+ PythonObject result = pfunc(args_list, PythonInteger(args_pos), PythonInteger(pos_in_arg));
+ // Returning None means do the ordinary completion
+ if (result.IsNone())
+ return {};
+
+ // Convert the return dictionary to a DictionarySP.
+ StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
+ if (!result_obj_sp)
+ return {};
+
+ StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(result_obj_sp));
+ if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
+ return {};
+ return dict_sp;
+}
+
+StructuredData::DictionarySP
+lldb_private::python::SWIGBridge::LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand(PyObject *implementor,
+ llvm::StringRef &long_option, size_t pos_in_arg) {
+
+ PyErr_Cleaner py_err_cleaner(true);
+
+ PythonObject self(PyRefType::Borrowed, implementor);
+ auto pfunc = self.ResolveName<PythonCallable>("handle_option_argument_completion");
+ // If this isn't implemented, return an empty dict to signal falling back to default completion:
+ if (!pfunc.IsAllocated())
+ return {};
+
+ PythonObject result = pfunc(PythonString(long_option), PythonInteger(pos_in_arg));
+ // Returning None means do the ordinary completion
+ if (result.IsNone())
+ return {};
+
+ // Returning a boolean:
+ // True means the completion was handled, but there were no completions
+ // False means that the completion was not handled, again, do the ordinary completion:
+ if (result.GetObjectType() == PyObjectType::Boolean) {
+ if (!result.IsTrue())
+ return {};
+ // Make up a completion dictionary with the right element:
+ StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary());
+ dict_sp->AddBooleanItem("no-completion", true);
+ return dict_sp;
+ }
+
+
+ // Convert the return dictionary to a DictionarySP.
+ StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
+ if (!result_obj_sp)
+ return {};
+
+ StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(result_obj_sp));
+ if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
+ return {};
+ return dict_sp;
+}
+
#include "lldb/Interpreter/CommandReturnObject.h"
bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
diff --git a/lldb/docs/use/python-reference.rst b/lldb/docs/use/python-reference.rst
index 041e541a96f083..de4f219a13292f 100644
--- a/lldb/docs/use/python-reference.rst
+++ b/lldb/docs/use/python-reference.rst
@@ -587,8 +587,150 @@ say
SBCommandReturnObject and SBStream both support this file-like behavior by
providing write() and flush() calls at the Python layer.
+The commands that are added using this Class definition are what lldb calls
+"raw" commands. The command interpreter doesn't attempt to parse the command,
+doesn't handle option values, neither generating help for them, or their
+completion. Raw commands are useful when the arguments passed to the command
+are unstructured, and having to protect them against lldb command parsing would
+be onerous. For instance, "expr" is a raw command.
+
+You can also add scripted commands that implement the "parsed command", where
+the options and their types are specified, as well as the argument and argument
+types. These commands look and act like the majority of lldb commands, and you
+can also add custom completions for the options and/or the arguments if you have
+special needs.
+
+The easiest way to do this is to derive your new command from the lldb.ParsedCommand
+class. That responds in the same way to the help & repeat command interfaces, and
+provides some convenience methods, and most importantly an LLDBOptionValueParser,
+accessed throught lldb.ParsedCommand.get_parser(). The parser is used to set
+your command definitions, and to retrieve option values in the __call__ method.
+
+To set the command definition, implement the ParsedCommand abstract method:
+
+::
+
+ def setup_command_definition(self):
+
+This is called when your command is added to lldb. In this method you add the
+options and their types, the option help strings, etc. to the command using the API:
+
+::
+
+ def add_option(self, short_option, long_option, help, default,
+ dest = None, required=False, groups = None,
+ value_type=lldb.eArgTypeNone, completion_type=None,
+ enum_values=None):
+ """
+ short_option: one character, must be unique, not required
+ long_option: no spaces, must be unique, required
+ help: a usage string for this option, will print in the command help
+ default: the initial value for this option (if it has a value)
+ dest: the name of the property that gives you access to the value for
+ this value. Defaults to the long option if not provided.
+ required: if true, this option must be provided or the command will error out
+ groups: Which "option groups" does this option belong to. This can either be
+ a simple list (e.g. [1, 3, 4, 5]) or you can specify ranges by sublists:
+ so [1, [3,5]] is the same as [1, 3, 4, 5].
+ value_type: one of the lldb.eArgType enum values. Some of the common arg
+ types also have default completers, which will be applied automatically.
+ completion_type: currently these are values form the lldb.CompletionType enum. If
+ you need custom completions, implement handle_option_argument_completion.
+ enum_values: An array of duples: ["element_name", "element_help"]. If provided,
+ only one of the enum elements is allowed. The value will be the
+ element_name for the chosen enum element as a string.
+ """
+
+Similarly, you can add argument types to the command:
+
+::
+
+ def make_argument_element(self, arg_type, repeat = "optional", groups = None):
+ """
+ arg_type: The argument type, one of the lldb.eArgType enum values.
+ repeat: Choose from the following options:
+ "plain" - one value
+ "optional" - zero or more values
+ "plus" - one or more values
+ groups: As with add_option.
+ """
+
+Then implement the body of the command by defining:
+
+::
+
+ def __call__(self, debugger, args_array, exe_ctx, result):
+ """This is the command callback. The option values are
+ provided by the 'dest' properties on the parser.
+
+ args_array: This is the list of arguments provided.
+ exe_ctx: Gives the SBExecutionContext on which the
+ command should operate.
+ result: Any results of the command should be
+ written into this SBCommandReturnObject.
+ """
+
+This differs from the "raw" command's __call__ in that the arguments are already
+parsed into the args_array, and the option values are set in the parser, and
+can be accessed using their property name. The LLDBOptionValueParser class has
+a couple of other handy methods:
+
+::
+ def was_set(self, long_option_name):
+
+returns True if the option was specified on the command line.
+
+::
+ def dest_for_option(self, long_option_name):
+ """
+ This will return the value of the dest variable you defined for opt_name.
+ Mostly useful for handle_completion where you get passed the long option.
+ """
+
+lldb will handle completing your option names, and all your enum values
+automatically. If your option or argument types have associated built-in completers,
+then lldb will also handle that completion for you. But if you have a need for
+custom completions, either in your arguments or option values, you can handle
+completion by hand as well. To handle completion of option value arguments,
+your lldb.ParsedCommand subclass should implement:
+
+::
+ def handle_option_argument_completion(self, args, arg_pos, cursor_pos):
+ """
+ args: A list of the arguments to the command
+ arg_pos: An index into the args list of the argument with the cursor
+ cursor_pos: The cursor position in the arg specified by arg_pos
+ """
+
+When this command is called, the command line has been parsed up to the word
+containing the cursor, and any option values set in that part of the command
+string are available from the option value parser. That's useful for instance
+if you have a --shared-library option that would constrain the completions for,
+say, a symbol name option or argument.
+
+The return value specifies what the completion options are. You have four
+choices:
+
+True: the completion was handled with no completions.
+
+False: the completion was not handled, forward it to the regular
+completion machinery.
+
+A dictionary with the key: "completion": there is one candidate,
+whose value is the value of the "completion" key. Optionally you can pass a
+"mode" key whose value is either "partial" or "complete". Return partial if
+the "completion" string is a prefix for all the completed value, and "complete"
+if it is the full completion. The default is "complete".
+
+A dictionary with the key: "values" whose value is a list of candidate completion
+strings. The command interpreter will present those strings as the available choices.
+You can optionally include a "descriptions" key, whose value is a parallel array
+of description strings, and the completion will show the description next to
+each completion.
+
+
One other handy convenience when defining lldb command-line commands is the
-command command script import which will import a module specified by file
+command "command script import" which will import a module specified by file
path, so you don't have to change your PYTHONPATH for temporary scripts. It
also has another convenience that if your new script module has a function of
the form:
diff --git a/lldb/examples/python/cmdtemplate.py b/lldb/examples/python/cmdtemplate.py
index 9a96888508b6f2..bbdc579f7e8f75 100644
--- a/lldb/examples/python/cmdtemplate.py
+++ b/lldb/examples/python/cmdtemplate.py
@@ -26,8 +26,8 @@ def register_lldb_command(cls, debugger, module_name):
)
def setup_command_definition(self):
-
- self.ov_parser.add_option(
+ ov_parser = self.get_parser()
+ ov_parser.add_option(
"i",
"in-scope",
help = "in_scope_only = True",
@@ -36,7 +36,7 @@ def setup_command_definition(self):
default = True,
)
- self.ov_parser.add_option(
+ ov_parser.add_option(
"i",
"in-scope",
help = "in_scope_only = True",
@@ -45,7 +45,7 @@ def setup_command_definition(self):
default=True,
)
- self.ov_parser.add_option(
+ ov_parser.add_option(
"a",
"arguments",
help = "arguments = True",
@@ -54,7 +54,7 @@ def setup_command_definition(self):
default = True,
)
- self.ov_parser.add_option(
+ ov_parser.add_option(
"l",
"locals",
help = "locals = True",
@@ -63,7 +63,7 @@ def setup_command_definition(self):
default = True,
)
- self.ov_parser.add_option(
+ ov_parser.add_option(
"s",
"statics",
help = "statics = True",
@@ -100,8 +100,9 @@ def __call__(self, debugger, command, exe_ctx, result):
result.SetError("invalid frame")
return
+ ov_parser = self.get_parser()
variables_list = frame.GetVariables(
- self.ov_parser.arguments, self.ov_parser.locals, self.ov_parser.statics, self.ov_parser.inscope
+ ov_parser.arguments, ov_parser.locals, ov_parser.statics, ov_parser.inscope
)
variables_count = variables_list.GetSize()
if variables_count == 0:
diff --git a/lldb/examples/python/templates/parsed_cmd.py b/lldb/examples/python/templates/parsed_cmd.py
index 06124adf43420a..c2be2d8d835f33 100644
--- a/lldb/examples/python/templates/parsed_cmd.py
+++ b/lldb/examples/python/templates/parsed_cmd.py
@@ -43,7 +43,65 @@ def __call__(self, debugger, args_list, exe_ctx, result):
will return True if the user set this option, and False if it was left at its default
value.
-There are example commands in the lldb testsuite at:
+Custom Completions:
+
+You can also implement custom completers for your custom command, either for the
+arguments to your command or to the option values in your command. If you use enum
+values or if your option/argument uses is one of the types we have completers for,
+you should not need to do this. But if you have your own completeable types, or if
+you want completion of one option to be conditioned by other options on the command
+line, you can use this interface to take over the completion.
+
+You can choose to add a completion for the option values defined for your command,
+or for the arguments, separately. For the option values, define:
+
+def handle_option_argument_completion(self, long_option, cursor_pos):
+
+The line to be completed will be parsed up to the option containint the cursor position,
+and the values will be set in the OptionValue parser object. long_option will be
+the option name containing the cursor, and cursor_pos will be the position of the cursor
+in that option's value. You can call the OVParser.dest_for_option(long_option) to get the
+value for that option. The other options that came before the cursor in the command
+line will also be set in the OV Parser when the completion handler is called.
+
+For argument values, define:
+
+def handle_argument_completion(self, args, arg_pos, cursor_pos):
+
+Again, the command line will be parsed up to the cursor position, and all the options
+before the cursor pose will be set in the OVParser. args is a python list of the
+arguments, arg_pos is the index of the argument with the cursor, and cursor_pos is
+the position of the cursor in the argument.
+
+In both cases, the return value determines the completion.
+
+Return False to mean "Not Handled" - in which case lldb will fall back on the
+standard completion machinery.
+
+Return True to mean "Handled with no completions".
+
+If there is a single unique completion, return a Python dictionary with two elements:
+
+return {"completion" : "completed_value", "mode" : <"partial", "complete">}
+
+If the mode is "partial", then the completion is to a common base, if it is "complete"
+then the argument is considered done - mostly meaning lldb will put a space after the
+completion string. "complete" is the default if no "mode" is specified.
+
+If there are multiple completion options, then return:
+
+return {"values" : ["option1", "option2"]}
+
+Optionally, you can return a parallel array of "descriptions" which the completer will
+print alongside the options:
+
+return {"values" : ["option1", "option2"], "descriptions" : ["the first option", "the second option"]}
+
+The cmdtemplate example currently uses the parsed command infrastructure:
+
+llvm-project/lldb/examples/python/cmdtemplate.py
+
+There are also a few example commands in the lldb testsuite at:
llvm-project/lldb/test/API/commands/command/script/add/test_commands.py
"""
@@ -229,7 +287,11 @@ def was_set(self, opt_name):
""" Call this in the __call__ method of your command to determine
whether this option was set on the command line. It is sometimes
useful to know whether an option has the default value because the
- user set it explicitly (was_set -> True) or not. """
+ user set it explicitly (was_set -> True) or not.
+ You can also call this in a handle_completion method, but it will
+ currently only report true values for the options mentioned
+ BEFORE the cursor point in the command line.
+ """
elem = self.get_option_element(opt_name)
if not elem:
@@ -239,6 +301,16 @@ def was_set(self, opt_name):
except AttributeError:
return False
+ def dest_for_option(self, opt_name):
+ """ This will return the value of the dest variable you defined for opt_name.
+ Mostly useful for handle_completion where you get passed the long option.
+ """
+ elem = self.get_option_element(opt_name)
+ if not elem:
+ return None
+ value = self.__dict__[elem["dest"]]
+ return value
+
def add_option(self, short_option, long_option, help, default,
dest = None, required=False, groups = None,
value_type=lldb.eArgTypeNone, completion_type=None,
@@ -251,11 +323,13 @@ def add_option(self, short_option, long_option, help, default,
dest: the name of the property that gives you access to the value for
this value. Defaults to the long option if not provided.
required: if true, this option must be provided or the command will error out
- groups: Which "option groups" does this option belong to
+ groups: Which "option groups" does this option belong to. This can either be
+ a simple list (e.g. [1, 3, 4, 5]) or you can specify ranges by sublists:
+ so [1, [3,5]] is the same as [1, 3, 4, 5].
value_type: one of the lldb.eArgType enum values. Some of the common arg
types also have default completers, which will be applied automatically.
- completion_type: currently these are values form the lldb.CompletionType enum, I
- haven't done custom completions yet.
+ completion_type: currently these are values form the lldb.CompletionType enum. If
+ you need custom completions, implement handle_option_argument_completion.
enum_values: An array of duples: ["element_name", "element_help"]. If provided,
only one of the enum elements is allowed. The value will be the
element_name for the chosen enum element as a string.
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index addb1394ab5652..6fc97466892ca9 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -436,6 +436,21 @@ class ScriptInterpreter : public PluginInterface {
Args &args) {
return std::nullopt;
}
+
+ virtual StructuredData::DictionarySP HandleArgumentCompletionForScriptedCommand(
+ StructuredData::GenericSP impl_obj_sp, std::vector<llvm::StringRef> &args,
+ size_t args_pos, size_t char_in_arg)
+ {
+ return {};
+ }
+
+ virtual StructuredData::DictionarySP
+ HandleOptionArgumentCompletionForScriptedCommand(
+ StructuredData::GenericSP impl_obj_sp, llvm::StringRef &long_name,
+ size_t char_in_arg)
+ {
+ return {};
+ }
virtual bool RunScriptFormatKeyword(const char *impl_function,
Process *process, std::string &output,
diff --git a/lldb/include/lldb/Utility/CompletionRequest.h b/lldb/include/lldb/Utility/CompletionRequest.h
index 1a2b1d639950fc..242ff383047410 100644
--- a/lldb/include/lldb/Utility/...
[truncated]
|
✅ With the latest revision this PR passed the Python code formatter. |
✅ With the latest revision this PR passed the C/C++ code formatter. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this looks fine. It would be nice to surround the keywords with backticks and the function definition with a .. code-block:: python
so it gets rendered properly on the website.
lldb/docs/use/python-reference.rst
Outdated
accessed throught lldb.ParsedCommand.get_parser(). The parser is used to set | ||
your command definitions, and to retrieve option values in the __call__ method. | ||
|
||
To set the command definition, implement the ParsedCommand abstract method: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we missing a word here ?
To set the command definition, implement the ParsedCommand abstract method: | |
To set up the command definition, implement the ParsedCommand abstract method: |
lldb/docs/use/python-reference.rst
Outdated
def handle_option_argument_completion(self, args, arg_pos, cursor_pos): | ||
""" | ||
args: A list of the arguments to the command | ||
arg_pos: An index into the args list of the argument with the cursor | ||
cursor_pos: The cursor position in the arg specified by arg_pos | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the same method used to implement custom completion for both option values and arguments ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I stopped half way through...
lldb/docs/use/python-reference.rst
Outdated
A dictionary with the key: "completion": there is one candidate, | ||
whose value is the value of the "completion" key. Optionally you can pass a | ||
"mode" key whose value is either "partial" or "complete". Return partial if | ||
the "completion" string is a prefix for all the completed value, and "complete" | ||
if it is the full completion. The default is "complete". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This paragraph is a bit confusing to me, an example would be useful or may be could be re-written.
lldb/docs/use/python-reference.rst
Outdated
A dictionary with the key: "values" whose value is a list of candidate completion | ||
strings. The command interpreter will present those strings as the available choices. | ||
You can optionally include a "descriptions" key, whose value is a parallel array | ||
of description strings, and the completion will show the description next to | ||
each completion. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto: an example might be useful here
|
||
def handle_option_argument_completion(self, long_option, cursor_pos): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use .. code-block:: python
so this gets rendered differently on the website
def handle_option_argument_completion(self, long_option, cursor_pos): | |
.. code-block:: python | |
def handle_option_argument_completion(self, long_option, cursor_pos): | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this throughout this section, though if we're being consistent there are probably a bunch of other places in this doc that we should do that.
for (size_t idx = 0; idx < num_completions; idx++) { | ||
auto val = completions->GetItemAtIndexAsString(idx); | ||
if (!val) | ||
// FIXME: How do I report this error? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
todo ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More of an existential question. We're in the middle of handling completion, we really don't have any acceptable way to stick an error message in the middle of that. So I actually don't currently know how to tell the user. We could log this, but that's not somewhere folks who are fiddling around with this would know to look.
I think more generally we should have a "python developer's" mode where we dump any python errors to the immediate debugger stdout so they will be obvious even if they mangle up the terminal contents. Then you could run in this verbose mode while developing extensions, but not inflict the damage on your users. But that's a separate feature...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks for addressing my comments :)
…e. (llvm#109062) If your arguments or option values are of a type that naturally uses one of our common completion mechanisms, you will get completion for free. But if you have your own custom values or if you want to do fancy things like have `break set -s foo.dylib -n ba<TAB>` only complete on symbols in foo.dylib, you can use this new mechanism to achieve that. (cherry picked from commit 04b443e)
If your arguments or option values are of a type that naturally uses one of our common completion mechanisms, you will get completion for free. But if you have your own custom values or if you want to do fancy things like have
break set -s foo.dylib -n ba<TAB>
only complete on symbols in foo.dylib, you can use this new mechanism to achieve that.