diff options
Diffstat (limited to 'lldb')
-rw-r--r-- | lldb/bindings/python/python-wrapper.swig | 73 | ||||
-rw-r--r-- | lldb/docs/use/python-reference.rst | 185 | ||||
-rw-r--r-- | lldb/examples/python/cmdtemplate.py | 15 | ||||
-rw-r--r-- | lldb/examples/python/templates/parsed_cmd.py | 97 | ||||
-rw-r--r-- | lldb/include/lldb/Interpreter/ScriptInterpreter.h | 14 | ||||
-rw-r--r-- | lldb/include/lldb/Utility/CompletionRequest.h | 2 | ||||
-rw-r--r-- | lldb/source/Commands/CommandObjectCommands.cpp | 191 | ||||
-rw-r--r-- | lldb/source/Interpreter/Options.cpp | 5 | ||||
-rw-r--r-- | lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h | 9 | ||||
-rw-r--r-- | lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp | 40 | ||||
-rw-r--r-- | lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h | 8 | ||||
-rw-r--r-- | lldb/test/API/commands/command/script/add/TestAddParsedCommand.py | 132 | ||||
-rw-r--r-- | lldb/test/API/commands/command/script/add/test_commands.py | 69 | ||||
-rw-r--r-- | lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp | 13 |
14 files changed, 771 insertions, 82 deletions
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig index 961fb2d..b72a462 100644 --- a/lldb/bindings/python/python-wrapper.swig +++ b/lldb/bindings/python/python-wrapper.swig @@ -667,6 +667,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 b12048f..95a6020 100644 --- a/lldb/docs/use/python-reference.rst +++ b/lldb/docs/use/python-reference.rst @@ -551,7 +551,7 @@ command definition form can't do the right thing. Since lldb 3.7, Python commands can also be implemented by means of a class which should implement the following interface: -:: +.. code-block:: python class CommandObjectType: def __init__(self, debugger, internal_dict): @@ -586,20 +586,193 @@ which should implement the following interface: As a convenience, you can treat the result object as a Python file object, and say -:: +.. code-block:: python print >>result, "my command does lots of cool stuff" 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 up the command definition, implement the ParsedCommand abstract method: + +.. code-block:: python + + 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: + +.. code-block:: python + + 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: + +.. code-block:: python + + 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: + +.. code-block:: python + + 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: + +.. code-block:: python + def was_set(self, long_option_name): + +returns True if the option was specified on the command line. + +.. code-block:: python + + 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: + +.. code-block:: python + + def handle_option_argument_completion(self, long_option, cursor_pos): + """ + long_option: The long option name of the option whose value you are + asked to complete. + cursor_pos: The cursor position in the value for that option - which + you can get from the option parser. + """ + +And to handle the completion of arguments: + +.. code-block:: python + + def handle_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 either of these API's is called, the command line will have 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. + +For instance, if the string you are completing is "Test" and the available completions are: +"Test1", "Test11" and "Test111", you should return the dictionary: + +.. code-block:: python + + return {"completion": "Test1", "mode" : "partial"} + +and then lldb will add the "1" at the curson and advance it after the added string, +waiting for more completions. But if "Test1" is the only completion, return: + +.. code-block:: python + + {"completion": "Test1", "mode": "complete"} + +and lldb will add "1 " at the cursor, indicating the command string is complete. + +The default is "complete", you don't need to specify a "mode" in that case. + +- 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: -:: +.. code-block python def __lldb_init_module(debugger, internal_dict): # Command Initialization code goes here @@ -615,7 +788,7 @@ creating scripts that can be run from the command line. However, for command line scripts, the debugger instance must be created manually. Sample code would look like: -:: +.. code-block:: python if __name__ == '__main__': # Initialize the debugger before making any API calls. @@ -638,7 +811,7 @@ look like: Now we can create a module called ls.py in the file ~/ls.py that will implement a function that can be used by LLDB's python command code: -:: +.. code-block:: python #!/usr/bin/env python diff --git a/lldb/examples/python/cmdtemplate.py b/lldb/examples/python/cmdtemplate.py index b6a21cb..a9fbe0b 100644 --- a/lldb/examples/python/cmdtemplate.py +++ b/lldb/examples/python/cmdtemplate.py @@ -29,8 +29,8 @@ class FrameStatCommand(ParsedCommand): return lldb.eCommandRequiresFrame | lldb.eCommandProcessMustBePaused 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", @@ -39,7 +39,7 @@ class FrameStatCommand(ParsedCommand): default = True, ) - self.ov_parser.add_option( + ov_parser.add_option( "i", "in-scope", help = "in_scope_only = True", @@ -48,7 +48,7 @@ class FrameStatCommand(ParsedCommand): default=True, ) - self.ov_parser.add_option( + ov_parser.add_option( "a", "arguments", help = "arguments = True", @@ -57,7 +57,7 @@ class FrameStatCommand(ParsedCommand): default = True, ) - self.ov_parser.add_option( + ov_parser.add_option( "l", "locals", help = "locals = True", @@ -66,7 +66,7 @@ class FrameStatCommand(ParsedCommand): default = True, ) - self.ov_parser.add_option( + ov_parser.add_option( "s", "statics", help = "statics = True", @@ -103,8 +103,9 @@ class FrameStatCommand(ParsedCommand): 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 06124ad..13d6eae 100644 --- a/lldb/examples/python/templates/parsed_cmd.py +++ b/lldb/examples/python/templates/parsed_cmd.py @@ -4,7 +4,8 @@ lldb parsed commands more Pythonic. The way to use it is to make a class for your command that inherits from ParsedCommandBase. That will make an LLDBOptionValueParser which you will use for your option definition, and to fetch option values for the current invocation -of your command. Access to the OV parser is through: +of your command. For concision, I'll call this the `OVParser`. +Access to the `OVParser` is through: ParsedCommandBase.get_parser() @@ -43,7 +44,65 @@ will fetch the value, and: 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` method: `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 `OVParser` 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 """ @@ -226,10 +285,14 @@ class LLDBOptionValueParser: return True 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. """ + """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. + 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 +302,16 @@ class LLDBOptionValueParser: 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,14 +324,16 @@ class LLDBOptionValueParser: 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. + only one of the enum elements is allowed. The value will be the + element_name for the chosen enum element as a string. """ if not dest: dest = long_option diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h index 901ecf3..2c2bd6f 100644 --- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h +++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h @@ -420,6 +420,20 @@ public: 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, Status &error) { diff --git a/lldb/include/lldb/Utility/CompletionRequest.h b/lldb/include/lldb/Utility/CompletionRequest.h index 1a2b1d6..650158a 100644 --- a/lldb/include/lldb/Utility/CompletionRequest.h +++ b/lldb/include/lldb/Utility/CompletionRequest.h @@ -139,6 +139,8 @@ public: return GetParsedLine()[GetCursorIndex()]; } + size_t GetCursorCharPos() const { return m_cursor_char_position; } + /// Drops the first argument from the argument list. void ShiftArguments() { m_cursor_index--; diff --git a/lldb/source/Commands/CommandObjectCommands.cpp b/lldb/source/Commands/CommandObjectCommands.cpp index e3291640..845b89a 100644 --- a/lldb/source/Commands/CommandObjectCommands.cpp +++ b/lldb/source/Commands/CommandObjectCommands.cpp @@ -1637,6 +1637,129 @@ private: size_t GetNumOptions() { return m_num_options; } + void PrepareOptionsForCompletion(CompletionRequest &request, + OptionElementVector &option_vec, + ExecutionContext *exe_ctx) { + // I'm not sure if we'll get into trouble doing an option parsing start + // and end in this context. If so, then I'll have to directly tell the + // scripter to do this. + OptionParsingStarting(exe_ctx); + auto opt_defs = GetDefinitions(); + + // Iterate through the options we found so far, and push them into + // the scripted side. + for (auto option_elem : option_vec) { + int cur_defs_index = option_elem.opt_defs_index; + // If we don't recognize this option we can't set it. + if (cur_defs_index == OptionArgElement::eUnrecognizedArg || + cur_defs_index == OptionArgElement::eBareDash || + cur_defs_index == OptionArgElement::eBareDoubleDash) + continue; + bool option_has_arg = opt_defs[cur_defs_index].option_has_arg; + llvm::StringRef cur_arg_value; + if (option_has_arg) { + int cur_arg_pos = option_elem.opt_arg_pos; + if (cur_arg_pos != OptionArgElement::eUnrecognizedArg && + cur_arg_pos != OptionArgElement::eBareDash && + cur_arg_pos != OptionArgElement::eBareDoubleDash) { + cur_arg_value = + request.GetParsedLine().GetArgumentAtIndex(cur_arg_pos); + } + } + SetOptionValue(cur_defs_index, cur_arg_value, exe_ctx); + } + OptionParsingFinished(exe_ctx); + } + + void + ProcessCompletionDict(CompletionRequest &request, + StructuredData::DictionarySP &completion_dict_sp) { + // We don't know how to process an empty completion dict, our callers have + // to do that. + assert(completion_dict_sp && "Must have valid completion dict"); + // First handle the case of a single completion: + llvm::StringRef completion; + // If the dictionary has one element "no-completion" then we return here + if (completion_dict_sp->GetValueForKeyAsString("no-completion", + completion)) + return; + + if (completion_dict_sp->GetValueForKeyAsString("completion", + completion)) { + llvm::StringRef mode_str; + CompletionMode mode = CompletionMode::Normal; + if (completion_dict_sp->GetValueForKeyAsString("mode", mode_str)) { + if (mode_str == "complete") + mode = CompletionMode::Normal; + else if (mode_str == "partial") + mode = CompletionMode::Partial; + else { + // FIXME - how do I report errors here? + return; + } + } + request.AddCompletion(completion, "", mode); + return; + } + // The completions are required, the descriptions are not: + StructuredData::Array *completions; + StructuredData::Array *descriptions; + if (completion_dict_sp->GetValueForKeyAsArray("values", completions)) { + completion_dict_sp->GetValueForKeyAsArray("descriptions", descriptions); + size_t num_completions = completions->GetSize(); + for (size_t idx = 0; idx < num_completions; idx++) { + auto val = completions->GetItemAtIndexAsString(idx); + if (!val) + // FIXME: How do I report this error? + return; + + if (descriptions) { + auto desc = descriptions->GetItemAtIndexAsString(idx); + request.AddCompletion(*val, desc ? *desc : ""); + } else + request.AddCompletion(*val); + } + } + } + + void + HandleOptionArgumentCompletion(lldb_private::CompletionRequest &request, + OptionElementVector &option_vec, + int opt_element_index, + CommandInterpreter &interpreter) override { + ScriptInterpreter *scripter = + interpreter.GetDebugger().GetScriptInterpreter(); + + if (!scripter) + return; + + ExecutionContext exe_ctx = interpreter.GetExecutionContext(); + PrepareOptionsForCompletion(request, option_vec, &exe_ctx); + + auto defs = GetDefinitions(); + + size_t defs_index = option_vec[opt_element_index].opt_defs_index; + llvm::StringRef option_name = defs[defs_index].long_option; + bool is_enum = defs[defs_index].enum_values.size() != 0; + if (option_name.empty()) + return; + // If this is an enum, we don't call the custom completer, just let the + // regular option completer handle that: + StructuredData::DictionarySP completion_dict_sp; + if (!is_enum) + completion_dict_sp = + scripter->HandleOptionArgumentCompletionForScriptedCommand( + m_cmd_obj_sp, option_name, request.GetCursorCharPos()); + + if (!completion_dict_sp) { + Options::HandleOptionArgumentCompletion(request, option_vec, + opt_element_index, interpreter); + return; + } + + ProcessCompletionDict(request, completion_dict_sp); + } + private: struct EnumValueStorage { EnumValueStorage() { @@ -1878,6 +2001,74 @@ public: Status GetArgsError() { return m_args_error.Clone(); } bool WantsCompletion() override { return true; } +private: + void PrepareOptionsForCompletion(CompletionRequest &request, + OptionElementVector &option_vec) { + // First, we have to tell the Scripted side to set the values in its + // option store, then we call into the handle_completion passing in + // an array of the args, the arg index and the cursor position in the arg. + // We want the script side to have a chance to clear its state, so tell + // it argument parsing has started: + Options *options = GetOptions(); + // If there are not options, this will be nullptr, and in that case we + // can just skip setting the options on the scripted side: + if (options) + m_options.PrepareOptionsForCompletion(request, option_vec, &m_exe_ctx); + } + +public: + void HandleArgumentCompletion(CompletionRequest &request, + OptionElementVector &option_vec) override { + ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter(); + + if (!scripter) + return; + + // Set up the options values on the scripted side: + PrepareOptionsForCompletion(request, option_vec); + + // Now we have to make up the argument list. + // The ParseForCompletion only identifies tokens in the m_parsed_line + // it doesn't remove the options leaving only the args as it does for + // the regular Parse, so we have to filter out the option ones using the + // option_element_vector: + + Options *options = GetOptions(); + auto defs = options->GetDefinitions(); + + std::unordered_set<size_t> option_slots; + for (const auto &elem : option_vec) { + if (elem.opt_defs_index == -1) + continue; + option_slots.insert(elem.opt_pos); + if (defs[elem.opt_defs_index].option_has_arg) + option_slots.insert(elem.opt_arg_pos); + } + + std::vector<llvm::StringRef> args_vec; + Args &args = request.GetParsedLine(); + size_t num_args = args.GetArgumentCount(); + size_t cursor_idx = request.GetCursorIndex(); + size_t args_elem_pos = cursor_idx; + + for (size_t idx = 0; idx < num_args; idx++) { + if (option_slots.count(idx) == 0) + args_vec.push_back(args[idx].ref()); + else if (idx < cursor_idx) + args_elem_pos--; + } + StructuredData::DictionarySP completion_dict_sp = + scripter->HandleArgumentCompletionForScriptedCommand( + m_cmd_obj_sp, args_vec, args_elem_pos, request.GetCursorCharPos()); + + if (!completion_dict_sp) { + CommandObject::HandleArgumentCompletion(request, option_vec); + return; + } + + m_options.ProcessCompletionDict(request, completion_dict_sp); + } + bool IsRemovable() const override { return true; } ScriptedCommandSynchronicity GetSynchronicity() { return m_synchro; } diff --git a/lldb/source/Interpreter/Options.cpp b/lldb/source/Interpreter/Options.cpp index b8a3f68a..3888a58 100644 --- a/lldb/source/Interpreter/Options.cpp +++ b/lldb/source/Interpreter/Options.cpp @@ -661,7 +661,9 @@ bool Options::HandleOptionCompletion(CompletionRequest &request, } else if (opt_arg_pos == request.GetCursorIndex()) { // Okay the cursor is on the completion of an argument. See if it has a - // completion, otherwise return no matches. + // completion, otherwise return no matches. Note, opt_defs_index == -1 + // means we're after an option, but that option doesn't exist. We'll + // end up treating that as an argument. Not sure we can do much better. if (opt_defs_index != -1) { HandleOptionArgumentCompletion(request, opt_element_vector, i, interpreter); @@ -688,7 +690,6 @@ void Options::HandleOptionArgumentCompletion( int opt_defs_index = opt_element_vector[opt_element_index].opt_defs_index; // See if this is an enumeration type option, and if so complete it here: - const auto &enum_values = opt_defs[opt_defs_index].enum_values; if (!enum_values.empty()) for (const auto &enum_value : enum_values) diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h index 81ee9ea0..518a478 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h @@ -200,6 +200,15 @@ public: LLDBSwigPythonGetRepeatCommandForScriptedCommand(PyObject *implementor, std::string &command); + static StructuredData::DictionarySP + LLDBSwigPythonHandleArgumentCompletionForScriptedCommand( + PyObject *implementor, std::vector<llvm::StringRef> &args_impl, + size_t args_pos, size_t pos_in_arg); + + static StructuredData::DictionarySP + LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand( + PyObject *implementor, llvm::StringRef &long_option, size_t pos_in_arg); + static bool LLDBSwigPythonCallModuleInit(const char *python_module_name, const char *session_dictionary_name, lldb::DebuggerSP debugger); diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp index 155efc0..db1a10e 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp +++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp @@ -2720,6 +2720,46 @@ ScriptInterpreterPythonImpl::GetRepeatCommandForScriptedCommand( return ret_val; } +StructuredData::DictionarySP +ScriptInterpreterPythonImpl::HandleArgumentCompletionForScriptedCommand( + StructuredData::GenericSP impl_obj_sp, std::vector<llvm::StringRef> &args, + size_t args_pos, size_t char_in_arg) { + StructuredData::DictionarySP completion_dict_sp; + if (!impl_obj_sp || !impl_obj_sp->IsValid()) + return completion_dict_sp; + + { + Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, + Locker::FreeLock); + + completion_dict_sp = + SWIGBridge::LLDBSwigPythonHandleArgumentCompletionForScriptedCommand( + static_cast<PyObject *>(impl_obj_sp->GetValue()), args, args_pos, + char_in_arg); + } + return completion_dict_sp; +} + +StructuredData::DictionarySP +ScriptInterpreterPythonImpl::HandleOptionArgumentCompletionForScriptedCommand( + StructuredData::GenericSP impl_obj_sp, llvm::StringRef &long_option, + size_t char_in_arg) { + StructuredData::DictionarySP completion_dict_sp; + if (!impl_obj_sp || !impl_obj_sp->IsValid()) + return completion_dict_sp; + + { + Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, + Locker::FreeLock); + + completion_dict_sp = SWIGBridge:: + LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand( + static_cast<PyObject *>(impl_obj_sp->GetValue()), long_option, + char_in_arg); + } + return completion_dict_sp; +} + /// In Python, a special attribute __doc__ contains the docstring for an object /// (function, method, class, ...) if any is defined Otherwise, the attribute's /// value is None. diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h index d15e2fd..2dc7847 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h @@ -166,6 +166,14 @@ public: GetRepeatCommandForScriptedCommand(StructuredData::GenericSP impl_obj_sp, Args &args) override; + StructuredData::DictionarySP HandleArgumentCompletionForScriptedCommand( + StructuredData::GenericSP impl_obj_sp, std::vector<llvm::StringRef> &args, + size_t args_pos, size_t char_in_arg) override; + + StructuredData::DictionarySP HandleOptionArgumentCompletionForScriptedCommand( + StructuredData::GenericSP impl_obj_sp, llvm::StringRef &long_options, + size_t char_in_arg) override; + Status GenerateFunction(const char *signature, const StringList &input, bool is_callback) override; diff --git a/lldb/test/API/commands/command/script/add/TestAddParsedCommand.py b/lldb/test/API/commands/command/script/add/TestAddParsedCommand.py index c7680e9..6fac1eb 100644 --- a/lldb/test/API/commands/command/script/add/TestAddParsedCommand.py +++ b/lldb/test/API/commands/command/script/add/TestAddParsedCommand.py @@ -68,6 +68,57 @@ class ParsedCommandTestCase(TestBase): return results + def handle_completion( + self, + cmd_str, + exp_num_completions, + exp_matches, + exp_descriptions, + match_description, + ): + matches = lldb.SBStringList() + descriptions = lldb.SBStringList() + + interp = self.dbg.GetCommandInterpreter() + num_completions = interp.HandleCompletionWithDescriptions( + cmd_str, len(cmd_str), 0, 1000, matches, descriptions + ) + self.assertEqual( + num_completions, exp_num_completions, "Number of completions is right." + ) + num_matches = matches.GetSize() + self.assertEqual( + num_matches, + exp_matches.GetSize(), + "matches and expected matches of different lengths", + ) + num_descriptions = descriptions.GetSize() + if match_description: + self.assertEqual( + num_descriptions, + exp_descriptions.GetSize(), + "descriptions and expected of different lengths", + ) + + self.assertEqual( + matches.GetSize(), + num_completions + 1, + "The first element is the complete additional text", + ) + + for idx in range(0, num_matches): + match = matches.GetStringAtIndex(idx) + exp_match = exp_matches.GetStringAtIndex(idx) + self.assertEqual( + match, exp_match, f"{match} did not match expectation: {exp_match}" + ) + if match_description: + desc = descriptions.GetStringAtIndex(idx) + exp_desc = exp_descriptions.GetStringAtIndex(idx) + self.assertEqual( + desc, exp_desc, f"{desc} didn't match expectation: {exp_desc}" + ) + def pycmd_tests(self): source_dir = self.getSourceDir() test_file_path = os.path.join(source_dir, "test_commands.py") @@ -176,24 +227,10 @@ class ParsedCommandTestCase(TestBase): descriptions = lldb.SBStringList() # First try an enum completion: - num_completions = interp.HandleCompletionWithDescriptions( - "no-args -e f", 12, 0, 1000, matches, descriptions - ) - self.assertEqual(num_completions, 1, "Only one completion for foo") - self.assertEqual( - matches.GetSize(), 2, "The first element is the complete additional text" - ) - self.assertEqual( - matches.GetStringAtIndex(0), "oo ", "And we got the right extra characters" - ) - self.assertEqual( - matches.GetStringAtIndex(1), "foo", "And we got the right match" - ) - self.assertEqual( - descriptions.GetSize(), 2, "descriptions matche the return length" - ) - # FIXME: we don't return descriptions for enum elements - # self.assertEqual(descriptions.GetStringAtIndex(1), "does foo things", "And we got the right description") + # Note - this is an enum so all the values are returned: + matches.AppendList(["oo ", "foo"], 2) + + self.handle_completion("no-args -e f", 1, matches, descriptions, False) # Now try an internal completer, the on disk file one is handy: partial_name = os.path.join(source_dir, "test_") @@ -201,24 +238,9 @@ class ParsedCommandTestCase(TestBase): matches.Clear() descriptions.Clear() - num_completions = interp.HandleCompletionWithDescriptions( - cmd_str, len(cmd_str) - 1, 0, 1000, matches, descriptions - ) - self.assertEqual(num_completions, 1, "Only one completion for source file") - self.assertEqual(matches.GetSize(), 2, "The first element is the complete line") - self.assertEqual( - matches.GetStringAtIndex(0), - "commands.py' ", - "And we got the right extra characters", - ) - self.assertEqual( - matches.GetStringAtIndex(1), test_file_path, "And we got the right match" - ) - self.assertEqual( - descriptions.GetSize(), 2, "descriptions match the return length" - ) - # FIXME: we don't return descriptions for enum elements - # self.assertEqual(descriptions.GetStringAtIndex(1), "does foo things", "And we got the right description") + matches.AppendList(["commands.py' ", test_file_path], 2) + # We don't have descriptions for the file path completer: + self.handle_completion(cmd_str, 1, matches, descriptions, False) # Try a command with arguments. # FIXME: It should be enough to define an argument and it's type to get the completer @@ -231,6 +253,44 @@ class ParsedCommandTestCase(TestBase): substrs=["0: First Argument", "1: Second Argument"], ) + # Now test custom completions - two-args has both option and arg completers. In both + # completers we return different values if the -p option is set, so we can test that too: + matches.Clear() + descriptions.Clear() + cmd_str = "two-args -p something -c other_" + matches.AppendString("something ") + matches.AppendString("other_something") + # This is a full match so no descriptions: + self.handle_completion(cmd_str, 1, matches, descriptions, False) + + matches.Clear() + descriptions.Clear() + cmd_str = "two-args -c other_" + matches.AppendList(["", "other_nice", "other_not_nice", "other_mediocre"], 4) + # The option doesn't return descriptions either: + self.handle_completion(cmd_str, 3, matches, descriptions, False) + + # Now try the argument - it says "no completions" if the proc_name was set: + matches.Clear() + descriptions.Clear() + cmd_str = "two-args -p something arg" + matches.AppendString("") + self.handle_completion(cmd_str, 0, matches, descriptions, False) + + cmd_str = "two-args arg_" + matches.Clear() + descriptions.Clear() + matches.AppendList(["", "arg_cool", "arg_yuck"], 3) + descriptions.AppendList(["", "good idea", "bad idea"], 3) + self.handle_completion(cmd_str, 2, matches, descriptions, True) + + # This one gets a single unique match: + cmd_str = "two-args correct_" + matches.Clear() + descriptions.Clear() + matches.AppendList(["answer ", "correct_answer"], 2) + self.handle_completion(cmd_str, 1, matches, descriptions, False) + # Now make sure get_repeat_command works properly: # no-args turns off auto-repeat diff --git a/lldb/test/API/commands/command/script/add/test_commands.py b/lldb/test/API/commands/command/script/add/test_commands.py index fcde6cd..b15ea93 100644 --- a/lldb/test/API/commands/command/script/add/test_commands.py +++ b/lldb/test/API/commands/command/script/add/test_commands.py @@ -18,7 +18,7 @@ class ReportingCmd(ParsedCommand): for long_option, elem in opt_def.items(): dest = elem["dest"] result.AppendMessage( - f"{long_option} (set: {elem['_value_set']}): {object.__getattribute__(self.ov_parser, dest)}\n" + f"{long_option} (set: {elem['_value_set']}): {object.__getattribute__(self.get_parser(), dest)}\n" ) else: result.AppendMessage("No options\n") @@ -31,7 +31,6 @@ class ReportingCmd(ParsedCommand): f"{idx}: {args_array.GetItemAtIndex(idx).GetStringValue(10000)}\n" ) - # Use these to make sure that get_repeat_command sends the right # command. no_args_repeat = None @@ -49,7 +48,8 @@ class NoArgsCommand(ReportingCmd): ParsedCommand.do_register_cmd(cls, debugger, module_name) def setup_command_definition(self): - self.ov_parser.add_option( + ov_parser = self.get_parser() + ov_parser.add_option( "b", "bool-arg", "a boolean arg, defaults to True", @@ -59,7 +59,7 @@ class NoArgsCommand(ReportingCmd): default=True, ) - self.ov_parser.add_option( + ov_parser.add_option( "s", "shlib-name", "A shared library name.", @@ -69,7 +69,7 @@ class NoArgsCommand(ReportingCmd): default=None, ) - self.ov_parser.add_option( + ov_parser.add_option( "d", "disk-file-name", "An on disk filename", @@ -78,7 +78,7 @@ class NoArgsCommand(ReportingCmd): default=None, ) - self.ov_parser.add_option( + ov_parser.add_option( "l", "line-num", "A line number", @@ -88,7 +88,7 @@ class NoArgsCommand(ReportingCmd): default=0, ) - self.ov_parser.add_option( + ov_parser.add_option( "e", "enum-option", "An enum, doesn't actually do anything", @@ -126,8 +126,9 @@ class OneArgCommandNoOptions(ReportingCmd): ParsedCommand.do_register_cmd(cls, debugger, module_name) def setup_command_definition(self): - self.ov_parser.add_argument_set( - [self.ov_parser.make_argument_element(lldb.eArgTypeSourceFile, "plain")] + ov_parser = self.get_parser() + ov_parser.add_argument_set( + [ov_parser.make_argument_element(lldb.eArgTypeSourceFile, "plain")] ) def get_repeat_command(self, command): @@ -154,7 +155,8 @@ class TwoArgGroupsCommand(ReportingCmd): ParsedCommand.do_register_cmd(cls, debugger, module_name) def setup_command_definition(self): - self.ov_parser.add_option( + ov_parser = self.get_parser() + ov_parser.add_option( "l", "language", "language defaults to None", @@ -164,7 +166,7 @@ class TwoArgGroupsCommand(ReportingCmd): default=None, ) - self.ov_parser.add_option( + ov_parser.add_option( "c", "log-channel", "log channel - defaults to lldb", @@ -174,7 +176,7 @@ class TwoArgGroupsCommand(ReportingCmd): default="lldb", ) - self.ov_parser.add_option( + ov_parser.add_option( "p", "process-name", "A process name, defaults to None", @@ -183,25 +185,23 @@ class TwoArgGroupsCommand(ReportingCmd): default=None, ) - self.ov_parser.add_argument_set( + ov_parser.add_argument_set( [ - self.ov_parser.make_argument_element( + ov_parser.make_argument_element( lldb.eArgTypeClassName, "plain", [1, 2] ), - self.ov_parser.make_argument_element( + ov_parser.make_argument_element( lldb.eArgTypeOffset, "optional", [1, 2] ), ] ) - self.ov_parser.add_argument_set( + ov_parser.add_argument_set( [ - self.ov_parser.make_argument_element( + ov_parser.make_argument_element( lldb.eArgTypePythonClass, "plain", [3, 4] ), - self.ov_parser.make_argument_element( - lldb.eArgTypePid, "optional", [3, 4] - ), + ov_parser.make_argument_element(lldb.eArgTypePid, "optional", [3, 4]), ] ) @@ -210,6 +210,35 @@ class TwoArgGroupsCommand(ReportingCmd): two_arg_repeat = command return command + " THIRD_ARG" + def handle_option_argument_completion(self, long_option, cursor_pos): + ov_parser = self.get_parser() + value = ov_parser.dest_for_option(long_option)[0 : cursor_pos + 1] + proc_value = ov_parser.proc_name + if proc_value != None: + new_str = value + proc_value + ret_arr = {"completion": new_str, "mode": "partial"} + return ret_arr + + ret_arr = {"values": [value + "nice", value + "not_nice", value + "mediocre"]} + return ret_arr + + def handle_argument_completion(self, args, arg_pos, cursor_pos): + ov_parser = self.get_parser() + orig_arg = args[arg_pos][0:cursor_pos] + if orig_arg == "correct_": + ret_arr = {"completion": "correct_answer"} + return ret_arr + + if ov_parser.was_set("process-name"): + # No completions if proc_name was set. + return True + + ret_arr = { + "values": [orig_arg + "cool", orig_arg + "yuck"], + "descriptions": ["good idea", "bad idea"], + } + return ret_arr + def get_short_help(self): return "This is my short help string" diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp index c67a2b4..3faeb58 100644 --- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp +++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp @@ -211,6 +211,19 @@ LLDBSwigPythonGetRepeatCommandForScriptedCommand(PyObject *implementor, return std::nullopt; } +StructuredData::DictionarySP +LLDBSwigPythonHandleArgumentCompletionForScriptedCommand( + PyObject *implementor, std::vector<llvm::StringRef> &args, size_t args_pos, + size_t pos_in_arg) { + return {}; +} + +StructuredData::DictionarySP +LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand( + PyObject *implementor, llvm::StringRef &long_options, size_t char_in_arg) { + return {}; +} + bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallModuleInit( const char *python_module_name, const char *session_dictionary_name, lldb::DebuggerSP debugger) { |