summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author0cyn <kat@vector35.com>2026-04-14 05:03:15 -0400
committerAlexander Taylor <alex@vector35.com>2026-05-18 14:55:52 -0400
commit297a98bff289ecde90d808f6538c2c5edf804af6 (patch)
tree4ca5f9868354cb26f7c6ef70140e4ed0f0395944
parentf1a688a1e7fb6d962d8c9addaaefdfd59e5dd28b (diff)
Argument Assist Popup
-rw-r--r--binaryninjaapi.h6
-rw-r--r--binaryninjacore.h8
-rw-r--r--python/bncompleter.py415
-rw-r--r--python/scriptingprovider.py59
-rw-r--r--scriptingprovider.cpp60
-rw-r--r--ui/scriptingconsole.h62
-rw-r--r--ui/uitypes.h4
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"> -&gt; {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