Fetch & display tooltips for each entry along with autocomplete options

This commit is contained in:
baldurk
2026-08-13 21:05:14 +01:00
parent 586886b3f5
commit b3710eba3b
5 changed files with 177 additions and 30 deletions
@@ -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;
}
}
}
+1 -1
View File
@@ -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);
+43 -21
View File
@@ -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(" ", "&nbsp;&nbsp;")
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(
+117 -4
View File
@@ -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();
+7 -1
View File
@@ -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();