mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-08-26 16:36:31 +00:00
Fetch & display tooltips for each entry along with autocomplete options
This commit is contained in:
@@ -1841,9 +1841,10 @@ QString PythonContext::typenameForLoc(int line, int col)
|
||||
return ret;
|
||||
}
|
||||
|
||||
QStringList PythonContext::completionOptions(int line, QString expr, int &prefix_len)
|
||||
QList<QPair<QString, QString>> PythonContext::completionOptions(int line, QString expr,
|
||||
int &prefix_len)
|
||||
{
|
||||
QStringList ret;
|
||||
QList<QPair<QString, QString>> ret;
|
||||
|
||||
PyGILState_STATE gil = PyGILState_Ensure();
|
||||
|
||||
@@ -1864,12 +1865,17 @@ QStringList PythonContext::completionOptions(int line, QString expr, int &prefix
|
||||
{
|
||||
PyObject *comp_list = PyTuple_GetItem(completions, 0);
|
||||
prefix_len = PyLong_AsLong(PyTuple_GetItem(completions, 1));
|
||||
PyObject *tip_list = PyTuple_GetItem(completions, 2);
|
||||
|
||||
if(comp_list)
|
||||
{
|
||||
for(Py_ssize_t i = 0, len = PyList_Size(comp_list); i < len; i++)
|
||||
{
|
||||
ret << ToQStr(PyList_GetItem(comp_list, i));
|
||||
QPair<QString, QString> item;
|
||||
item.first = ToQStr(PyList_GetItem(comp_list, i));
|
||||
if(tip_list)
|
||||
item.second = ToQStr(PyList_GetItem(tip_list, i));
|
||||
ret << item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ public:
|
||||
|
||||
void reflectSource(QString src);
|
||||
QString tooltipForLoc(int line, int col);
|
||||
QStringList completionOptions(int line, QString expr, int &prefix_len);
|
||||
QList<QPair<QString, QString>> completionOptions(int line, QString expr, int &prefix_len);
|
||||
QString tryFunctionCompletion(int line, QString expr);
|
||||
QString typenameForLoc(int line, int col);
|
||||
|
||||
|
||||
@@ -1809,18 +1809,19 @@ class PyReflector:
|
||||
|
||||
try:
|
||||
expr = self._get_atom_expr(self.module, line, col)
|
||||
if expr is not None:
|
||||
loctype = self._get_type(self.scopes[line], expr)
|
||||
else:
|
||||
if expr is None:
|
||||
return ""
|
||||
except:
|
||||
return ""
|
||||
|
||||
if loctype is Any:
|
||||
return ""
|
||||
return self._get_tooltip_for_node(expr, line)
|
||||
|
||||
def _get_tooltip_for_node(self, expr: ast.AST, line: int) -> str:
|
||||
loctype = self._get_type(self.scopes[line], expr)
|
||||
|
||||
if (
|
||||
callable(loctype)
|
||||
loctype is not Any
|
||||
and callable(loctype)
|
||||
and not inspect.isclass(loctype)
|
||||
and not _is_generic(List, loctype)
|
||||
and not _is_generic(Tuple, loctype)
|
||||
@@ -1848,7 +1849,10 @@ class PyReflector:
|
||||
else:
|
||||
ret = "expression: "
|
||||
|
||||
ret += self.get_name(loctype)
|
||||
if loctype is Any:
|
||||
ret += "Unknown Type"
|
||||
else:
|
||||
ret += self.get_name(loctype)
|
||||
|
||||
if docappend != "":
|
||||
ret += "\n\n"
|
||||
@@ -1860,6 +1864,9 @@ class PyReflector:
|
||||
ret = ""
|
||||
|
||||
if _is_generic(Callable, functype):
|
||||
if not hasattr(functype, "__args__"):
|
||||
return "Callable()"
|
||||
|
||||
args = functype.__args__
|
||||
|
||||
retType = args[-1]
|
||||
@@ -1983,11 +1990,13 @@ class PyReflector:
|
||||
ret = ret.replace(" ", " ")
|
||||
return ret.strip()
|
||||
|
||||
def get_autocompletion(self, line: int, expr: str) -> Tuple[List[str], int]:
|
||||
def get_autocompletion(
|
||||
self, line: int, expr: str
|
||||
) -> Tuple[List[str], int, List[str]]:
|
||||
expr = _get_trailing_expr(expr).strip()
|
||||
|
||||
if expr == "":
|
||||
return [], 0
|
||||
return [], 0, []
|
||||
|
||||
trailing_dot = expr[-1] == "."
|
||||
if trailing_dot:
|
||||
@@ -1999,10 +2008,10 @@ class PyReflector:
|
||||
try:
|
||||
node = ast.parse(src)
|
||||
if not isinstance(node, ast.Module):
|
||||
return [], 0
|
||||
return [], 0, []
|
||||
node = node.body[0]
|
||||
if not isinstance(node, ast.Expr):
|
||||
return [], 0
|
||||
return [], 0, []
|
||||
node = node.value
|
||||
|
||||
curscope = self.scopes[min(len(self.scopes) - 1, line)]
|
||||
@@ -2010,32 +2019,43 @@ class PyReflector:
|
||||
ret: List[str] = []
|
||||
prefix_filter = ""
|
||||
|
||||
def get_member_tooltip(member_name: str):
|
||||
if isinstance(node, ast.Attribute):
|
||||
node.attr = member_name
|
||||
return self._get_tooltip_for_node(node, line)
|
||||
elif isinstance(node, ast.Name):
|
||||
node.id = member_name
|
||||
return self._get_tooltip_for_node(node, line)
|
||||
else:
|
||||
raise RuntimeError("Unexpected node type getting member tooltip")
|
||||
|
||||
# for just a name, filter the identifiers at this point if there was no trailing dot
|
||||
if isinstance(node, ast.Name) and not trailing_dot:
|
||||
idents = []
|
||||
prefix_filter = node.id
|
||||
while curscope is not None:
|
||||
for k, v in curscope.identifiers.items():
|
||||
if any([x.line <= line for x in v]):
|
||||
idents.append(k)
|
||||
ret.append(k)
|
||||
curscope = curscope.parent
|
||||
ret = idents
|
||||
prefix_filter = node.id
|
||||
else:
|
||||
# we provide completion for attribute access, which can either look like just a
|
||||
# name (if there was a trailing dot so we didn't hit the case above)
|
||||
base_type = Any
|
||||
prefix_filter = ""
|
||||
|
||||
if isinstance(node, ast.Name) or trailing_dot:
|
||||
base_type = self._get_type(curscope, node)
|
||||
|
||||
node = ast.Attribute(node, "", lineno=node.lineno)
|
||||
elif isinstance(node, ast.Attribute):
|
||||
base_type = self._get_type(curscope, node.value)
|
||||
prefix_filter = node.attr
|
||||
else:
|
||||
return [], 0
|
||||
return [], 0, []
|
||||
|
||||
# if the base type is unknown in some fashion, nothing to do
|
||||
if base_type is Any or base_type is None:
|
||||
return [], 0
|
||||
return [], 0, []
|
||||
|
||||
# if this is a user type, look up its identifiers from our list
|
||||
if isinstance(base_type, TypeVar):
|
||||
@@ -2044,7 +2064,7 @@ class PyReflector:
|
||||
base_scope = self.scopes[base_ident.line]
|
||||
ret = list(base_scope.identifiers.keys())
|
||||
else:
|
||||
return [], 0
|
||||
return [], 0, []
|
||||
else:
|
||||
# pre-python 3.8 Dict, List etc are not the real types, substitute here
|
||||
if sys.version_info < (3, 9):
|
||||
@@ -2072,11 +2092,11 @@ class PyReflector:
|
||||
# sort alphabetically, case-insensitively
|
||||
ret = sorted(ret, key=lambda x: x.upper())
|
||||
|
||||
return ret, len(prefix_filter)
|
||||
return ret, len(prefix_filter), [get_member_tooltip(x) for x in ret]
|
||||
|
||||
except:
|
||||
pass
|
||||
return [], 0
|
||||
return [], 0, []
|
||||
|
||||
def get_funccompletion(self, line: int, expr: str) -> Tuple[str, str, str]:
|
||||
func, argidx = _get_func_arg(_get_trailing_expr(expr + ")"))
|
||||
@@ -2151,6 +2171,8 @@ class PyReflector:
|
||||
]
|
||||
for g, n in generics:
|
||||
if _is_generic(g, obj):
|
||||
if not hasattr(obj, "__args__"):
|
||||
return n
|
||||
args = ", ".join([self.get_name(a) for a in obj.__args__])
|
||||
return f"{n}[{args}]"
|
||||
|
||||
@@ -2410,7 +2432,7 @@ if __name__ == "__main__" and sys.version_info >= (3, 8):
|
||||
if "# AUTOCOMPLETE TEST" in line_text and not "#exclude" in line_text:
|
||||
line = i + 1
|
||||
|
||||
completions, prefix_len = refl.get_autocompletion(line, entry)
|
||||
completions, prefix_len, tooltips = refl.get_autocompletion(line, entry)
|
||||
|
||||
if expected_prefix_len != prefix_len:
|
||||
raise RuntimeError(
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QFontDatabase>
|
||||
#include <QKeyEvent>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QScrollBar>
|
||||
#include <QStringListModel>
|
||||
@@ -222,6 +223,32 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
|
||||
m_SyntaxCheckTimer->setSingleShot(true);
|
||||
m_SyntaxCheckTimer->setInterval(1200);
|
||||
|
||||
m_CompletionTipTimer = new QTimer(this);
|
||||
m_CompletionTipTimer->setSingleShot(false);
|
||||
// this timer will only be active while auto completing so we can be aggressive with its timer
|
||||
m_CompletionTipTimer->setInterval(10);
|
||||
QObject::connect(m_CompletionTipTimer, &QTimer::timeout, [this]() {
|
||||
EditorWrapper *editor = curEditor();
|
||||
|
||||
if(!editor || !editor->scintilla()->autoCActive())
|
||||
{
|
||||
m_CompletionTipTimer->stop();
|
||||
m_CompletionTipList.clear();
|
||||
m_CurrentCompletionTip = -1;
|
||||
|
||||
updateCompletionTip();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(editor->scintilla()->autoCCurrent() != m_CurrentCompletionTip)
|
||||
{
|
||||
m_CurrentCompletionTip = editor->scintilla()->autoCCurrent();
|
||||
|
||||
updateCompletionTip();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// only update the current line intermittently. We don't need to update every single time and if
|
||||
// there is a large number of traces this will rate limit it.
|
||||
m_CurLineTimer = new QTimer(this);
|
||||
@@ -271,6 +298,9 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
|
||||
|
||||
m_ToolTip->setFont(Formatter::FixedFont());
|
||||
|
||||
m_CompletionTip = new RDToolTip(this);
|
||||
m_CompletionTip->setFont(Formatter::FixedFont());
|
||||
|
||||
m_InteractiveCompleter = new QCompleter(this);
|
||||
m_InteractiveCompleter->popup()->setFont(Formatter::FixedFont());
|
||||
m_InteractiveCompleter->popup()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
|
||||
@@ -282,6 +312,21 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
|
||||
m_InteractiveCompleter->setModel(m_InteractiveCompletionModel);
|
||||
m_InteractiveCompleter->setCompletionRole(Qt::DisplayRole);
|
||||
|
||||
m_InteractiveCompleter->popup()->installEventFilter(this);
|
||||
|
||||
QObject::connect(m_InteractiveCompleter,
|
||||
OverloadedSlot<const QModelIndex &>::of(&QCompleter::highlighted),
|
||||
[this](const QModelIndex &idx) {
|
||||
if(idx.isValid() && idx.row() < m_CompletionTipList.count())
|
||||
{
|
||||
if(m_CurrentCompletionTip != idx.row())
|
||||
{
|
||||
m_CurrentCompletionTip = idx.row();
|
||||
updateCompletionTip();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(m_InteractiveCompleter,
|
||||
OverloadedSlot<const QModelIndex &>::of(&QCompleter::activated),
|
||||
[this](const QModelIndex &idx) {
|
||||
@@ -1084,6 +1129,13 @@ bool PythonShell::eventFilter(QObject *watched, QEvent *event)
|
||||
}
|
||||
}
|
||||
|
||||
if(m_InteractiveCompleter && watched == m_InteractiveCompleter->popup() &&
|
||||
(event->type() == QEvent::Hide || event->type() == QEvent::FocusOut))
|
||||
{
|
||||
m_CurrentCompletionTip = -1;
|
||||
updateCompletionTip();
|
||||
}
|
||||
|
||||
if(m_FuncTip && watched == m_FuncTipWidget && event->type() == QEvent::FocusOut)
|
||||
{
|
||||
hideFunccompleteTooltip();
|
||||
@@ -2185,9 +2237,20 @@ void PythonShell::interactive_keypress(QKeyEvent *event)
|
||||
QString base = ui->lineInput->text();
|
||||
|
||||
QStringList completions;
|
||||
int oldCount = m_CompletionTipList.count();
|
||||
m_CompletionTipList.clear();
|
||||
|
||||
if(base.trimmed() != QString())
|
||||
completions = interactiveContext->completionOptions(0, base, m_InteractiveCompletionPrefix);
|
||||
{
|
||||
m_CompletionTipList =
|
||||
interactiveContext->completionOptions(0, base, m_InteractiveCompletionPrefix);
|
||||
|
||||
for(const QPair<QString, QString> &item : m_CompletionTipList)
|
||||
completions << item.first;
|
||||
}
|
||||
|
||||
if(oldCount != m_CompletionTipList.count())
|
||||
m_CurrentCompletionTip = -1;
|
||||
|
||||
if(completions.isEmpty())
|
||||
{
|
||||
@@ -2241,6 +2304,8 @@ void PythonShell::interactive_keypress(QKeyEvent *event)
|
||||
ui->lineInput->style()->pixelMetric(QStyle::PM_ButtonMargin));
|
||||
|
||||
m_InteractiveCompleter->complete(r);
|
||||
m_InteractiveCompleter->popup()->selectionModel()->setCurrentIndex(
|
||||
m_InteractiveCompletionModel->index(0), QItemSelectionModel::ClearAndSelect);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -2355,18 +2420,33 @@ void PythonShell::doAutocomplete(ScintillaEdit *editor)
|
||||
QByteArray lineText = editor->getLine(line);
|
||||
lineText.resize(pos - lineStart);
|
||||
|
||||
int oldCount = m_CompletionTipList.count();
|
||||
|
||||
int prefix_len = 0;
|
||||
QStringList completions =
|
||||
m_CompletionTipList =
|
||||
completionContext->completionOptions(line, QString::fromUtf8(lineText), prefix_len);
|
||||
|
||||
if(completions.empty())
|
||||
if(m_CompletionTipList.empty())
|
||||
{
|
||||
doFunccomplete(editor);
|
||||
return;
|
||||
}
|
||||
|
||||
QString completion_merged;
|
||||
for(const QPair<QString, QString> &item : m_CompletionTipList)
|
||||
{
|
||||
completion_merged += item.first;
|
||||
completion_merged += QLatin1Char(' ');
|
||||
}
|
||||
completion_merged.remove(completion_merged.count() - 1, 1);
|
||||
|
||||
if(oldCount != m_CompletionTipList.count())
|
||||
m_CurrentCompletionTip = -1;
|
||||
|
||||
hideFunccompleteTooltip();
|
||||
editor->autoCShow(prefix_len, completions.join(QLatin1Char(' ')).toUtf8().data());
|
||||
editor->autoCShow(prefix_len, completion_merged.toUtf8().data());
|
||||
// scintilla doesn't give us a callback/event when an item is highlighted, so we query in a timer
|
||||
m_CompletionTipTimer->start();
|
||||
}
|
||||
|
||||
void PythonShell::doFunccomplete(ScintillaEdit *editor)
|
||||
@@ -2409,6 +2489,39 @@ void PythonShell::hideFunccompleteTooltip()
|
||||
m_SyntaxCheckTimer->start();
|
||||
}
|
||||
|
||||
void PythonShell::updateCompletionTip()
|
||||
{
|
||||
if(m_CompletionTipList.empty() || m_CurrentCompletionTip < 0 ||
|
||||
m_CurrentCompletionTip >= m_CompletionTipList.count())
|
||||
{
|
||||
m_CurrentCompletionTip = -1;
|
||||
m_CompletionTip->hideTip();
|
||||
return;
|
||||
}
|
||||
|
||||
m_CompletionTip->configureTip(this, m_CompletionTipList[m_CurrentCompletionTip].second);
|
||||
{
|
||||
QPoint pos;
|
||||
if(m_InteractiveCompleter->popup()->isVisible())
|
||||
{
|
||||
pos = m_InteractiveCompleter->popup()->mapToGlobal(
|
||||
m_InteractiveCompleter->popup()->rect().topRight());
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorWrapper *editor = curEditor();
|
||||
|
||||
if(editor)
|
||||
{
|
||||
pos = QPoint(editor->scintilla()->autoCRectRight(), editor->scintilla()->autoCRectTop());
|
||||
}
|
||||
}
|
||||
|
||||
if(pos != QPoint())
|
||||
m_CompletionTip->showTipAtPos(pos);
|
||||
}
|
||||
}
|
||||
|
||||
PythonContext *PythonShell::newContext()
|
||||
{
|
||||
PythonContext *ret = new PythonContext();
|
||||
|
||||
@@ -186,10 +186,15 @@ private:
|
||||
|
||||
QTimer *m_SyntaxCheckTimer;
|
||||
|
||||
QCompleter *m_InteractiveCompleter;
|
||||
QCompleter *m_InteractiveCompleter = NULL;
|
||||
QStringListModel *m_InteractiveCompletionModel;
|
||||
int m_InteractiveCompletionPrefix = 0;
|
||||
|
||||
RDToolTip *m_CompletionTip;
|
||||
QList<QPair<QString, QString>> m_CompletionTipList;
|
||||
sptr_t m_CurrentCompletionTip = -1;
|
||||
QTimer *m_CompletionTipTimer;
|
||||
|
||||
static const int CURRENT_MARKER = 0;
|
||||
static const int STYLE_ERROR = 100;
|
||||
|
||||
@@ -245,6 +250,7 @@ private:
|
||||
void doFunccomplete(ScintillaEdit *editor);
|
||||
|
||||
void hideFunccompleteTooltip();
|
||||
void updateCompletionTip();
|
||||
|
||||
void selectedHelp(QString word);
|
||||
void refreshCurrentHelp();
|
||||
|
||||
Reference in New Issue
Block a user