diff options
| author | 0cyn <kat@vector35.com> | 2026-04-14 05:03:15 -0400 |
|---|---|---|
| committer | Alexander Taylor <alex@vector35.com> | 2026-05-18 14:55:52 -0400 |
| commit | 297a98bff289ecde90d808f6538c2c5edf804af6 (patch) | |
| tree | 4ca5f9868354cb26f7c6ef70140e4ed0f0395944 | |
| parent | f1a688a1e7fb6d962d8c9addaaefdfd59e5dd28b (diff) | |
Argument Assist Popup
| -rw-r--r-- | binaryninjaapi.h | 6 | ||||
| -rw-r--r-- | binaryninjacore.h | 8 | ||||
| -rw-r--r-- | python/bncompleter.py | 415 | ||||
| -rw-r--r-- | python/scriptingprovider.py | 59 | ||||
| -rw-r--r-- | scriptingprovider.cpp | 60 | ||||
| -rw-r--r-- | ui/scriptingconsole.h | 62 | ||||
| -rw-r--r-- | ui/uitypes.h | 4 |
7 files changed, 609 insertions, 5 deletions
diff --git a/binaryninjaapi.h b/binaryninjaapi.h index c48a5a6c..249d5ae6 100644 --- a/binaryninjaapi.h +++ b/binaryninjaapi.h @@ -18938,6 +18938,8 @@ namespace BinaryNinja { static void SetCurrentSelectionCallback(void* ctxt, uint64_t begin, uint64_t end); static char* CompleteInputCallback(void* ctxt, const char* text, uint64_t state); static void StopCallback(void* ctxt); + static bool CanCompleteArgumentsCallback(void* ctxt, const char* text); + static char* CompleteArgumentsCallback(void* ctx, const char* text, uint64_t* argumentStart); virtual void DestroyInstance(); @@ -18953,6 +18955,8 @@ namespace BinaryNinja { virtual void SetCurrentSelection(uint64_t begin, uint64_t end); virtual std::string CompleteInput(const std::string& text, uint64_t state); virtual void Stop(); + virtual bool CanCompleteArguments(const std::string& text); + virtual std::pair<std::string, uint64_t> CompleteArguments(const std::string& text); void Output(const std::string& text); void Warning(const std::string& text); @@ -18987,6 +18991,8 @@ namespace BinaryNinja { virtual void SetCurrentSelection(uint64_t begin, uint64_t end) override; virtual std::string CompleteInput(const std::string& text, uint64_t state) override; virtual void Stop() override; + virtual bool CanCompleteArguments(const std::string& text) override; + virtual std::pair<std::string, uint64_t> CompleteArguments(const std::string& text) override; }; /*! diff --git a/binaryninjacore.h b/binaryninjacore.h index 61b7798c..0f820e06 100644 --- a/binaryninjacore.h +++ b/binaryninjacore.h @@ -37,14 +37,14 @@ // Current ABI version for linking to the core. This is incremented any time // there are changes to the API that affect linking, including new functions, // new types, or modifications to existing functions or types. -#define BN_CURRENT_CORE_ABI_VERSION 167 +#define BN_CURRENT_CORE_ABI_VERSION 168 // Minimum ABI version that is supported for loading of plugins. Plugins that // are linked to an ABI version less than this will not be able to load and // will require rebuilding. The minimum version is increased when there are // incompatible changes that break binary compatibility, such as changes to // existing types or functions. -#define BN_MINIMUM_CORE_ABI_VERSION 166 +#define BN_MINIMUM_CORE_ABI_VERSION 168 #ifdef __GNUC__ #ifdef BINARYNINJACORE_LIBRARY @@ -3122,6 +3122,8 @@ extern "C" void (*setCurrentSelection)(void* ctxt, uint64_t begin, uint64_t end); char* (*completeInput)(void* ctxt, const char* text, uint64_t state); void (*stop)(void* ctxt); + bool (*canCompleteArguments)(void* ctx, const char* text); + char* (*completeArguments)(void* ctxt, const char* text, uint64_t* argumentStart); } BNScriptingInstanceCallbacks; typedef struct BNScriptingProviderCallbacks @@ -7841,6 +7843,8 @@ extern "C" BNScriptingInstance* instance, uint64_t begin, uint64_t end); BINARYNINJACOREAPI char* BNScriptingInstanceCompleteInput( BNScriptingInstance* instance, const char* text, uint64_t state); + BINARYNINJACOREAPI bool BNScriptingInstanceCanCompleteArguments(BNScriptingInstance* instance, const char* text); + BINARYNINJACOREAPI char* BNScriptingInstanceCompleteArguments(BNScriptingInstance* instance, const char* text, uint64_t* argumentStart); BINARYNINJACOREAPI void BNStopScriptingInstance(BNScriptingInstance* instance); BINARYNINJACOREAPI size_t BNFuzzyMatchSingle(const char* target, const char* query); diff --git a/python/bncompleter.py b/python/bncompleter.py index 66bf1763..f8eb46a7 100644 --- a/python/bncompleter.py +++ b/python/bncompleter.py @@ -41,6 +41,11 @@ Notes: """ import atexit +import html +import io +import tokenize +import typing + import binaryninja import __main__ import inspect @@ -67,6 +72,17 @@ def fnsignature(obj): return sig +def _format_annotation(annotation): + if isinstance(annotation, str): + return annotation + if isinstance(annotation, typing.ForwardRef): + return annotation.__forward_arg__ + try: + return inspect.formatannotation(annotation) + except Exception: + return repr(annotation) + + class Completer: def __init__(self, namespace=None): """Create a new completer for the command line. @@ -95,6 +111,405 @@ class Completer: self.use_main_ns = 0 self.namespace = namespace + def _resolve_callable(self, callable_path: str): + if self.use_main_ns: + self.namespace = __main__.__dict__ + namespace = self.namespace + parts = callable_path.split(".") + + if not parts: + return None + + function_obj = namespace.get(parts[0]) + if function_obj is None: + if hasattr(__builtins__, '__dict__'): + function_obj = __builtins__.__dict__.get(parts[0]) + else: + function_obj = __builtins__.get(parts[0]) + if function_obj is None: + return None + + for attr in parts[1:]: + try: + function_obj = getattr(function_obj, attr) + except Exception: + return None + + return function_obj + + @staticmethod + def _split_call_arguments( + tokens: typing.List[tokenize.TokenInfo], opening_paren_index: int + ) -> typing.List[typing.List[tokenize.TokenInfo]]: + arguments = [] + current_argument = [] + depth = 0 + + for tok in tokens[opening_paren_index + 1:]: + if tok.type == tokenize.OP: + if tok.string in "([{": + depth += 1 + elif tok.string in ")]}": + if depth == 0: + break + depth -= 1 + elif tok.string == "," and depth == 0: + arguments.append(current_argument) + current_argument = [] + continue + + current_argument.append(tok) + + arguments.append(current_argument) + return arguments + + @staticmethod + def _keyword_argument_name( + argument_tokens: typing.List[tokenize.TokenInfo] + ) -> Optional[str]: + depth = 0 + + for index, tok in enumerate(argument_tokens): + if tok.type != tokenize.OP: + continue + + if tok.string in "([{": + depth += 1 + elif tok.string in ")]}": + if depth > 0: + depth -= 1 + elif tok.string == "=" and depth == 0: + if index == 1 and argument_tokens[0].type == tokenize.NAME: + return argument_tokens[0].string + return None + + return None + + @staticmethod + def _is_keyword_unpack_argument( + argument_tokens: typing.List[tokenize.TokenInfo] + ) -> bool: + return ( + len(argument_tokens) > 0 + and argument_tokens[0].type == tokenize.OP + and argument_tokens[0].string == "**" + ) + + @staticmethod + def _is_iterable_unpack_argument( + argument_tokens: typing.List[tokenize.TokenInfo] + ) -> bool: + return ( + len(argument_tokens) > 0 + and argument_tokens[0].type == tokenize.OP + and argument_tokens[0].string == "*" + ) + + @staticmethod + def _var_positional_parameter_index( + parameters: typing.List[inspect.Parameter] + ) -> Optional[int]: + return next( + ( + index for index, parameter in enumerate(parameters) + if parameter.kind == inspect.Parameter.VAR_POSITIONAL + ), + None, + ) + + @staticmethod + def _keyword_only_parameter_index( + parameters: typing.List[inspect.Parameter], used_keyword_parameters: typing.Set[str] + ) -> Optional[int]: + return next( + ( + index for index, parameter in enumerate(parameters) + if parameter.kind == inspect.Parameter.KEYWORD_ONLY + and parameter.name not in used_keyword_parameters + ), + None, + ) + + @staticmethod + def _keyword_parameter_index( + parameters: typing.List[inspect.Parameter], keyword_name: str + ) -> Optional[int]: + var_keyword_index = None + + for index, parameter in enumerate(parameters): + if parameter.kind == inspect.Parameter.VAR_KEYWORD: + var_keyword_index = index + elif ( + parameter.name == keyword_name + and parameter.kind + in { + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + } + ): + return index + + return var_keyword_index + + @staticmethod + def _positional_parameter_index( + parameters: typing.List[inspect.Parameter], + positional_argument_count: int, + used_keyword_parameters: typing.Set[str], + ) -> Optional[int]: + positional_parameter_count = 0 + var_positional_index = None + + for index, parameter in enumerate(parameters): + if parameter.kind == inspect.Parameter.VAR_POSITIONAL: + var_positional_index = index + elif parameter.kind in { + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + }: + if ( + parameter.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + and parameter.name in used_keyword_parameters + ): + continue + + if positional_parameter_count == positional_argument_count: + return index + positional_parameter_count += 1 + + return var_positional_index + + @classmethod + def _current_argument_index( + cls, + parameters: typing.List[inspect.Parameter], + arguments: typing.List[typing.List[tokenize.TokenInfo]], + ) -> Optional[int]: + positional_argument_count = 0 + used_keyword_parameters = set() + var_keyword_index = next( + ( + index for index, parameter in enumerate(parameters) + if parameter.kind == inspect.Parameter.VAR_KEYWORD + ), + None, + ) + seen_var_keyword_argument = False + seen_iterable_unpack_argument = False + + for argument_tokens in arguments[:-1]: + keyword_name = cls._keyword_argument_name(argument_tokens) + if keyword_name is not None: + parameter_index = cls._keyword_parameter_index(parameters, keyword_name) + if parameter_index is not None: + used_keyword_parameters.add(parameters[parameter_index].name) + if parameter_index == var_keyword_index: + seen_var_keyword_argument = True + elif cls._is_iterable_unpack_argument(argument_tokens): + seen_iterable_unpack_argument = True + elif not cls._is_keyword_unpack_argument(argument_tokens): + positional_argument_count += 1 + + current_argument = arguments[-1] + keyword_name = cls._keyword_argument_name(current_argument) + if keyword_name is not None: + return cls._keyword_parameter_index(parameters, keyword_name) + + if cls._is_keyword_unpack_argument(current_argument): + return var_keyword_index + if cls._is_iterable_unpack_argument(current_argument): + return cls._var_positional_parameter_index(parameters) + + if seen_var_keyword_argument: + return var_keyword_index + if seen_iterable_unpack_argument: + return cls._var_positional_parameter_index(parameters) + + parameter_index = cls._positional_parameter_index( + parameters, positional_argument_count, used_keyword_parameters + ) + if parameter_index is not None: + return parameter_index + + return cls._keyword_only_parameter_index(parameters, used_keyword_parameters) + + def _get_argument_completion_context( + self, text: str + ) -> typing.Optional[typing.Dict[str, typing.Any]]: + if "(" not in text: + return None + + line_offsets = [0] + for line in text.splitlines(keepends=True): + line_offsets.append(line_offsets[-1] + len(line)) + + def absolute_byte_index(position: typing.Tuple[int, int]) -> int: + line, column = position + return len(text[:line_offsets[line - 1] + column].encode("utf-8")) + + reader = io.StringIO(text).readline + stream = tokenize.generate_tokens(reader) + + tokens = [] + try: + for tok in stream: + if tok.type in { + tokenize.ENCODING, + tokenize.NL, + tokenize.NEWLINE, + tokenize.INDENT, + tokenize.DEDENT, + tokenize.ENDMARKER, + tokenize.COMMENT, + }: + continue + tokens.append(tok) + except tokenize.TokenError: + pass + + if not tokens: + return None + + stack = [] + for index, tok in enumerate(tokens): + if tok.type == tokenize.OP and tok.string in "([{": + callable_path = None + + if tok.string == "(": + i = index - 1 + parts = [] + + if i >= 0 and tokens[i].type == tokenize.NAME: + parts.append(tokens[i].string) + i -= 1 + + while i >= 1: + if ( + tokens[i].type == tokenize.OP + and tokens[i].string == "." + and tokens[i - 1].type == tokenize.NAME + ): + parts.append(tokens[i - 1].string) + i -= 2 + else: + break + + parts.reverse() + callable_path = ".".join(parts) + + stack.append( + { + "bracket": tok.string, + "token_index": index, + "callable_path": callable_path, + } + ) + elif tok.type == tokenize.OP and tok.string in ")]}": + if stack: + stack.pop() + + call_context = None + for entry in reversed(stack): + if entry["bracket"] == "(" and entry["callable_path"] is not None: + call_context = entry + break + + if call_context is None: + return None + + function_obj = self._resolve_callable(call_context["callable_path"]) + if function_obj is None: + return None + + try: + signature = inspect.signature(function_obj) + except Exception: + return None + + parameters = list(signature.parameters.values()) + + arguments = self._split_call_arguments(tokens, call_context["token_index"]) + + return { + "signature": signature, + "parameters": parameters, + "current_argument_index": self._current_argument_index(parameters, arguments), + "start_index": absolute_byte_index(tokens[call_context["token_index"]].end), + } + + def can_complete_arguments(self, text: str) -> bool: + """ + A faster check to see if argument assistance is even needed currently. + + :param text: + :return: + """ + return self._get_argument_completion_context(text) is not None + + def complete_arguments(self, text: str) -> typing.Tuple[Optional[str], int]: + """Given input up to the contents of 'text', return a HTML string containing + the arguments for the function. + + Used in UI to display and highlight arguments of a function as the user types them. + """ + context = self._get_argument_completion_context(text) + if context is None: + return None, 0 + + signature = context["signature"] + parameters = context["parameters"] + current_argument_index = context["current_argument_index"] + + return_args = [] + positional_only_count = sum( + 1 for p in parameters if p.kind == inspect.Parameter.POSITIONAL_ONLY + ) + + for i, parameter in enumerate(parameters): + if ( + parameter.kind == inspect.Parameter.KEYWORD_ONLY + and "*" not in return_args + and not any( + p.kind == inspect.Parameter.VAR_POSITIONAL + for p in parameters[:i] + ) + ): + return_args.append("*") + + if parameter.kind == inspect.Parameter.VAR_POSITIONAL: + arg = f"*{parameter.name}" + elif parameter.kind == inspect.Parameter.VAR_KEYWORD: + arg = f"**{parameter.name}" + else: + arg = parameter.name + + if i == current_argument_index: + arg_postfix = '' + if parameter.annotation is not inspect.Signature.empty: + annotation = _format_annotation(parameter.annotation) + arg_postfix += f": {annotation}" + + if parameter.default is not inspect.Signature.empty: + arg_postfix += f" = {parameter.default!r}" + arg = html.escape(arg) + arg_postfix = html.escape(arg_postfix) + arg = f'<span class="currentArgument"><b>{arg}</b>{arg_postfix}</span>' + else: + arg = html.escape(arg) + + return_args.append(arg) + + if positional_only_count > 0 and i + 1 == positional_only_count: + return_args.append("/") + + result = ", ".join(return_args) + + if signature.return_annotation is not inspect.Signature.empty: + return_annotation = _format_annotation(signature.return_annotation) + result += f'<span class="returnType"> -> {html.escape(return_annotation)}</span>' + + return result, context["start_index"] + def complete(self, text: str, state) -> Optional[str]: """Return the next possible completion for 'text'. diff --git a/python/scriptingprovider.py b/python/scriptingprovider.py index 8a541422..b1f83ec4 100644 --- a/python/scriptingprovider.py +++ b/python/scriptingprovider.py @@ -152,6 +152,8 @@ class ScriptingInstance: self._cb.setCurrentAddress = self._cb.setCurrentAddress.__class__(self._set_current_address) self._cb.setCurrentSelection = self._cb.setCurrentSelection.__class__(self._set_current_selection) self._cb.completeInput = self._cb.completeInput.__class__(self._complete_input) + self._cb.canCompleteArguments = self._cb.canCompleteArguments.__class__(self._can_complete_arguments) + self._cb.completeArguments = self._cb.completeArguments.__class__(self._complete_arguments) self._cb.stop = self._cb.stop.__class__(self._stop) self._completed_input = None self.handle = core.BNInitScriptingInstance(provider.handle, self._cb) @@ -264,6 +266,29 @@ class ScriptingInstance: logger.log_error_for_exception("Unhandled Python exception in ScriptingInstance._complete_input") return core.BNAllocString("") + def _can_complete_arguments(self, ctx, text): + try: + if not isinstance(text, str): + text = text.decode("utf-8") + return self.perform_can_complete_arguments(text) + except Exception: + logger.log_error_for_exception("Unhandled Python exception in ScriptingInstance._can_complete_arguments") + return False + + def _complete_arguments(self, ctx, text, argument_start): + try: + if not isinstance(text, str): + text = text.decode("utf-8") + result, start = self.perform_complete_arguments(text) + argument_start[0] = start + if result is None: + return None + return core.BNAllocString(result) + except Exception: + logger.log_error_for_exception("Unhandled Python exception in ScriptingInstance._complete_arguments") + argument_start[0] = 0 + return None + def _stop(self, ctxt): try: self.perform_stop() @@ -307,6 +332,14 @@ class ScriptingInstance: return NotImplemented @abc.abstractmethod + def perform_can_complete_arguments(self, text: str) -> bool: + return NotImplemented + + @abc.abstractmethod + def perform_complete_arguments(self, text: str) -> Tuple[Optional[str], int]: + return NotImplemented + + @abc.abstractmethod def perform_stop(self): return NotImplemented @@ -360,6 +393,14 @@ class ScriptingInstance: def complete_input(self, text, state): return core.BNScriptingInstanceCompleteInput(self.handle, text, state) + def can_complete_arguments(self, text): + return core.BNScriptingInstanceCanCompleteArguments(self.handle, text) + + def complete_arguments(self, text): + argument_start = ctypes.c_ulonglong() + result = core.BNScriptingInstanceCompleteArguments(self.handle, text, argument_start) + return result, argument_start.value + def stop(self): core.BNStopScriptingInstance(self.handle) @@ -1039,6 +1080,24 @@ from binaryninja import * return "" return result + @abc.abstractmethod + def perform_can_complete_arguments(self, text: str) -> bool: + try: + self.interpreter.update_locals() + except Exception: + traceback.print_exc() + return self.interpreter.completer.can_complete_arguments(text) + + @abc.abstractmethod + def perform_complete_arguments(self, text: str) -> Tuple[Optional[str], int]: + try: + self.interpreter.update_locals() + except Exception: + traceback.print_exc() + result = self.interpreter.completer.complete_arguments(text) + if result[0] is None: + return None, 0 + return result class PythonScriptingProvider(ScriptingProvider): name = "Python" diff --git a/scriptingprovider.cpp b/scriptingprovider.cpp index 4fd40879..95726d6d 100644 --- a/scriptingprovider.cpp +++ b/scriptingprovider.cpp @@ -56,12 +56,13 @@ void ScriptingOutputListener::NotifyInputReadyStateChanged(BNScriptingProviderIn ScriptingInstance::ScriptingInstance(ScriptingProvider* provider) { - BNScriptingInstanceCallbacks cb; + BNScriptingInstanceCallbacks cb = {}; cb.context = this; cb.destroyInstance = DestroyInstanceCallback; cb.externalRefTaken = nullptr; cb.externalRefReleased = nullptr; cb.executeScriptInput = ExecuteScriptInputCallback; + cb.executeScriptInputFromFilename = ExecuteScriptFromFilenameCallback; cb.cancelScriptInput = CancelScriptInputCallback; cb.releaseBinaryView = ReleaseBinaryViewCallback; cb.setCurrentBinaryView = SetCurrentBinaryViewCallback; @@ -71,6 +72,8 @@ ScriptingInstance::ScriptingInstance(ScriptingProvider* provider) cb.setCurrentSelection = SetCurrentSelectionCallback; cb.completeInput = CompleteInputCallback; cb.stop = StopCallback; + cb.canCompleteArguments = CanCompleteArgumentsCallback; + cb.completeArguments = CompleteArgumentsCallback; AddRefForRegistration(); m_object = BNInitScriptingInstance(provider->GetObject(), &cb); } @@ -96,6 +99,13 @@ BNScriptingProviderExecuteResult ScriptingInstance::ExecuteScriptInputCallback(v } +BNScriptingProviderExecuteResult ScriptingInstance::ExecuteScriptFromFilenameCallback(void* ctxt, const char* filename) +{ + CallbackRef<ScriptingInstance> instance(ctxt); + return instance->ExecuteScriptInputFromFilename(filename); +} + + void ScriptingInstance::CancelScriptInputCallback(void* ctxt) { CallbackRef<ScriptingInstance> instance(ctxt); @@ -162,6 +172,24 @@ char* ScriptingInstance::CompleteInputCallback(void* ctxt, const char* text, uin } +bool ScriptingInstance::CanCompleteArgumentsCallback(void* ctxt, const char* text) +{ + CallbackRef<ScriptingInstance> instance(ctxt); + bool result = instance->CanCompleteArguments(text); + return result; +} + + +char* ScriptingInstance::CompleteArgumentsCallback(void* ctx, const char* text, uint64_t* argumentStart) +{ + CallbackRef<ScriptingInstance> instance(ctx); + auto result = instance->CompleteArguments(text); + if (argumentStart) + *argumentStart = result.second; + return BNAllocString(result.first.c_str()); +} + + void ScriptingInstance::StopCallback(void* ctxt) { CallbackRef<ScriptingInstance> instance(ctxt); @@ -202,6 +230,18 @@ std::string ScriptingInstance::CompleteInput(const std::string&, uint64_t) } +bool ScriptingInstance::CanCompleteArguments(const std::string&) +{ + return false; +} + + +std::pair<std::string, uint64_t> ScriptingInstance::CompleteArguments(const std::string&) +{ + return {"", 0}; +} + + void ScriptingInstance::Output(const string& text) { BNNotifyOutputForScriptingInstance(m_object, text.c_str()); @@ -326,6 +366,24 @@ std::string CoreScriptingInstance::CompleteInput(const std::string& text, uint64 } +bool CoreScriptingInstance::CanCompleteArguments(const std::string& text) +{ + return BNScriptingInstanceCanCompleteArguments(m_object, text.c_str()); +} + + +std::pair<std::string, uint64_t> CoreScriptingInstance::CompleteArguments(const std::string& text) +{ + uint64_t argumentStart = 0; + char* result = BNScriptingInstanceCompleteArguments(m_object, text.c_str(), &argumentStart); + if (!result) + return {"", argumentStart}; + std::string ret = result; + BNFreeString(result); + return {ret, argumentStart}; +} + + void CoreScriptingInstance::Stop() { BNStopScriptingInstance(m_object); diff --git a/ui/scriptingconsole.h b/ui/scriptingconsole.h index 9ebc0463..a403fbd9 100644 --- a/ui/scriptingconsole.h +++ b/ui/scriptingconsole.h @@ -11,7 +11,10 @@ #include <QtWidgets/QListView> #include <QtCore/QAbstractListModel> #include <QtCore/QMimeData> +#include <QTextBrowser> +#include <atomic> #include <mutex> +#include <memory> #include <string> #include <utility> #include <vector> @@ -79,6 +82,33 @@ class BINARYNINJAUIAPI ScriptingCompletionPopup : public QDialog }; /*! + Argument display popup. + + \ingroup scriptingconsole +*/ +class BINARYNINJAUIAPI ScriptingArgumentAssistPopup : public QDialog +{ + Q_OBJECT + + QTextBrowser* m_textDisplay; + QString m_text; + + Q_PROPERTY(QString text READ getText WRITE setText RESET unsetText) + + void updateText(); + +public: + ScriptingArgumentAssistPopup(QWidget* parent); + virtual ~ScriptingArgumentAssistPopup(); + void setText(const QString& text) { m_text = text; updateText(); } + QString getText() const { return m_text; } + void unsetText() { m_text.clear(); updateText(); } + +protected: + virtual bool eventFilter(QObject* obj, QEvent* event) override; +}; + +/*! \ingroup scriptingconsole */ class BINARYNINJAUIAPI ScriptingConsoleEdit : public QTextEdit @@ -87,8 +117,17 @@ class BINARYNINJAUIAPI ScriptingConsoleEdit : public QTextEdit public: typedef std::function<std::vector<std::string>(const std::string&)> CompletionCallback; + typedef std::function<bool(const std::string&)> CanDoCompleteArgumentsCallback; + typedef std::function<std::pair<std::string, uint64_t>(const std::string&)> CompleteArgumentsCallback; private: + struct ArgumentAssistRequestState + { + std::atomic<uint64_t> latestRequest {0}; + std::mutex callbackMutex; + }; + static constexpr int ArgumentAssistDebounceInterval = 100; + ScriptingConsole* m_console; int m_charHeight; bool m_continuation; @@ -97,15 +136,33 @@ class BINARYNINJAUIAPI ScriptingConsoleEdit : public QTextEdit CompletionCallback m_completionCallback; ScriptingCompletionPopup* m_popup; + CanDoCompleteArgumentsCallback m_canDoArgumentCompletionCallback; + CompleteArgumentsCallback m_completeArgumentsCallback; + + ScriptingArgumentAssistPopup* m_argumentAssistPopup; + QTimer* m_argumentAssistUpdateTimer; + std::shared_ptr<ArgumentAssistRequestState> m_argumentAssistState; + bool m_argumentAssistRequestRunning; + bool m_argumentAssistRequestPending; + uint64_t m_argumentAssistPopupPosition; + uint64_t m_completionRegionStart; uint64_t m_completionRegionInitialStop; uint64_t m_completionRegionStop; + bool canShowArgumentAssistPopup() const; + void hideArgumentAssistPopup(); + void scheduleArgumentAssistPopupUpdate(); + void updateArgumentAssistPopup(); + void applyArgumentAssistPopup(const QString& text, uint64_t popupPosition); + public: ScriptingConsoleEdit(ScriptingConsole* parent); void setCharHeight(int height); void setContinuation(bool cont); void setCompletionCallback(CompletionCallback callback) { m_completionCallback = callback; } + void setCanDoCompleteArgumentsCallback(CanDoCompleteArgumentsCallback callback) { m_canDoArgumentCompletionCallback = callback; } + void setCompleteArgumentsCallback(CompleteArgumentsCallback callback) { m_completeArgumentsCallback = callback; } void insertFromMimeData(const QMimeData* source) override; @@ -114,6 +171,11 @@ class BINARYNINJAUIAPI ScriptingConsoleEdit : public QTextEdit protected: virtual void keyPressEvent(QKeyEvent* event) override; + virtual void resizeEvent(QResizeEvent* event) override; + virtual void scrollContentsBy(int dx, int dy) override; + virtual void focusOutEvent(QFocusEvent* event) override; + virtual void hideEvent(QHideEvent* event) override; + virtual void changeEvent(QEvent* event) override; }; /*! diff --git a/ui/uitypes.h b/ui/uitypes.h index f12a48e6..b3131c18 100644 --- a/ui/uitypes.h +++ b/ui/uitypes.h @@ -6,14 +6,14 @@ // there are changes to the API that affect linking, including new functions, // new types, modifications to existing functions or types, or new versions // of the Qt libraries. -#define BN_CURRENT_UI_ABI_VERSION 16 +#define BN_CURRENT_UI_ABI_VERSION 17 // Minimum ABI version that is supported for loading of plugins. Plugins that // are linked to an ABI version less than this will not be able to load and // will require rebuilding. The minimum version is increased when there are // incompatible changes that break binary compatibility, such as changes to // existing types or functions, or a new version of Qt. -#define BN_MINIMUM_UI_ABI_VERSION 16 +#define BN_MINIMUM_UI_ABI_VERSION 17 #ifdef __GNUC__ #ifdef BINARYNINJAUI_LIBRARY |
